Nuke Olaf - Log Store
[Android] 안드로이드 - camera2 api를 사용해서 사진 촬영하는 앱 만들기 (feat. Kotlin) 본문
[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(this, Manifest.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 {
} 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
jpegSizes = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(ImageFormat.JPEG)
var width = jpegSizes[0].width
var height = jpegSizes[0].height
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())
var output: OutputStream? = null
try {
output = FileOutputStream(file)
} 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,0, bitmap.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
}
}
|
'Android' 카테고리의 다른 글
[Android] 안드로이드 - camera2 api 로 동영상을 촬영, 재생하며 생긴 코덱 문제 해결 방법 (0) | 2019.12.16 |
---|---|
[Android] 안드로이드 - camera2 api 를 사용하여 동영상 촬영하는 앱 만들기 (feat. Kotlin) (0) | 2019.12.14 |
[Android] 안드로이드 - camera2 api를 이용해서 사진 찍기 (0) | 2019.12.13 |
[Android] 안드로이드 - 큰 비트맵을 효율적으로 로드하기 (feat. BitmapFactory) (0) | 2019.12.09 |
[Android] 안드로이드 - camera2 api 사용해서 카메라앱 만들기 (0) | 2019.12.09 |