이번에는 간단한 오버레이 뷰를 만들어 보도록 하자


안드로이드 6.0이후의 기기는 오버레이 기능을 위해 퍼미션을 받아야 한다.


하지만 그 방식은 다른 퍼미션을 받는 방식과는 약간 다르다.


다른 퍼미션(카메라나 저장장치)는 퍼미션 허가창만 띄우면 가능하지만 오버레이는 설정창을 띄워주어야 한다.


코드로 확인한다면 아래와 같다.


public void openView() {
if(Settings.canDrawOverlays(this))
startService(new Intent(this, FloatingViewService.class));
else
onObtainingPermissionOverlayWindow();
}
public void onObtainingPermissionOverlayWindow() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQ_CODE_OVERLAY_PERMISSION);
}

오버레이 뷰를 실행할 때는  openView()함수를 호출할 예정이다.


Settings.canDrawOverlays(this)를 호출하면 현재 앱이 오버레이를 띄울수 있는지 확인하는 단계이다.


만일 현재 오버레이를 띄울수 있다면 오버레이를 호출하고, 띄울수없다면 오버레이 퍼이션을 받아야 한다.


오버레이 퍼미션은 onObtainingPermissionOverlayWindow()함수 내부 처럼 두줄로 받을 수 있다.


오버레이 퍼미션의 더 자세한 내용은 아래 블로그에서 확인 가능하다. 


꿈 많은 개발자가 되자 : http://thdev.tech/androiddev/2017/01/30/Android-Overlay-Permission.html





오버레이 뷰는 다른 액티비티가 종료되어도 동작해야하기 때문에 Service에서 동작시킨다. 서비스가 실행된다면 오버레이 뷰를 실행시킨다.



@Override
public void onCreate() {
super.onCreate();

LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
onTopView = inflater.inflate(R.layout.always_on_top_layout, null);
onTopView.setOnTouchListener(this);

WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL|
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);

params.gravity = Gravity.LEFT | Gravity.TOP;

manager = (WindowManager) getSystemService(WINDOW_SERVICE);
manager.addView(onTopView, params);

Button closeBtn = onTopView.findViewById(R.id.close_this_window);
closeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
manager.removeView(onTopView);
onTopView = null;
stopSelf();
}
});
}

위는 서비스의 onCreate이다. 서비스가 시작되면 레이아웃을 설정하고 레이아웃 파라메터를 설정해주면 된다.


그리고 버튼은 현재 뷰를 지워주는 역활을 하게되는데 뷰와 서비스를 종료하면 된다.





마지막으로 뷰의 이동을 구현한다.


switch (action) {
case MotionEvent.ACTION_DOWN:
if (pointerCount == 1) {
xpos = motionEvent.getRawX();
ypos = motionEvent.getRawY();
}
break;
case MotionEvent.ACTION_MOVE:
if (pointerCount == 1) {
WindowManager.LayoutParams lp = (WindowManager.LayoutParams) view.getLayoutParams();
float dx = xpos - motionEvent.getRawX();
float dy = ypos - motionEvent.getRawY();
xpos = motionEvent.getRawX();
ypos = motionEvent.getRawY();

Log.d(TAG, "lp.x : " + lp.x + ", dx : " + dx + "lp.y : " + lp.y + ", dy : " + dy);

lp.x = (int) (lp.x - dx);
lp.y = (int) (lp.y - dy);

manager.updateViewLayout(view,lp);
return true;
}
break;

}

onTouch내부에  switch함수를 넣어 구성하였다. layoutparam을 변경해주어 이동시키면 된다.



GitHub : https://github.com/pchan1401-ICIL/AlwaysOnTopViewTest

개발자가 실내에서 GPS를 이용한 프로그램을 개발하는 경우 GPS를 가상위치에 놓아야 하는 상황이 발생한다. 


이럴때 가장 쉬운 방법은 가상GPS프로그램을 사용하면 된다.


하지만, 이러한 프로그램은 자신의 위치를 계속적으로 컨트롤 하거나 개발자가 필요한 기능을 가지지 않는 경우가 있다.


예를들어 반복적인 위치 움직임을 필요로하는 경우가 있다. 이러한 프로그램은 찾아도 없었다. 따라서 직접 만들어보았다.





해당 프로그램은 백그라운드 작업을 필요로한다. 액티비티가 종료되어도 사용자의 위치는 계속적으로 나타나져야 한다. 따라서 아래와 같이 서비스를 이용하였다.

