Nuke Olaf - Log Store

[Android] 안드로이드 - camera2 api를 사용해서 사진 촬영하는 앱 만들기 (feat. Kotlin) 본문

Android

[Android] 안드로이드 - camera2 api를 사용해서 사진 촬영하는 앱 만들기 (feat. Kotlin)

NukeOlaf 2019. 12. 14. 13:49

안드로이드 camera2 api 를 사용해서 사진을 촬영하는 방법에 대해 알기 위해서는

camera2 api 를 이용해서 기기의 카메라 장치로 보여지는 것을 view 에 보여주는 과정에 대해 이해해야한다.

개괄적으로 정리해 보자면 이렇다.

1. manifest 에 카메라 관련 권한 등록하기

1
2
3
4
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

 

2. layout 에서 textureView 를 만들어주기

: surfaceView 로도 미리보기 화면을 보여줄 수 있기는 하지만, textureView 에서 지원되는 기능이 더 많다고 한다. 이부분은 아직 잘 모름. 일단 구글 예제에서는 textureView 쓰고 있으니 textureView 를 선언해 준다. 

1
2
3
4
5
6
<TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_weight="1" />

 

3. textureView 에 surfaceTextureListener 달아주기

: textureView 는 엄밀히 말하자면 카메라 미리보기를 랜더링하기 위한 place holder 라고 볼 수 있다. place holder 란, 회원가입 창 같은곳에서 아이디를 입력하는 부분에 아이디라고 적어놓는 것처럼, 여기에 카메라 미리보기 화면을 보여줄것이라고 지정해놓는다고 생각하면 된다.

surfaceTextureListener 는 TextureView 에서 SurfaceTexture 가 사용 가능한 경우, openCamer() 메서드를 호출한다.

openCamer() 메서드는 말 그대로 카메라를 열어주는 메서드라고 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private var textureListener: TextureView.SurfaceTextureListener = object : TextureView.SurfaceTextureListener {
        override fun onSurfaceTextureSizeChanged(
            surface: SurfaceTexture?,
            width: Int,
            height: Int
        ) {
 
        }
 
        override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
 
        }
 
        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
            // 지정된 SurfaceTexture 를 파괴하고자 할 때 호출된다
            // true 를 반환하면 메서드를 호출한 후 SurfaceTexture 에서 랜더링이 발생하지 않는다
            // 대부분의 응용프로그램은 true 를 반환한다
            // false 를 반환하면 SurfaceTexture#release() 를 호출해야 한다
            return false
        }
 
        override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
            // TextureListener 에서 SurfaceTexture 가 사용가능한 경우, openCamera() 메서드를 호출한다
            openCamera()
        }
 
    }

 

4. openCamera() 메서드 만들기

openCamera() 메서드는 TextureView 의 SurfaceTexture 가 사용가능할 때 호출되어야 한다.

openCamera() 메서드는 해당 기기의 카메라 정보를 가져와서 cameraId 와 imageDimension 에 값을 할당하고, 카메라를 열어주는 기능을 수행한다. 그러기 위해서는 우선, CameraManager 객체를 가져와야 한다. CameraManger 에는 많은 정보들이 담겨있는데, 그중에서 여기서 사용할 것은 cameraIdList 와 characteristics 이다.

그리고, 카메라를 사용하기 전에 카메라 사용권한이 있는지 확인해주는 절차가 필요하다.

