1. 카메라 열기
카메라를 사용하려면 카메라부터 열어야 한다. 그러면 카메라 매니저를 얻어와야한다
아래처럼 카메라 매니저와, 내가 열고싶은 카메라 id를 가져오자. 보통 0번은 후면 메인카메라고, 1번은 전면카메라, 나머지는 딸려있는 카메라들이다
val cameraManager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraId: String = cameraManager.cameraIdList[0]// 카메라 ID 선택
val cameraThread = HandlerThread("CameraThread")
cameraThread.start()
val cameraHandler = Handler(cameraThread.looper)
val cameraDeviceStatecallback = object : CameraDevice.StateCallback() {
/*
구현
*/
}
cameraManager.openCamera(cameraId, cameraDeviceStateCallback, cameraHandler)
위 openCamera를 통해 카메라를 열면된다. openCamera는 세가지 인풋을 요구한다. 카메라id, 스테이트콜백, 핸들러
여기서 stateCallback은 카메라가 열린 이후 카메라 상태들에 대한 콜백을 정의하는 곳이고 핸들러는 이 콜백들이 어느 쓰레드에서 실행될지를 결정한다.
핸들러를 null 값으로 주면 스테이트 콜백들은 현재 스레드에서 실행된다.
2. CameraDeviceStateCallback 정의하기
먼저 용어부터 정의하자. Camera2 API에서 카메라 하나를 CameraDevice라고 한다. 하나의 CameraDevice는 여러개의 스트림을 생성할 수 있다. 각각 프리뷰, 사진캡처, 비디오촬영이 있다. 이 사실만 알고 넘어가자
위 openCamera에서 이 카메라 디바이스에 대한 콜백을 정의해야한다 했다.
val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
// 카메라 디바이스가 열렸을 때 호출되는 로직
}
override fun onDisconnected(camera: CameraDevice) {
// 카메라 디바이스 연결이 끊겼을 때 호출되는 로직
}
override fun onError(camera: CameraDevice, error: Int) {
// 카메라 디바이스 오류 시 호출되는 로직
}
}
이 콜백에서는 아래 세가지 함수를 정의해아 한다
- onOpened : 카메라 열렸을 때 호출
- onDisconnected : 카메라의 연결이 끊겼을 때 호출
- onError : 카메라에 오류가 났을 때 호출
아무튼 우리는 카메라 열고 쓰는거에 관심이 있으니, 다음으로 onOpend되었을 때 내용으로 넘어가자
3. 세션만들기
카메라가 열렸으니 우리는 이제 카메라에게 사진찍어서 이미지를 가져와! 하면된다. 이때 세션이라는걸 만들어야 한다. 그리고 이 세션을 통해 리퀘스트를 날려야 한다.
세션이라 함은 보통 어떤 두개의 무언가를 연결해주는 다리라고 생각하면 편하다.
세션을 만들기전에 카메라의 이미지를 받는 방법은 4가지라는점을 기억하자
- SurfaceView : 화면에 카메라에서 얻어온 이미지를 표시할 때 사용한다
- ImageReader: 화면에 카메라에서 얻어온 프리뷰를 표시하지 않고, 이미지 데이터만 얻어와 분석하고 싶을 때 사용한다
- OpenGLTexture 또는 TextureView : opengl쓸일이 있거나 커스템한 작업을 한 후 표시하고 싶을 때 사용
- RenderScript.Allocation : 병렬작업을 하고 싶을 때 사용하라고 하는데... 쓰는걸 본적이 없는듯
가장 많이 쓰이는 방법은 SurfaceView 혹은 ImageReader이다. 카메라에서 받아오는 이미지가 화면에 나와야 한다면 SurfaceView, 아니면 데이터만 처리하고 싶다면 ImageReader를 사용한다
프리뷰를 띄우는 예제는 구글에 많으니 나는 ImageReader를 사용하는 예제를 만들어 보겠다.
순서는 리퀘스트 만들기 -> 세션 만들기 순으로 진행한다.
먼저 아래처럼 카메라가 open이 되었다면 캡처 리퀘스트를 만들어주자.
- CameraDevice로 부터 createCaptureReuqest 메소드를 사용하면 리퀘스트 빌더를 리턴해준다. 이때 리퀘스트의 useCase를 넘겨주면 그에 맞게 최적화된 리퀘스트빌더를 만들어준다.
- useCase의 종류는 https://developer.android.com/training/camera2/capture-sessions-requests#kotlin를 참고하자. 아래 예제에서는 Preview UseCase를 사용한 예시이다
- 리퀘스트의 Target을 설정한다. 카메라의 이미지가 렌더링되는 위치이다. 아래 예제는 imageReader를 사용했으므로 화면에 렌더링되지 않는다. 화면에 렌더링 하고 싶다면 SurfaceView를 활용하자
- 이미지 리더의 onImageAvailable 콜백을 작성한다. 이때 이미지를 사용후에 반드시 close해 주어야 한다. 안그러면 이미지 리더가 꽉 차서 다음 프레임이 안올라옴.
- 마지막으로 리퀘스트를 빌드한다
val imageReader: ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 1)
imageReader.setOnImageAvailableListener({
val image = it.acquireNextImage()
/*
대충 이미지 처리 작업
이미지 처리한 후에 반드시 close 해 주어야 한다
안그러면 ImageReader꽉 차서 다음 프레임아 안올라옴
*/
image.close()
}, cameraHandler)
val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
@RequiresApi(Build.VERSION_CODES.P)
override fun onOpened(camera: CameraDevice) {
// 카메라 디바이스가 열렸을 때 호출되는 로직
val captureRequestBuilder =
camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val surface = imageReader.surface
captureRequestBuilder.addTarget(surface)
val captureRequest = captureRequestBuilder.build()
//생략...
다음으로 captureCallback과 SessionConfig를 만들자
- captureCallback : 카메라로부터 이미지를 리퀘스트 하는 과정이 담긴 콜백이다.
- SessionConfig : 세션의 구성과정에 대한 콜백들이 정의 되어 있다.
val imageReader: ImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 1)
imageReader.setOnImageAvailableListener({
val image = it.acquireNextImage()
/*
대충 이미지 처리 작업
이미지 처리한 후에 반드시 close 해 주어야 한다
안그러면 ImageReader꽉 차서 다음 프레임아 안올라옴
*/
image.close()
}, cameraHandler)
val cameraDeviceStateCallback = object : CameraDevice.StateCallback() {
@RequiresApi(Build.VERSION_CODES.P)
override fun onOpened(camera: CameraDevice) {
// 카메라 디바이스가 열렸을 때 호출되는 로직
val captureRequestBuilder =
camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val surface = imageReader.surface
captureRequestBuilder.addTarget(surface)
val captureRequest = captureRequestBuilder.build()
val captureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
// 프리뷰 프레임 처리 로직
// 프레임을 사용하여 원하는 작업을 수행합니다.
// imageReader의 경우 image reader의 콜백에서 수행
}
}
val config: SessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_HIGH_SPEED,
listOf(OutputConfiguration(surface)),
Executor { cb -> cameraHandler.post(cb) },
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
// 세션이 생성되었을 때 필요한 작업 수행
session.setRepeatingRequest(captureRequest,captureCallback,cameraHandler)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
// 세션 생성 실패 시 필요한 작업 수행
}
}
)
camera.createCaptureSession(config)
위 코드대로 작성하면 이제 세션까지 만들어졌다. 사실상 완성이다.
위코드에서 세션이 configured 되면 세션을 받아와서 리퀘스트를 날릴 수 있다. 위코드중 아래가 그 부분이다.
override fun onConfigured(session: CameraCaptureSession) {
// 세션이 생성되었을 때 필요한 작업 수행
session.setRepeatingRequest(captureRequest,captureCallback,cameraHandler)
}
또한 SessionConfiguration에서 Executor라는걸 요구하는데 openCamera할 때 handler와 비슷한 개념으로 요구한다. 즉 세션의 콜백들이 어디서 실행되는지를 요구하는것.
4. 공식문서에서 나오지 않는 추가적으로 알아야 할 것
자 이제 위에 나오는 대로 이미지를 출력해보면?! 짜잔, 화면이 돌아가 있다.
공식문서의 프리뷰에서 나오는 내용을 보면, 대충 카메라 회전을 한번 시켜주어야 한다는 내용이 있다.
아래의 두 api를 통해, 디바이스의 orientation과 센서의 orientation을 구한다.
deviceOrientation = windowManager.defaultDisplay.orientation
sensorOrientation = cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SENSOR_ORIENTATION)!!
그리고 아래 공식대로 이미지를 회전할 각도를 구한다
(sensorOrientation - deviceOrientation * sign + 360) % 360
//sign값은 전면이면 1, 후면이면 -1로 설정
그런데 한가지 더 문제가 있다. 위 코드대로 이미지를 받아와서 돌려보면 좌우가 반전되어있다. 고로 이미지를 한번 좌우반전 시켜주어야 한다. 구글의 샘플 코드를 보니 나와 비슷한 문제를 겪고 좌우반전을 하는것을 발견함.
대충 대부분 전면카메라는 270도 돌아가 있고, 좌우반전을 해주어야 하는듯?
아무튼 아래 예시코드는 imageReader로 타겟을 잡은 뒤, 270도 돌리고 좌우반전하여 TextureView에 그려주는 예시이다.
imageReaderCallback = ImageReader.OnImageAvailableListener {
val image = it.acquireNextImage()
Log.d(TAG,"width : ${image.width} x height: ${image.height} ${image.format}")
val surfaceTexture = debugTextureView.surfaceTexture
val surface = Surface(surfaceTexture)
val canvas = surface.lockCanvas(null)
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.capacity())
buffer[bytes]
val bitmapImage = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, null)
val matrix = Matrix()
matrix.setRotate(((sensorOrientation - deviceOrientation + 360) % 360).toFloat())
matrix.postScale(-1f, 1f)
val rotatedFlippedBitmap = Bitmap.createBitmap(
bitmapImage,
0,
0,
bitmapImage.width,
bitmapImage.height,
matrix,
true
)
canvas.drawBitmap(rotatedFlippedBitmap, 0f, 0f, null)
surface.unlockCanvasAndPost(canvas)
surface.release()
image.close()
}
만약 Target이 SurfaceView인 경우, 구글이 만든 AutoFitSurfaceView라는 것이 있다.
위 서페이스 뷰를 사용하면 알아서 화면에 알맞게 회전도 해주고 맞춰준다.
가령 내가 720*960 형식으로 세로가 길쭉한 4:3 프리뷰를 출력하고 싶다고 하자, 센서의 프리뷰 리스트에는 960*720밖에 없다. 일단 그럼 960*720으로 요청한 뒤, 위 AutoFitSurfaceView의 AspectRatio를 720*960으로 주면, 알아서 화면비율에 맞춰서 출력해준다
'Android' 카테고리의 다른 글
[Android] 백그라운드 서비스에서 카메라 사용시 주의사항 (0) | 2023.12.01 |
---|---|
onUserLeaveHint 홈키 눌럿을때 이벤트 (0) | 2023.07.12 |
[Android] 안드로이드 스튜디오 자동완성 코드찾기 안될 때 해결방법 (0) | 2022.06.20 |
댓글