public class LocationUpdateService extends IntentService




아래는 본격적인 가상위치를 제공하는 코드이다.

public MockLocationProvider(String name, Context ctx) {
providerName = name;
mctx = ctx;

LocationManager lm = (LocationManager) ctx.getSystemService(
Context.LOCATION_SERVICE);
lm.addTestProvider(providerName, true, true, true, true, true,
true, true, Criteria.NO_REQUIREMENT, Criteria.ACCURACY_FINE);
lm.setTestProviderEnabled(providerName, true);
}

가상위치를 설정해주기 위하여 초기화하는 부분이다. addTestProvider함수로 가상위치를 사용할 준비를 한다.


addTestProvider함수는 여러변수를 가지는데 위치제공자의 이름, 필요한 것, 제공하는 값을 나타낸다. 자세한 내용은 아래 링크를 참조하면 된다.



https://developer.android.com/reference/android/location/LocationManager.html#addTestProvider(java.lang.String, boolean, boolean, boolean, boolean, boolean, boolean, boolean, int, int)





아래는 가상위치의 값들을 설정하는 코드이다.

public void pushLocation(double lat, double lon, double alt) {
try {
LocationManager lm = (LocationManager) mctx.getSystemService(
Context.LOCATION_SERVICE);

Location mockLocation = new Location(providerName);
long currentTime = System.currentTimeMillis();
mockLocation.setLatitude(lat);
mockLocation.setLongitude(lon);
mockLocation.setAltitude(alt);
mockLocation.setTime(currentTime);
mockLocation.setAccuracy(1.0f);
mockLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());

lm.setTestProviderStatus(providerName, LocationProvider.AVAILABLE, mockLocation.getExtras(), currentTime);
lm.setTestProviderLocation(providerName, mockLocation);
} catch(RuntimeException e){
e.printStackTrace();
}
}

여러 값을 설정하고 이 값들을 setTestProviderLocation에 넣으면 GPS값이 출력된다.


pushLocation을 지속적으로 호출하여 사용자의 위치를 계속 갱신한다.






마지막으로 Manifest파일에 가상위치를 사용한다고 권한을 설정해주어야한다.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>

ACCES_COARSE_LOCATION과 ACCES_FINE_LOCATION은 위치제공자를 사용하기 위해 필요하다.


ACCES_MOCK_LOCATION은 가상위치를 사용하기 위해 필요하다.






테스트를 하기 위하여 위치사용허가를 해야한다. 


설정 > 시스템 > 개발자옵션 > 모의위치 앱에서 TestMockLocation을 선택한다.


이 내용은 안드로이드 버전에서 약간씩 다르다.






아래 화면에서 버튼을 누르면 가상 위치가 실행된다.


버튼을 누르면 실행


이 상태에서 지도(카카오맵)을 켜면 사용자의 위치가 나타난다.


카카오맵


위치값을 읽는 코드를 이용하여 제대로 작동하는지 확인한다.


이 코드는 아래 github링크를 놓겠다.


받는 데이터




마지막 사진에서 가상위치라고 isFromMockProvider가 true로 나온다. 실제 위치일 경우 false가 나타난다.




이처럼 개발자는 사무실에 있는데 광화문 한복판에 있다.


이러한 기능을 이용하여 GPS를 사용한 앱의 디버깅을 쉽게 할 수 있다.






아래는 참고한 웹페이지이다.


https://mobiarch.wordpress.com/2012/07/17/testing-with-mock-location-data-in-android/





Github


가상위치 : https://github.com/pchan1401-ICIL/TestMockLocation


GPS읽기 : https://github.com/pchan1401-ICIL/LocationReader










코드 : https://github.com/pchan1401-ICIL/Camera1_2_Compare



이번 글은 단순히 Camera2와 Camera API의 Fov를 비교하는 글이다.


최근에 Camera2를 사용하던 코드른 Camera API로 변환하는 과정에서 어떠한 값을 나타내는지 비교하는 것이다.


Camera 코드의 Fov를 구하는 코드는 많이 있으니 생략한다.


두 API의 차이점은 아래와 같다.


 - Camera : API가 이미 존재하여 값을 불러온다.

 - Camera2 : 센서의 크기를 이용하여 계산한다.


그리고 결과는 아래와 같다.


갤럭시탭 S3


한번 다른 기기를 이용해보았다


