본문 바로가기

Android/Kotlin

Camera2 Api 를 사용해보자!

반응형

해당 글은 Camera2Api 를 사용한 카메라 분석으로서, 카메라의 "내부"까지 살펴보는 것이 아닌, 카메라가 어떻게 동작하고, 어떤 부분에 대해 어떤 기능을 하는 지에 대한 작성 글입니다. 부족한 부분이 있을 거, "참고" 해주세요~!

 

분석한 코드 : https://github.com/googlesamples/android-Camera2Basic→ kotlinApp 파일

  • 화면 별 설명

    1. CameraActivity
      • 화면을 풀 스크린으로 변경하는 등의 역할을 함
    2. Camera2BasicFragment
      • AutoFitTextureView 를 통해 화면을 보여주고, 사진을 찍거나 등의 역할을 함
    3. CompareSizesByArea
      • 영역에 대한 사이즈 비교하는 역할
    4. ConfirmationDialog
      • 카메라 권한이 없을 경우 나타나는 팝업
    5. ErrorDialog
      • 권한 요청에 실패하거나, 카메라에 관련된 member variables를 설정 시 실패할 경우 뜨는 팝업
    6. ImageSaver
      • 이미지를 저장하기 위한 class
    7. AutoFitTextureView
      • TextureView에 대한 비율을 설정하는 class

    이렇게 class가 구성되며, 가장 중요한 class는 역시나 Camera2BasicFragment가 되겠다.