모든 준비가 완료되면, manager.openCamera(cameraId!!, stateCallback, null) 을 통해 실제로 카메라를 열어주게 된다. 이때 인자로 넘겨주는 stateCallback 은 카메라가 제대로 연결되었는지 확인하고, 카메라가 제대로 연결되었다면, 카메라 미리보기를 생성해준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// openCamera() 메서드는 TextureListener 에서 SurfaceTexture 가 사용 가능하다고 판단했을 시 실행된다
    private fun openCamera() {
        Log.e(TAG, "openCamera() : openCamera()메서드가 호출되었음")
 
        // 카메라의 정보를 가져와서 cameraId 와 imageDimension 에 값을 할당하고, 카메라를 열어야 하기 때문에
        // CameraManager 객체를 가져온다
        val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
 
        try {
            // CameraManager 에서 cameraIdList 의 값을 가져온다
            // FaceCamera 값이 true 이면 전면, 아니면 후면 카메라
            cameraId = if (faceCamera) {
                manager.cameraIdList[1]
            }else {
                manager.cameraIdList[0]
            }
 
            val characteristics = manager.getCameraCharacteristics(cameraId!!)
 
            // SurfaceTexture 에 사용할 Size 값을 map 에서 가져와 imageDimension 에 할당해준다
            imageDimension = map!!.getOutputSizes<SurfaceTexture>(SurfaceTexture::class.java)[0]
 
            // 카메라를 열기전에 카메라 권한, 쓰기 권한이 있는지 확인한다
            if(ActivityCompat.checkSelfPermission(thisManifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED // 카메라 권한없음
                && ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 쓰기권한 없음
                // 카메라 권한이 없는 경우 권한을 요청한다
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CAMERA_PERMISSION)
                return
            }
 
            // CameraManager.openCamera() 메서드를 이용해 인자로 넘겨준 cameraId 의 카메라를 실행한다
            // 이때, stateCallback 은 카메라를 실행할때 호출되는 콜백메서드이며, cameraDevice 에 값을 할달해주고, 카메라 미리보기를 생성한다
            manager.openCamera(cameraId!!, stateCallback, null)
        } catch (e: CameraAccessException) {
            e.printStackTrace()
        }
   }

 

5. CameraDevice.StateCallback() 메서드 overriding 해주기

카메라가 제대로 연결되었으면, createCameraPreviewSession() 메서드로 카메라 미리보기를 시작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// openCamera() 메서드에서 CameraManager.openCamera() 를 실행할때 인자로 넘겨주어야하는 콜백메서드
    // 카메라가 제대로 열렸으면, cameraDevice 에 값을 할당해주고, 카메라 미리보기를 생성한다
    private val stateCallback = object : CameraDevice.StateCallback() {
        override fun onOpened(camera: CameraDevice) {
            Log.d(TAG, "stateCallback : onOpened")
 
            // MainActivity 의 cameraDevice 에 값을 할당해주고, 카메라 미리보기를 시작한다
            // 나중에 cameraDevice 리소스를 해지할때 해당 cameraDevice 객체의 참조가 필요하므로,
            // 인자로 들어온 camera 값을 전역변수 cameraDevice 에 넣어 준다
            cameraDevice = camera
 
            // createCameraPreview() 메서드로 카메라 미리보기를 생성해준다
            createCameraPreviewSession()
        }
 
        override fun onDisconnected(camera: CameraDevice) {
            Log.d(TAG, "stateCallback : onDisconnected")
 
            // 연결이 해제되면 cameraDevice 를 닫아준다
            cameraDevice!!.close()
        }
 
        override fun onError(camera: CameraDevice, error: Int) {
            Log.d(TAG, "stateCallback : onError")
 
            // 에러가 뜨면, cameraDevice 를 닫고, 전역변수 cameraDevice 에 null 값을 할당해 준다
            cameraDevice!!.close()
            cameraDevice = null
        }
 
   }

 

6. createCameraPreviewSession() 메서드 작성하기

여기서 말하는 카메라 미리보기 (preview) 란, 카메라 장치로 보여지고 있는것을 사용자가 미리 볼 수 있게 해준다는 의미이다. 찍은 사진을 미리보기하는 개념이랑 헷갈릴 수 있는데, 실시간으로 카메라가 보고 있는 장면을 스트리밍해준다고 생각하면 편하다.

미리보기를 보여줄 출력표면인 surface 에 textureView.surfaceTexture 를 넣어준다. 이때, Surface 의 기본 크기는 openCamera() 메서드에서 cameraManager 의 characteristics 를 통해 얻어온 디바이스가 지원하는 출력표면의 크기인 imageDimension 에서 가져와 setDefaultBufferSize 로 설정해준다.

그리고 surface 에서 화면을 어떻게 보여줄지 captureRequestBuilder 를 통해 결정해야 한다. 카메라 기기에는 자동으로 색상 및 심도 같은 것들을 보정할 수 있는 기능이 있다. 그래서 카메라 장치에서 미리보기를 가져올 때, 어떤식으로 보여달라고 할 지 request 를 보내게 되는데, 이 request 를 만들어주는 것이 바로 captureRequestBuilder 이다. 여기서는 미리보기 화면을 요청할 것이므로, 파라미터에 CameraDevice.TEMPLATE_PREVIEW 를 넣어준다.