갤럭시 노트4




삼성기기만 그러한지는 모르겠으나 두 기기를 비교해보면 Camera와 Camera2의 FOV출력은 정확히 같은계산을 하는 것 같다.






안드로이드에서 플레이하기 위해서 VLC라이브러리를 빌드해야 한다.


빌드 방법은 VLC공식 사이트에 나와있다.


https://wiki.videolan.org/AndroidCompile/


리눅스에서 빌드를 해야하기 때문에 번거롭다.


그래서 아래에 빌드 결과물을 받을 수 있도록 하였다. 이 라이브러리는 구버전이기 때문에 직접 받아서 빌드하기를 추천한다.


libvlc.z01

libvlc.zip



이렇게 얻은 libvlc를 프로젝트에 추가한다.




우선 액티비티에 여러 인자를 Implement한다.

public class MainActivity extends AppCompatActivity implements IVLCVout.Callback, LibVLC.HardwareAccelerationError, TextureView.SurfaceTextureListener 

IVLCVout.Callback, LibVLC.HardwareAccelerationError, TextureView.SurfaceTextureListener를 implement한 이유는 콜백함수를 사용하기 위함이다.


각각의 기능은 아래에 설명한다.




IVLCVout.Callback는 비디오 실행시 콜백함수에 해당한다. 이 것을 위하여 아래의 세 함수를 작성해 주어야 한다.

@Override //VLC 레이아웃 설정
public void onNewLayout(IVLCVout vout, int width, int height, int visibleWidth, int visibleHeight, int sarNum, int sarDen) {
if (width * height == 0)
return;
// store video size
mVideoWidth = width;
mVideoHeight = height;
setSize(mVideoWidth, mVideoHeight);
}

@Override
public void onSurfacesCreated(IVLCVout ivlcVout) {

}

@Override
public void onSurfacesDestroyed(IVLCVout ivlcVout) {

}

위 세 함수는 각각 새 레이아웃 생성시, 화면 생성시, 화면 제거시 각각 동작한다.


비디오를 실행하면 비디오의 크기를 변경해주어야 한다. 비디오 사이즈는 TextureView의 사이즈를 변경해주면 된다.


LibVLC.HardwareAccelerationError는 하드웨어 가속에 에러가 생기면 어떠한 동작을 하는지 나타낸다.

@Override  //하드웨어 가속 에러시 플레이어 종료
public void eventHardwareAccelerationError() {
releasePlayer();
Toast.makeText(this, "Error with hardware acceleration", Toast.LENGTH_LONG).show();
}

하드웨어 가속중 에러가 생기면 플레이어를 종료하고 에러 토스트 메시지를 띄운다.


마지막으로 SurfaceTextureListener는 TextureView의 상태에 따라 동작하는 함수이다.

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

}

@Override //SurfaceTexture 화면(동영상 해상도 및 사이즈)에 따라 크기 변경
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
setSize(mVideoWidth, mVideoHeight);
}

@Override //SurfaceTexture 화면이 종료되었을때 종료
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
if(!(mMediaPlayer == null))
mMediaPlayer.release();
return true;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

각각 텍스쳐뷰의 사용가능, 텍스쳐뷰의 크기 변화, 텍스쳐뷰의 제거, 텍스쳐뷰의 업데이트 시에 실행한다.


크기가 변하면(orientation의 변화) 동영상의 크기를 변화시킨다.


또한 텍스쳐뷰가 제거가 되면 비디오를 종료한다.



다음은 Mediaplayer.EventListener이다. 재생중에 어떠한 일이 발생하면 실행하는 함수이다.

//미디어 플레이어 리스너 클래스
private static class MyPlayerListener implements MediaPlayer.EventListener {
private WeakReference<MainActivity> mOwner;

//액티비티 변수를 받아오기 위하여 지정
private MyPlayerListener(MainActivity owner) {
mOwner = new WeakReference<>(owner);
}

@Override
public void onEvent(MediaPlayer.Event event) {
MainActivity player = mOwner.get();

switch(event.type) {
case MediaPlayer.Event.EndReached:
player.releasePlayer();
break;
case MediaPlayer.Event.Playing:
player.mSeekBar.setMax((int) player.mMediaPlayer.getLength());
player.mSeekBar.setOnSeekBarChangeListener(player.mSeekListener);
break;
case MediaPlayer.Event.Paused:
break;
case MediaPlayer.Event.Stopped:
break;
case MediaPlayer.Event.PositionChanged:
player.mSeekBar.setProgress((int)player.mMediaPlayer.getTime());
default:
break;
}
}
}

