안녕하세요.
CameraX를 사용하면서 이미지 회전과 잘림 이슈에 대해서 어떻게 해결했는 지 설명하려고 합니다.
https://developer.android.com/media/camera/camerax/configuration#crop
공식문서에서도 Camera 화면이 보이는 영역인 Preview 영역을 줄였을 때, 아래와 같이 자르는 가이드 라인을 제공합니다.
val viewPort =
ViewPort.Builder(Rational(width, height), imageCapture.targetRotation).build()
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview)
.addUseCase(imageCapture)
.addUseCase(imageAnalysis)
.setViewPort(viewPort)
.build()
cameraProvider.bindToLifecycle(
lifecycleOwner = lifecycleOwner,
cameraSelector = cameraSelector,
useCaseGroup,
)
이미지를 크롭하기 위해서 width와 height를 카메라 영역의 크기를 가져와야 합니다. Preview의 가로와 세로길이를 가져오면 되는 간단한 문제입니다.
하지만 width, height의 값이 0으로 전달 받는 것을 겪을 수 있을 겁니다. 해당 논의는 스택오버플로우에서도 논의되어지고 있었습니다. 기본적으로 가로와 세로길이를 할당해줄 때는 onCreate에서 해줍니다.
이 문제는 xml 사용 시, wrap_content & match_parent을 사용하면서 뷰가 렌더링되어지는 과정이 일반적으로 onResume 까지 완료되지 않는다고 합니다. 그렇기 때문에 당연히 0의 값을 오는 겁니다.
https://stackoverflow.com/questions/3591784/views-getwidth-and-getheight-returns-0
저는 아래 코드처럼 문제를 해결했습니다. (자세한 내용은 링크에서 확인부탁드려요!)
binding.cameraView.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.cameraView.viewTreeObserver.removeOnGlobalLayoutListener(this)
cameraX.startCamera(
this@CameraXActivity,
binding.cameraView.measuredWidth.dpToPx(this@CameraXActivity).toInt(),
binding.cameraView.measuredHeight.dpToPx(this@CameraXActivity).toInt()
)
}
})
private fun Int.dpToPx(context: Context) = this / context.resources.displayMetrics.density
CameraX를 사용해서 사진을 촬영하는 방법은 두 가지 입니다.
OnImageSavedCallback 콜백을 사용하면, 회전도 잘 되고 잘림 이슈도 없이 촬영된 화면과 똑같은 사진이 저장되어지고 가져와 사용할 수 있습니다.
하지만, 갤러리에 저장되는 것을 기대하지 않습니다. 어쩔 수 없이 OnImageCapturedCallback 을 사용해야 합니다.
이 과정에서 겪은 내용을 차례차례 OnImageCapturedCallback 챕터에서 설명하도록 하겠습니다.
https://developer.android.com/reference/androidx/camera/core/ImageCapture.OnImageCapturedCallback
사진 촬영 후 이미지의 비트맵을 가져와 보여줄 수 있습니다.
하지만 여기서 이슈는 회전과 잘림 이슈입니다.
위 두 가지 주제를 좀 더 자세하게 알아보겠습니다.
우선 매개변수로 image에서 회전과 잘림에 대해서 사용할 객체를 가져올 수 있습니다.
참고 하고자 사진도 첨부해놓겠습니다! 아래에서 부터 본격적인 이슈 해결입니다.
사진을 촬영하게 되면, 단순히 비트맵으로 보여줄 때, 아래처럼 반시계방향으로 90도 회전하게 되어있습니다.
이 부분을 해결하기 위해서는 간단하게 해결할 수 있습니다.
아래 rotate 함수에 주목해주고, image.imageInfo.rotationDegrees 값에 주목해주세요.
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
var rotatedBitmap = image.toBitmap().rotate(image.imageInfo.rotationDegrees.toFloat())
}
private fun Bitmap.rotate(degrees: Float): Bitmap =
Bitmap.createBitmap(this, 0, 0, width, height, Matrix().apply { postRotate(degrees) }, true)
추가로 알아두면 좋은 점은 전면 카메라인 CameraSelector.LENS_FACING_FRONT 을 사용할 경우(내 얼굴 보이는 시점)에 아래 reverse함수를 사용하면 됩니다.
private fun Bitmap.reverse(): Bitmap =
Bitmap.createBitmap(this, 0, 0, width, height, Matrix().apply { setScale(-1f, 1f) }, false)
위 처럼 잘 진행되었다면, 아래 사진처럼 원하는 방향으로 이미지의 비트맵을 받을 수 있습니다.
하지만, 뭔가 이상하죠?
위에 여백이 많이 늘었어요. 그 이유는 OnImageCapturedCallback 은 OnImageSavedCallback 처럼 이미지를 잘라서 주지 않습니다. 밑에서 더 자세히 설명합니다.
우선 이해를 돕고자 차근차근 위에도 내용도 복기하며 설명해보도록 하겠습니다.
아래 사진처럼 일반적으로 사진을 촬영했는데, 반시계방향으로 90도 회전하면서 이미지의 위 영역도 넓어집니다.
여기서의 이미지 크기 데이터는 image.image?.cropRect를 보게되면 아래 디버깅에서 Rect 객체의 값을 볼 수 있었습니다.
그 다음에는 시계방향으로 90도 회전하게 됩니다. 위에서 설명했듯이 위에 여백이 늘었습니다. 그리고 회전의 값은 아래 디버깅 결과처럼 image.imageInfo.rotationDegrees 값이 90으로 설정되어있는 것을 확인할 수 있습니다. 시계 방향으로 90도 회전하라는 의미이죠.
그 다음으로 이미지를 자르기 이전에 기본 이미지에 대한 사이즈 말고, 잘리는 영역에 대한 정보도 제공해줍니다.
image.cropRect를 참조해보면 아래의 디버깅 결과를 확인할 수 있고, 그림처럼 처음 시점에서의 crop하는 설정이기 때문에 아래의 그림을 보면 더 이해하기 쉽습니다.
이제는 실제로 이미지를 자를 시간이 왔습니다. 우선 Rect에 대한 이해를 돕고자 Canvas 동작원리를 가져왔고, createBitmap에서 x,y 좌표 그리고 width, height으로 그려지는 과정을 쉽게 이해하실 수 있을 겁니다.
아래처럼 (x,y) 좌표가 (0, 882)이기 때문에 위에 영역이 잘리고, 전체 높이에서 y 값을 빼면 높이가 설정되며 가로 길이는 전체 가로길이가 됩니다.
결과적으로 우리가 사진 촬영 후 똑같은 사진의 결과물을 얻을 수 있습니다.
참 쉽죠?
마지막으로 결과물에 대한 코드도 첨부해놓겠습니다.
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
val imageCropRect = image.image?.cropRect
val imageProxyCropRect = image.cropRect
val cropBitmap: Bitmap?
val cropX: Int
val cropY: Int
val cropWidth: Int
val cropHeight: Int
when (image.imageInfo.rotationDegrees) {
90 -> { // 정방향 (현재 회전된 상태),
cropX = 0
cropY = imageProxyCropRect.left
cropWidth = rotatedBitmap.width
cropHeight = imageProxyCropRect.right - imageProxyCropRect.left
}
... // 다른 각도도 설정
}
}
위에서 정상적으로 사진 촬영 방법에 대한 예시였습니다.
근데 생각해보면 저렇게 사진찍는 사람만 있다면 위에 방법으로 모든 문제가 해결됩니다. 하지만 아래처럼 사진 촬영의 방법은 4가지의 각도를 가지고 구분됩니다.
image.imageInfo.rotationDegrees 값은 차례대로 90, 180, 270, 0 으로 구분됩니다.
이 부분도 참고해서 회전과 이미지를 자르는 과정에서 분기 처리하여 진행해주시면 됩니다.
안드로이드 관련 문서를 검색하다보면, 자주 보이시는 분께서 너무 멋진 글을 작성해주셔서, 따로 설명하지 않아요! 해당 아티클에서 정말 잘 소개해주고 있기 때문에 링크 남겨놓을게요! https://velog.io/@mraz3068/Android-%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%B4%AC%EC%98%81%EC%8B%9C-%EC%85%94%ED%84%B0-%EC%86%8C%EB%A6%AC%EC%B0%B0%EC%B9%B5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0
위 아티클에 짧게 요약하자면,
결과적으로 CameraX를 사용하면서 발생했던 이슈인 회전, 잘림 이슈를 해결해보았습니다.
아래의 사진은 결과물이고, 레이아웃은 어떻게 구성했는 지 스펙도 같이 첨부했어요!
마지막으로 예시 코드를 참고하시려면 아래 깃허브 링크에 들어와서 확인해주세요!
감사합니다!!
https://github.com/Jokwanhee/camerax-sample?tab=readme-ov-file