모든 준비가 완료되면, cameraDevice!!.createCaptureSession() 메서드를 시작한다. 이 메서드의 콜백 메서드는, session 이 준비가 완료되면, cameraCaptureSessions.setRepeatingRequest(captureRequest.Builder.build(), null, null) 로 아까 만든 requestBuilder 를 인자로 넘겨주면서 미리보기를 보여주기 시작한다.

아래에서 session 을 전역변수로 만든 이유는, 아래의 takePicture() 메서드에서 설명하겠다

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// openCamera() 에 넘겨주는 stateCallback 에서 카메라가 제대로 연결되었으면
    // createCameraPreviewSession() 메서드를 호출해서 카메라 미리보기를 만들어준다
    private fun createCameraPreviewSession() {
        try {
 
            // 캡쳐세션을 만들기 전에 프리뷰를 위한 Surface 를 준비한다
            // 레이아웃에 선언된 textureView 로부터 surfaceTexture 를 얻을 수 있다
            texture = textureView.surfaceTexture
 
            // 미리보기를 위한 Surface 기본 버퍼의 크기는 카메라 미리보기크기로 구성
            texture.setDefaultBufferSize(imageDimension!!.width, imageDimension!!.height)
 
            // 미리보기를 시작하기 위해 필요한 출력표면인 surface
            surface = Surface(texture)
 
            // 미리보기 화면을 요청하는 RequestBuilder 를 만들어준다.
            // 이 요청은 위에서 만든 surface 를 타겟으로 한다
            captureRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            captureRequestBuilder.addTarget(surface)
 
            // 위에서 만든 surface 에 미리보기를 보여주기 위해 createCaptureSession() 메서드를 시작한다
            // createCaptureSession 의 콜백메서드를 통해 onConfigured 상태가 확인되면
            // CameraCaptureSession 을 통해 미리보기를 보여주기 시작한다
            cameraDevice!!.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() {
                override fun onConfigureFailed(session: CameraCaptureSession) {
                    Log.d(TAG, "Configuration change")
                }
 
                override fun onConfigured(session: CameraCaptureSession) {
                    if(cameraDevice == null) {
                        // 카메라가 이미 닫혀있는경우, 열려있지 않은 경우
                        return
                    }
                    // session 이 준비가 완료되면, 미리보기를 화면에 뿌려주기 시작한다
                    cameraCaptureSessions = session
 
 
                    try {
                        cameraCaptureSessions.setRepeatingRequest(captureRequestBuilder.build(), nullnull)
                    } catch (e: CameraAccessException) {
                        e.printStackTrace()
                    }
                }
 
            }, null)
 
        } catch (e: CameraAccessException) {
            e.printStackTrace()
        }
    }
 

 

7. takePicture() 메서드 작성하기

이제 거의 다 왔다. 미리보기 화면만 본다면, 사진을 어떻게 찍겠는가? 사진을 찍고, 저장하기 위해서 takePicture() 메서드를 만들어준다. takePicture() 메서드에서는 ImageReader 객체를 생성해주어야 한다. ImageReader 란, Surface 에서 랜더링되고 있는 image data 에 직접 접근할 수 있는 객체이다. 쉽게 말하자면, surface 에 랜더링되고 있는 미리보기 화면에서 이미지를 가져올 수 있다는 뜻이다.

ImageReader.newInstance() 메서드를 이용해서 원하는 크기와 형식의 이미지를 받아올 수 있는 ImageReader 객체를 생성할 수 있다. 아래에서는 현재 사용중인 카메라 cameraId 로부터 characteristics 를 가져온 다음, 해당 카메라가 지원가능한 JPEG 이미지 포맷 형식의 이미지 크기를 가져와서 imageReader 에 설정해주고 있다.

그 다음, ImageReader.OnImageAvailableListener 로 ImageReader 객체에 달아줄 리스너를 만들어 준다. 이 리스너에는 surface 에 랜더링중인 이미지에 접근해서 사용가능한 경우, 이미지를 저장해주는 코드를 작성해준다. 