이 함수는 동영상이 플레이되는 중에 계속 호출된다.





아래는 VLC플레리어 실행을 위한 코드이다. 초기화 정보가 들어있다.

//VLC 플레이어 실행
private void createPlayer(String media) {
releasePlayer();
try {
if (media.length() > 0) {
Toast toast = Toast.makeText(this, media, Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0,
0);
toast.show();
}

// Create LibVLC
ArrayList<String> options = new ArrayList<>();
//options.add("--subsdec-encoding <encoding>");
options.add("--aout=opensles");
options.add("--audio-time-stretch"); // time stretching
options.add("-vvv"); // verbosity
libvlc = new LibVLC(options);
libvlc.setOnHardwareAccelerationError(this);

mTexture.setKeepScreenOn(true);

// Create media player
mMediaPlayer = new MediaPlayer(libvlc);
mMediaPlayer.setEventListener(mPlayerListener);

// Set up video output
final IVLCVout vout = mMediaPlayer.getVLCVout();
vout.setVideoView(mTexture);
//vout.setSubtitlesView(mSurfaceSubtitles);
vout.addCallback(this);
vout.attachViews();

Media m = new Media(libvlc, media);
mMediaPlayer.setMedia(m);
mMediaPlayer.play();

} catch (Exception e) {
Toast.makeText(this, "Error creating player!", Toast.LENGTH_LONG).show();
}
}

코드를 요약하자면 비디오 실행 옵션설정, 콜백함수 설정, 플레이 할 비디오를 설정하고 재생시킨다.


여러가지 옵션는 VLC홈페이지에 나와있다.



VLC의 미디어플레이어는 안드로이드의 미디어플레이어와 사용법이 유사하다.


게다가 여러 재생옵션 기능을 제공한다. 


하지면 여러 콜백함수가 없어서 필요한 코드는 작성해주어야 한다.


이제 프로그램을 실행하고 play버튼을 누르면 동영상이 재생되면서 볼 수 있다.


모든 코드는 Github에 올려져있다.


Github : https://github.com/pchan1401-ICIL/VLCTestProject



출처 : http://androidarena.co.in/how-to-clear-or-kill-stack-of-activity-android/





Intent intent = new Intent(getApplicationContext(), NewActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

startActivity(intent);


새로운 액티비티 스택을 지우면서 새로운 액티비티를 생성하려면 인텐트에 플래그에 입력한다.


인터넷에서는 작동하는 경우도 있다.


하지만 어떤경우에서 작동이 안하는 경우가 있는데 아마 백그라운드에서 애니메이션처리같이 계속 동작하는 액티비티는 중지하지 않는 것 같다.


이런 경우 AndroidManifests.xml에 지워야 할 액티비티에 아래와같이 추가하면 된다.


android:noHistory="true"

위와 같이 추가하고 실행하면 액티비티 스택에 저장되지 않는다.


http://stackoverflow.com/a/21375102


위의 링크에서도 finish() 보다는 더 적절한 noHistory 설정을 적용시키는게 옳다고 나온다.

안드로이드 센서를 이용하다 보면 센서 값들에 정확도에 문제가 있다.


가속도계는 노이즈가 심하고, 자이로는 바이어스가 있다.


이러한 단점을 보완하기 위하여 센서를 합성해야 한다.


센서 합성은 수학적 지식이 있어야 한다.


보통은 그러한 지식이 없기 때문에 안드로이드에서는 내장함수로 계산을 도와준다.


Rotation Vector는 하나의 방향벡터와 방향벡터의 회전정도를 준다.


[출처 : https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation]


위 그림처럼 e는 방향을 나타내고, θ는 벡터가 시계 방향으로 회전한 정도를 나타낸다.


Android Developer : https://developer.android.com/reference/android/hardware/SensorEvent.html#values


안드로이드에서는 센서와 같게 사용하면 된다.

mSensorManager.registerListener(mSensorListener, mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR), SensorManager.SENSOR_DELAY_UI);

value에서 출력되는 값은 다음과 같다.


  • values[0]: x*sin(θ/2)
  • values[1]: y*sin(θ/2)
  • values[2]: z*sin(θ/2)
  • values[3]: cos(θ/2)
  • values[4]: estimated heading Accuracy (in radians) (-1 if unavailable)

