CameraX 회전 및 자르기(rotate & crop)

조관희·2025년 1월 16일
0
post-thumbnail

안녕하세요.

CameraX를 사용하면서 이미지 회전과 잘림 이슈에 대해서 어떻게 해결했는 지 설명하려고 합니다.

Crop Rect

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,
)

why width is 0?

이미지를 크롭하기 위해서 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

OnImageCapturedCallback is not crop !!

CameraX를 사용해서 사진을 촬영하는 방법은 두 가지 입니다.

  • OnImageSavedCallback : 갤러리에 사진을 저장한 뒤, 저장된 이미지 Uri를 가져옵니다.
  • OnImageCapturedCallback : 이미지 데이터를 가져옵니다. 비트맵, 이미지 메타데이터 등

OnImageSavedCallback 콜백을 사용하면, 회전도 잘 되고 잘림 이슈도 없이 촬영된 화면과 똑같은 사진이 저장되어지고 가져와 사용할 수 있습니다.


하지만, 갤러리에 저장되는 것을 기대하지 않습니다. 어쩔 수 없이 OnImageCapturedCallback 을 사용해야 합니다.

이 과정에서 겪은 내용을 차례차례 OnImageCapturedCallback 챕터에서 설명하도록 하겠습니다.

OnImageCapturedCallback

https://developer.android.com/reference/androidx/camera/core/ImageCapture.OnImageCapturedCallback

사진 촬영 후 이미지의 비트맵을 가져와 보여줄 수 있습니다.

하지만 여기서 이슈는 회전과 잘림 이슈입니다.

  • 기본적으로 OnImageCapturedCallback 은 반시계방향으로 90도 회전되어있습니다.
  • 이미지가 잘리지 않고, 기존 이미지의 Rect 값과 잘린 이미지의 Rect 값을 전달받습니다.

위 두 가지 주제를 좀 더 자세하게 알아보겠습니다.

Rect

우선 매개변수로 image에서 회전과 잘림에 대해서 사용할 객체를 가져올 수 있습니다.

참고 하고자 사진도 첨부해놓겠습니다! 아래에서 부터 본격적인 이슈 해결입니다.

회전(rotate)

사진을 촬영하게 되면, 단순히 비트맵으로 보여줄 때, 아래처럼 반시계방향으로 90도 회전하게 되어있습니다.

이 부분을 해결하기 위해서는 간단하게 해결할 수 있습니다.

아래 rotate 함수에 주목해주고, image.imageInfo.rotationDegrees 값에 주목해주세요.

  • image.imageInfo.rotationDegrees 는 위에서 정상적으로 찍었을 때, 90을 전달 받습니다. 즉, 해당 값만큼 시계방향으로 회전하라는 의미입니다.
  • rotate함수는 비트맵을 전달받은 각도만큼 시계방향으로 회전합니다.
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 처럼 이미지를 잘라서 주지 않습니다. 밑에서 더 자세히 설명합니다.

이미지 자르기 (crop)

우선 이해를 돕고자 차근차근 위에도 내용도 복기하며 설명해보도록 하겠습니다.

아래 사진처럼 일반적으로 사진을 촬영했는데, 반시계방향으로 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 으로 구분됩니다.

이 부분도 참고해서 회전과 이미지를 자르는 과정에서 분기 처리하여 진행해주시면 됩니다.

Camera 촬영 사운드

안드로이드 관련 문서를 검색하다보면, 자주 보이시는 분께서 너무 멋진 글을 작성해주셔서, 따로 설명하지 않아요! 해당 아티클에서 정말 잘 소개해주고 있기 때문에 링크 남겨놓을게요! 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는 기본적으로 촬영 소리에 대한 기능을 제공하지 않는다.
  • 자체적으로 안드로이드에서 소리를 낼 수 있도록 시스템에서 소리를 내어 “찰칵” 소리를 만들 수 있다.
    • 하지만 문제점이 있다. 소리도 크고, 소리를 키우면 엄청크고 작게 줄이면 안들린다.
  • 결론은 MediaPlayer를 이용해서 원하는 찰칵.raw 파일을 만들어 촬영 시 소리를 입히면 됩니다~!

결과

결과적으로 CameraX를 사용하면서 발생했던 이슈인 회전, 잘림 이슈를 해결해보았습니다.

아래의 사진은 결과물이고, 레이아웃은 어떻게 구성했는 지 스펙도 같이 첨부했어요!

마지막으로 예시 코드를 참고하시려면 아래 깃허브 링크에 들어와서 확인해주세요!

감사합니다!!

https://github.com/Jokwanhee/camerax-sample?tab=readme-ov-file

profile
Allright!

0개의 댓글

관련 채용 정보