그리고, CameraCaptureSession.CaptureCallback() 로 카메라 캡쳐, 즉 사진촬영이 완료된 경우, 6번에서 작성한 createCameraPreviewSession() 메서드를 호출해준다. openCamera() 에서 session 을 전역변수로 설정했던 이유가 여기에 있는데, createCameraPreviewSession() 메서드를 호출할 때마다 session 을 계속 만들어주면 오류가 생기기 때문이다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 사진찍을 때 호출하는 메서드
    private fun takePicture() {
        try {
            val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
            val characteristics = manager.getCameraCharacteristics(cameraDevice!!.id)
            var jpegSizes: Array<Size>= null
 
            var width = jpegSizes[0].width
            var height = jpegSizes[0].height
 
            imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1)
 
            val outputSurface = ArrayList<Surface>(2)
            outputSurface.add(imageReader!!.surface)
            outputSurface.add(Surface(textureView!!.surfaceTexture))
 
            val captureBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
            captureBuilder.addTarget(imageReader!!.surface)
 
 
            // 사진의 rotation 을 설정해준다
            val rotation = windowManager.defaultDisplay.rotation
 
            var file = File(Environment.getExternalStorageDirectory().toString() + "/pic${fileCount}.jpg")
            val readerListener = object : ImageReader.OnImageAvailableListener {
                override fun onImageAvailable(reader: ImageReader?) {
                    var image : Image? = null
 
                    try {
                        image = imageReader!!.acquireLatestImage()
 
                        val buffer = image!!.planes[0].buffer
                        val bytes = ByteArray(buffer.capacity())
                        buffer.get(bytes)
 
                        var output: OutputStream? = null
                        try {
                            output = FileOutputStream(file)
                            output.write(bytes)
                        } finally {
                            output?.close()
 
                            var uri = Uri.fromFile(file)
                            Log.d(TAG, "uri 제대로 잘 바뀌었는지 확인 ${uri}")
 
                            // 프리뷰 이미지에 set 해줄 비트맵을 만들어준다
                            var bitmap: Bitmap = BitmapFactory.decodeFile(file.path)
 
                            // 비트맵 사진이 90도 돌아가있는 문제를 해결하기 위해 rotate 해준다
                            var rotateMatrix = Matrix()
                            rotateMatrix.postRotate(90F)
                            var rotatedBitmap: Bitmap = Bitmap.createBitmap(bitmap, 0,0bitmap.width, bitmap.height, rotateMatrix, false)
 
                            // 90도 돌아간 비트맵을 이미지뷰에 set 해준다
                            img_previewImage.setImageBitmap(rotatedBitmap)
 
                            // 리사이클러뷰 갤러리로 보내줄 uriList 에 찍은 사진의 uri 를 넣어준다
                            uriList.add(0, uri.toString())
 
                            fileCount++
                        }
 
                    } catch (e: FileNotFoundException) {
                        e.printStackTrace()
                    } catch (e: IOException) {
                        e.printStackTrace()
                    } finally {
                        image?.close()
                    }
                }
 
            }
 
            // imageReader 객체에 위에서 만든 readerListener 를 달아서, 이미지가 사용가능하면 사진을 저장한다
            imageReader!!.setOnImageAvailableListener(readerListener, null)
 
            val captureListener = object : CameraCaptureSession.CaptureCallback() {
                override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
                    super.onCaptureCompleted(session, request, result)
                    /*Toast.makeText(this@MainActivity, "Saved:$file", Toast.LENGTH_SHORT).show()*/
                    Toast.makeText(this@MainActivity, "사진이 촬영되었습니다", Toast.LENGTH_SHORT).show()
                    createCameraPreviewSession()
                }
            }
 
            // outputSurface 에 위에서 만든 captureListener 를 달아, 캡쳐(사진 찍기) 해주고 나서 카메라 미리보기 세션을 재시작한다
            cameraDevice!!.createCaptureSession(outputSurface, object : CameraCaptureSession.StateCallback() {
                override fun onConfigureFailed(session: CameraCaptureSession) {}
 
                override fun onConfigured(session: CameraCaptureSession) {
                    try {
                        session.capture(captureBuilder.build(), captureListener, null)
                    } catch (e: CameraAccessException) {
                        e.printStackTrace()
                    }
                }
 
            }, null)
 
 
        } catch (e: CameraAccessException) {
            e.printStackTrace()
        }
    }
 

 

8. closeCamera()

1
2
3
4
5
6
7
8
9
// 카메라 객체를 시스템에 반환하는 메서드
    // 카메라는 싱글톤 객체이므로 사용이 끝나면 무조건 시스템에 반환해줘야한다
    // 그래야 다른 앱이 카메라를 사용할 수 있다
    private fun closeCamera() {
        if (null != cameraDevice) {
            cameraDevice!!.close()
            cameraDevice = null
        }
    }
 
Comments