Camera2BasicFragment

  • LifeCycle
    1. onCreateView : layout을 inflate한 결과를 View로 리턴한다.
    2. onViewCreated : View를 선언한다.
    3. onActivityCreated : File 정의
    4. onResume
      • startBackgroundThread()를 통해 쓰레드를 .start()하고, handler를 적용시킨다.

      • 카메라와 관련된 작업은 UI를 그리는 메인쓰레드를 방해해지 않기 위해 onResume에서 새로운 쓰레드와 핸들러를 생성

      • 처음 액티비티가 시작되면 else조건을 타지만, 다른 액티비티의 호출로 카메라 리소스와 텍스쳐뷰들이 잠시 비활성화 되었다가 다시 재게 되는 경우에는 if문안의 openCamera()를 바로 호출

      • TextureView의 초기화가 완료되고 화면에 텍스쳐를 그릴 준비가 되었을때, onSurfaceTextureAvailable()을 호출해 카메라를 엽니다(openCamera() 호출).

      • openCamera()

        • 카메라 권한 확인(requestCameraPermission())
        • setUpCameraOutputs(width, height)를 통해 카메라 설정
        • configureTransform(width, height)을 통해 화면 회전 설정

        (1) setUpCameraOutputs(width, height)

        • CameraManager로부터 카메라 리스트들을 가져오고, 그 리스트들에 대한 특성 중 원하는 특성을 사용(후면 카메라를 사용을 하겠다.)
        • 이미지 리더의 해상도 및 포맷 설정(width, height, *format, *maxImages)
        • 이미지 방향(areDimensionsSwapped())
        • 프리뷰 사이즈 선택(chooseOptimalSize())
        • 선택한 프리뷰 사이즈에 대해 textureView의 비율 적용
        • 플래시 지원 여부

        (2) configureTransform(width, height)

        • 카메라 프리뷰 사이즈가 (2)에서 결정된 후 호출이 되며, 'textureView'의 사이즈 또한 고정시킨다.
        • 배치 변경, 즉, 화면과 카메라 영상의 방향을 맞추기 위해 View를 matrix 연산으로 회전 시킴
        • 2.5 기다린 후 manager를 통해 카메라를 오픈(manager.openCamera(id, stateCallback, handler))
        • 카메라가 열리고 나면 stateCallback - onOpened(cameraDevice)가 호출 되며, createCameraPreviewSession()을 통해 프리뷰 세션을 만든다.

        (3) createCameraPreviewSession()

        • 카메라 프리뷰에 대한 new Camera Capture Session을 만들기 위한 함수이다.
        • texture = textureView.surfaceTexture를 가져와 PreviewSession을 만들기 위한 준비를 하고 이를 통해 cameraDevice.createCaptureSession()을 통해 세션을 만든다.
        • 반복적으로 이미지 버퍼를 얻기 위해 setRepeatingRequest()를 호출을 하며, 이렇게 TextureView에 카메라 영상이 나오게 됩니다.
        • 프리뷰 세션을 만들 때, mCaptureCallback은 JPEG 캡쳐와 관련된 이벤트를 다룰 때 사용합니다.
    5. onPause()
      • closeCamera() 메소드를 통해 captureSession과 cameraDevice, imageReader를 초기화 시켜준다.
      • stopBackgroundThread()를 통해 쓰레드와 핸들러를 초기화시킨다.
    6. onDestroy()
  • 기타 method
    1. takePicture()
      • 사진을 캡쳐하기 위해 2를 호출한다.
    2. lockFocus()
      • 사진을 캡쳐하기 위한 첫번째 단계로서 포커스를 잡는다.
      • 카메라에게 초점을 잡으라고 request를 보내기 위해 해당 파라미터 설정을 해줍니다. 준비된 captureSession()의 capture()메소드와 함께 request인자를 넣어 호출합니다. 초점이 잡혔다면 mCaptureCallback으로부터 captureStillPicture()을 호출하게 될 것입니다.
    3. captureStillPicture()
      • 사진을 찍기 위한 CaptureRequest.Builder에서 surface는 textureView가 아닌 ImageReader에서의 surface이며, 사진을 찍기 위한 설정을 set 합니다.
      • CameraCaptureSession.CapturCallback()을 통해 사진을 저장하며, unlockFocus()를 호출해 포커스를 풀어줍니다.
  • 추가 사항
    • preview에서 나타나는 이미지를 읽어 bitmap으로 변경하는 작업이 필요.
      1. 이미지를 가져다 Mat(GRAY scale과 RGB, RGBA)으로 변화시킨 뒤, Mat의 cols()와 rows(), ARG_8888을 통해 bitmap을 create해주고, matToBitmap을 통해 bitmap을 재설정해준다.(이렇게 하는 이유는 rgb로만 만든 bitmap에 rgba를 넣어 재 출력해주기 위함이다.)
      2. 이렇게 만든 bitmap을 원본대로 보기 위해 rotate해주면, 원하는 bitmap을 만들 수 있다.
    • 위 작업을 하기 위해 Camera2BasicFragment에 추가해야할 부분
      1. image를 읽어오기 위해 imageReader를 하나 더 정의한다.(프리뷰 사이즈와 format은 420_888)
      2. ImageReader.OnImageAvailableListener를 정의한다
      3. previewRequestBuilder를 만들 때(createCameraPreviewSession()) target을 같이 추가한다.
      4. 추가한 imageReader가 2에서 정의한 listener를 가질 수 있도록 set해준다.
      5. createCaptureSession에도 새로 추가한 imageReader의 surface를 추가한다.
    • 위와 같은 절차를 진행하면 image를 따로 가져와 bitmap으로 변경할 수 있다.
    • 참고 소스
    fun imageToBitmap(image: Image): Bitmap {
        val mYuvMat = imageToMat(image, CvType.CV_8UC1)
        val bgrMat = Mat(image.width, image.height, CvType.CV_8UC4)

        Imgproc.cvtColor(mYuvMat, bgrMat, Imgproc.COLOR_YUV2BGR_I420)

        val rgbaMatOut = Mat()
        Imgproc.cvtColor(bgrMat, rgbaMatOut, Imgproc.COLOR_BGR2RGBA, 0)

        var bitmap = Bitmap.createBitmap(bgrMat.cols(), bgrMat.rows(), Bitmap.Config.ARGB_8888)
        Utils.matToBitmap(rgbaMatOut, bitmap)

        val matrix = Matrix()
        matrix.postRotate(90f)
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)

        return bitmap
    }

    private fun imageToMat(image: Image, type: Int): Mat {
        var buffer: ByteBuffer
        var rowStride: Int
        var pixelStride: Int
        val width = image.width
        val height = image.height
        var offset = 0

        val planes = image.planes
        val data = ByteArray(image.width * image.height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8)
        val rowData = ByteArray(planes[0].rowStride)

        for (i in planes.indices) {
            buffer = planes[i].buffer
            buffer.rewind()
            rowStride = planes[i].rowStride
            pixelStride = planes[i].pixelStride
            val w = if (i == 0) width else width / 2
            val h = if (i == 0) height else height / 2
            for (row in 0 until h) {
                val bytesPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8
                if (pixelStride == bytesPerPixel) {
                    val length = w * bytesPerPixel
                    buffer.get(data, offset, length)

                    if (h - row != 1) {
                        buffer.position(buffer.position() + rowStride - length)
                    }
                    offset += length
                } else {


                    if (h - row == 1) {
                        buffer.get(rowData, 0, width - pixelStride + 1)
                    } else {
                        buffer.get(rowData, 0, rowStride)
                    }

                    for (col in 0 until w) {
                        data[offset++] = rowData[col * pixelStride]
                    }
                }
            }
        }

        val mat = Mat(height + height / 2, width, type)
        mat.put(0, 0, data)

        return mat
    }

 

 

  • 겪은 오류 사항
    • image를 bitmap으로 변경하기 위해 다양한 방법을 써 봤지만 계속 실패했었다. 디버깅도 해봤지만... 이유는 못 찾고 2~3일을 보내던 중에 설마하고 bitmap을 imageView에 바로 띄워 보았는데.... bitmap으로 변경은 되었지만 90도 회전이 된 상태여서 원하는 값이 출력이 되지 않았던 것... 쓸데없는 곳에서 시간을 버렸고 찾아 잘 나왔다...
    • image를 bitmap으로 변경 후 작업을 하는데, 어떤 폰에서는 느려지는 것을 찾을 수 있었다. 이유를 찾기 위해 분석하던 중, 폰 마다 해상도가 달라 가지고 있는 해상도의 list들이 원하는 값이 맞지 않으면 "가장 큰" 해상도를 지원하게 되어 있었다. 가장 큰 해상도를 처리하다보니 느려지는 것을 발견했고, 이 부분을 수정해 잘 찍어냈다.