기존에 사용한 Orientation Sensor는 자이로를 사용하지 않아서 노이즈가 심했지만, Rotation Vector는 세개의 센서를 이용하여 각 센서의 단점을 보완했다



이전에 있던 Camera2 프리뷰 기능에 캡쳐기능을 추가하였다.


우선 코드는 책(빛과 소리의 향연 안드로이드 프로그래밍, 박헌재)의 코드를 사용하였다.


코드가 간단할 뿐만 아니라, 블로그에 있던 코드와 거의 흡사하여 적용이 용이하다.


기존의 코드에서 takePicture함수만 추가되었다.


코드 설명에 앞서 Camera2의 특징은 출력을 여러가지로 할 수 있다.


하나의 카메라의 출력을 프리뷰, 사진, 동영상으로 전송할 수 있다.


각각의 출력을 surface라고 한다.


여러 surface를 지정하여 다른 해상도의 사진을 동시에 촬영 가능하다.


또한 동영상을 촬영하는 동안 사진을 저장할 수 있다.


그럼 코드를 보자


protected void takePicture() {
if(null == mCameraDevice) {
Log.e(TAG, "mCameraDevice is null, return");
return;
}

try {
Size[] jpegSizes = null;
if (map != null) {
jpegSizes = map.getOutputSizes(ImageFormat.JPEG);
}
int width = 640;
int height = 480;
if (jpegSizes != null && 0 < jpegSizes.length) {
width = jpegSizes[0].getWidth();
height = jpegSizes[0].getHeight();
}

ImageReader reader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
List<Surface> outputSurfaces = new ArrayList<Surface>(2);
outputSurfaces.add(reader.getSurface());
outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture()));

final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(reader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);

// Orientation
int rotation = ((Activity)mContext).getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));

final File file = new File(Environment.getExternalStorageDirectory()+"/DCIM", "pic.jpg");

ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = null;
try {
image = reader.acquireLatestImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
save(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (image != null) {
image.close();
reader.close();
}
}
}

private void save(byte[] bytes) throws IOException {
OutputStream output = null;
try {
output = new FileOutputStream(file);
output.write(bytes);
} finally {
if (null != output) {
output.close();
}
}
}
};

HandlerThread thread = new HandlerThread("CameraPicture");
thread.start();
final Handler backgroudHandler = new Handler(thread.getLooper());
reader.setOnImageAvailableListener(readerListener, backgroudHandler);

final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session,
CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
Toast.makeText(mContext, "Saved:"+file, Toast.LENGTH_SHORT).show();
startPreview();
}

};

mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
session.capture(captureBuilder.build(), captureListener, backgroudHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

@Override
public void onConfigureFailed(CameraCaptureSession session) {

}
}, backgroudHandler);

} catch (CameraAccessException e) {
e.printStackTrace();
}
}


중간에 outputSurface를 설정하여 이곳에서 출력을 사진과 프리뷰를 설정하였다.


사진을 찍는 동시에 프리뷰를 보는 동작을 하기 위해서이다.


찍은 사진은 ImageReader클래스를 이용하여 출력을 한다.


ImageReader에서 출력된 사진은 JPEG 형식으로 저장된다.



좀 더 복잡한 코드는 다음과 같다.


Camera2Basic : https://github.com/googlesamples/android-Camera2Basic


mCaptureCallback을 이용하여 최적화가 잘 되어있다. 



GitHub : https://github.com/pchan1401-ICIL/picture2.git









기존의 Camera API는 카메라를 카메라로 보았다면


Android 5.0이상에서는 카메라는 센서처럼 취급한다.


camera2 api 강좌 : http://myandroidarchive.tistory.com/1


우선 카메라의 화각 계산 공식은 아래 사이트에서 볼 수 있다.


Calculation Field of View : http://www.bobatkins.com/photography/technical/field_of_view.html


이전 코드와 변경사항은 아래 코드가 추가되었다는 점이다.


float horizonalAngle;
float verticalAngle;

private void calculateFOV(CameraManager cManager) {
try {
for (final String cameraId : cManager.getCameraIdList()) {
CameraCharacteristics characteristics = cManager.getCameraCharacteristics(cameraId);
int cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cOrientation == CameraCharacteristics.LENS_FACING_BACK) {
float[] maxFocus = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
SizeF size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
float w = size.getWidth();
float h = size.getHeight();
horizonalAngle = (float) (2*Math.atan(w/(maxFocus[0]*2)));
verticalAngle = (float) (2*Math.atan(h/(maxFocus[0]*2)));
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

먼저, LENS_INFO_AVAILABLE_FOCUS_LENGTHS는 카메라가 사용가능한 렌즈의 초점거리를 얻기 위하여 사용하였다.


다음으로 SENSOR_INFO_PHYSICAL_SIZE는 센서의 크기를 불러오기 위해 사용하였다.


이 값을 이용하여 수직, 수평 FOV각도 값을 구할 수 있다.


FOV (rectilinear) =  2 * arctan (frame size/(focal length * 2))



GitHub : https://github.com/pchan1401-ICIL/Camera2FOV

pcm데이터를 이용한 플레이어이다. mp3파일을 pcm데이터로 추출하여 재생시키는 코드이다.


drcrter님 블로그 : http://drcarter.tistory.com/162


여기에 동영상에서 pcm을 추출하여 재생하려한다. 해당 코드는 아래에 있다.


stackoverflow : http://drcarter.tistory.com/162


위 두 코드를 응용하여 만들었다.


동영상의 음악을 추출하여 처리를 하려고 하는데 인코딩 되어있는 음원은 처리가 어렵다.


따라서 음악을 pcm형식으로 추출해야한다.


이 코드는 분석하기 보다는 코드를 변경해가면서 이해하는편이 더 효율적이다.


위 두 링크에서 가져온 코드를 비교하고 합쳐보면서 코드를 이해하였다.


더 나아가 getInputBuffer와 getOutputBuffer가 API21에서 더 이상 사용되지 않는다.


따라서 다른 코드로 변경하였다.


변경된 pcm데이터는 1샘플이 16비트인데 스테레오 이기 때문에 8비트가 실제 하나의 샘플이다.


직접 해보면서 프로그래밍을 즐기기 바란다.


GitHub : https://github.com/pchan1401-ICIL/PCMfromVideo

스마트폰을 사용하다보면 스마트폰의 센서를 사용한다.


그중에서 방향 계산을 하는 일이 있을 것이다.


일반적으로 아래 그림처럼 Azimuth, Pitch, Roll을 이용하여 방향을 계산한다.


여기서 Azimuth는 항상 Y+의 방향과 북쪽 방향의 각도 차이를 나타낸다.



지도 앱처럼 북쪽이 Y+인 경우는 큰 문제가 없다. 하지만 카메라가 가르키는 방향을 북으로 잡으려면 어떻게 할까


그에 따라서 방향각을 다시 계산해야하는 경우가 있다.


예제는 책(안드로이드 프로그래밍 / 한빛미디어 / 김상형 지음)의 내용을 응용하여 만들었다.


SensorEventListener mSeonsorListneer = new SensorEventListener() {
float[] mR = new float[9];
float[] mI = new float[9];
float[] mV = new float[9];
@SuppressLint("SetTextI18n")
@Override
public void onSensorChanged(SensorEvent event) {
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
mGravity = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mGeometric = event.values.clone();
break;
}

if(mOrientCount++ % FREQ != 0) return;

if(mGravity != null && mGeometric != null) {
SensorManager.getRotationMatrix(mR, mI, mGravity, mGeometric);
float inclination = SensorManager.getInclination(mI);
SensorManager.getOrientation(mR, mV);

mTxtOrient.setText(
("회수 = " + mOrientCount / FREQ + "회\n")
+ "\nGra : " + dumpValues(mGravity)
+ "\nMag : " + dumpValues(mGeometric)
+ "\n\nR : \n" + dumpMatrix(mR)
+ "\nI : \n" + dumpMatrix(mI)
+ "\ninclination : " + inclination
+ "\n\nRot : \n" + dumpMatrix(mV)
+ "\n\nTop : "
+ "\nx : " + String.format("%.3f", Math.cos(mV[0])*Math.cos(mV[1]))
+ "\ny : " + String.format("%.3f", Math.sin(mV[0])*Math.cos(mV[1]))
+ "\nz : " + String.format("%.3f", -Math.cos(mV[1]-Math.PI/2))

+ "\n\nLeft : "
+ "\nx : " + String.format("%.3f", -Math.cos(mV[0])*Math.sin(mV[1])*Math.sin(mV[2]) + Math.sin(mV[0])*Math.cos(mV[2]))
+ "\ny : " + String.format("%.3f", -Math.sin(mV[0])*Math.sin(mV[1])*Math.sin(mV[2]) - Math.cos(mV[0])*Math.cos(mV[2]))
+ "\nz : " + String.format("%.3f", Math.cos(mV[1])*Math.sin(mV[2]))

+ "\n\nBack : "
+ "\nx : " + String.format("%.3f", -Math.cos(mV[0])*Math.sin(mV[1])*Math.cos(mV[2]) + Math.sin(mV[0])*Math.sin(mV[2]))
+ "\ny : " + String.format("%.3f", -Math.sin(mV[0])*Math.sin(mV[1])*Math.cos(mV[2]) - Math.cos(mV[0])*Math.sin(mV[2]))
+ "\nz : " + String.format("%.3f", Math.cos(mV[1])*Math.sin(mV[2]-Math.PI/2))
);
}
}

다른 부분은 레이아웃 설정 관련 설정이므로 생략한다. 센서리스너만 보도록 한다.


센서의 사용법은 다른 블로그에도 많으니 생략한다.


mV는 센서의 azimuth, pitch, roll값을 저장한다. 이 값을 이용하여 아래는 벡터를 이용하였다.


수식은 아래 링크를 참고하면 된다.


http://stackoverflow.com/questions/1568568/how-to-convert-euler-angles-to-directional-vector


Top은 Y+방향의 벡터


Left는 X-방향의 벡터


Back은 Z-방향의 벡터이다.


실질적으로 만들고 싶었던 기능은 카메라가 어느 방향을 가르키는지 알려주는 코드를 만들고 싶었다.


그럼 어떻게 만들었는지 보도록 하자.


수학적으로 회전행렬 * 벡터를 사용하였다.


회전행렬은 다음과 같다.


| cos(yaw)cos(pitch) -cos(yaw)sin(pitch)sin(roll)-sin(yaw)cos(roll) -cos(yaw)sin(pitch)cos(roll)+sin(yaw)sin(roll)|
| sin(yaw)cos(pitch) -sin(yaw)sin(pitch)sin(roll)+cos(yaw)cos(roll) -sin(yaw)sin(pitch)cos(roll)-cos(yaw)sin(roll)|
| sin(pitch)          cos(pitch)sin(roll)                            cos(pitch)sin(roll)|


여기에 [1, 0, 0]을 곱하면 Top(Y+)방향의 벡터


여기에 [0, 1, 0]을 곱하면 Left(X-)방향의 벡터


여기에 [0, 0, 1]을 곱하면 Back(Z-)방향의 벡터

 


+ "\n\nTop : "
+ "\nx : " + String.format("%.3f", Math.cos(mV[0])*Math.cos(mV[1]))
+ "\ny : " + String.format("%.3f", Math.sin(mV[0])*Math.cos(mV[1]))
+ "\nz : " + String.format("%.3f", -Math.cos(mV[1]-Math.PI/2))



우선, Top(Y+)방향을 보자


여기서 주의할 점은 pitch의 범위이다. 센서매너저에서 출력된는 범위는 0 ~ 2*PI이다. 하지만 실질적으로 필요한 z의 값은 -1 ~ 1이다.


따라서 z의 값을 -PI ~ PI로 변경하기 위해서 코사인 함수내에 PI/2를 빼주었다. 


                    + "\n\nBack : "
+ "\nx : " + String.format("%.3f", -Math.cos(mV[0])*Math.sin(mV[1])*Math.cos(mV[2]) + Math.sin(mV[0])*Math.sin(mV[2]))
+ "\ny : " + String.format("%.3f", -Math.sin(mV[0])*Math.sin(mV[1])*Math.cos(mV[2]) - Math.cos(mV[0])*Math.sin(mV[2]))
+ "\nz : " + String.format("%.3f", Math.cos(mV[1])*Math.sin(mV[2]-Math.PI/2))


이와 마찬가지로 Back(Z-)의 벡터는 roll값에 PI/2의 값을 빼주었다. 


이렇게 하지 않는다면, z값이 1이 나와야할 경우에 0이 나오거나 항상 양수값만 나오는 경우가 발생한다.


코드를 대충 짰기 때문에 x, y, z가 순방향이 아닐것이다. 사용할거라면 직접 고치기 바란다.


GitHub : https://github.com/pchan1401-ICIL/GetOrientation.git



+ Recent posts