참고)

  1. maxImages : 사용자가 동시에 접근하려는 최대 이미지 수이며, 메모리 사용을 제한하기 위해 가능한 한 "작아야"합니다. 우선, 이미지들이 사용자에 의해 얻어 진다면, 그것들 중 하나는 acquireNextImage() 또는 acquireLatestImage()를 통해 접근한 새 이미지가 나오기 전에 release 되어야만 합니다. 1이상이어야 합니다. - 디벨로퍼
  2. Queue : 컴퓨터의 기본적인 자료 구조의 한가지로, 먼저 집어 넣은 데이터가 먼저 나오는 FIFO (First In First Out)구조로 저장하는 형식을 말한다. 영어 단어 queue는 표를 사러 일렬로 늘어선 사람들로 이루어진 줄을 말하기도 하며, 먼저 줄을 선 사람이 먼저 나갈 수 있는 상황을 연상하면 된다. - 위키백과
  3. CvType.CV_8UC1 : gray scale image and is mostly used in computer vision algorithms.
  4. CvType.CV_8UC4 : 8-bit per channel RGBA image and can be captured from camera with NativeCameraView or JavaCameraView classes and drawn on surface.
  5. CvType.CV_8UC4 : RGB

참고 :

위키백과

디벨로퍼(https://developer.android.com/reference/android/media/ImageReader)

찰스의 안드로이드(https://www.charlezz.com/?p=1118)

카메라2 구글 쌤플(https://github.com/googlesamples/android-Camera2Basic→ kotlinApp)

 

 

오타가 있거나 빠진 부분이 있으면 댓글 부탁드립니당

 

이상 끝 ~

반응형