본 포스트는 developer.android.com에 작성되어 있는 Getting Started with CameraX 문서를 한글로 번역한 문서입니다.
본래 한 포스트로 작성되어 있으나, 티스토리 블로그의 문제인지 자꾸 포스팅이 날아가는 오류가 발생해 부득이 나눠 발행합니다.
[번역] CameraX 기초 예제 - Part1
[번역] CameraX 기초 예제 - Part2 (이전)
[번역] CameraX 기초 예제 - Part3 (현재)
4. Preview use case 구현
카메라 어플리케이션에서 뷰파인더는 사용자가 자신이 찍을 사진을 미리 볼 수 있도록 하기 위해 사용됩니다.
CameraX의 Preview 클래스를 사용하여 뷰파인더를 구현할 수 있습니다.
Preview를 사용하려면 먼저 구성을 정의한 다음 이 구성을 사용하여 use case의 객체를 생성해야 합니다.
사용 사례의 인스턴스를 만들어야 합니다.
반환된 인스턴스는 CameraX 라이프사이클에 바인딩합니다.
아래 코드를 startCamera()함수에 복사합니다. 이후 코드를 하나하나 분석해보도록 합시다.
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
ProcessCameraProvider의 인스턴스를 생성합니다. 카메라의 라이프 사이클을 라이프 사이클 소유자에 바인딩하는 데 사용됩니다.
이렇게 하면 카메라X가 라이프사이클을 인식하므로 카메라를 열고 닫을 필요가 없습니다.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture에 리스너를 등록합니다.
Runnable 을 하나의 인수로 추가합니다.
ContextCompat.getMainExecutor()를 두 번째 인수로 추가합니다.(기본 스레드에서 실행되는 실행자를 반환합니다.)
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
Runnable 내부에 ProcessCameraProvider 를 추가합니다.
카메라의 라이프 사이클을 어플리케이션 프로세스 내에서 LifecycleOwner에 바인딩 하기위해 사용됩니다.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
Preview 객체를 초기화 하고 빌드한 뒤, 뷰파인더에서 surface provider를 가져와 preview에 설정합니다.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
CameraSelector객체를 생성하고 후면 카메라를 지정합니다
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try블록을 만들어 내부에 cameraProvider가 아무것도 바인딩 되지 않게 하고 cameraSelector 와 preview 객체를 cameraProvider에 바인딩 합니다.
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
}
앱이 더 이상 포커스가 맞지 않는 것처럼 이 코드가 실패할 수 있는 몇 가지 방법이 있습니다. 오류가 발생하면 이 코드를 캐치 블록에 래핑하여 기록합니다.
catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
이제 앱을 실행하면 카메라 프리뷰를 볼 수 있습니다!
5. ImageCapture use case 구현
다른 use case들은 Preview와 매우 유사한 방식으로 동작 합니다.
먼저 실제 use case 객체를 인스턴스화하는 데 사용되는 구성 객체를 정의합니다.
사진을 캡처하려면 사진 촬영 버튼을 눌렀을 때 호출되는 takePhoto()메서드를 구현합니다 .
아래 코드를 takePhoto()함수에 복사합니다. 이후 코드를 하나하나 분석해보도록 합시다.
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time-stamped output file to hold the image
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}
먼저 ImageCapture use case에 대한 참조를 가져옵니다 . use case가 null 인 경우 함수를 종료합니다. 이미지 캡처가 설정되기 전에 사진 버튼을 누르면 null 이 반환됩니다. return구문 없이 null값이라면 앱은 crash 날 것입니다.
val imageCapture = imageCapture ?: return
다음으로 이미지를 저장할 파일을 만듭니다. 파일 이름이 고유하도록 타임스탬프를 추가합니다.
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")
OutputFileOptions 객체를 만듭니다 .
해당 객체는 원하는 출력 방법을 지정할 수 있습니다. 방금 만든 파일에 출력을 저장하려면 photoFile을 추가합니다.
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture객체의 takePicture()함수를 호출합니다.
outputOptions, 실행자, 이미지 저장 시 콜백을 파라미터로 전달합니다
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {}
)
이미지 캡처에 실패하거나 이미지 캡처 저장에 실패한 경우의 로그를 추가합니다.
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
캡처에 실패하지 않으면 사진이 성공적으로 촬영된 것입니다
이전에 생성한 파일에 사진을 저장하고 토스트 메세지를 작성하여 유저가 이를 인지하게 합니다.
로그를 추가합니다.
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
startCamera()메서드로 이동하여 preview코드 아래에 이 코드를 복사합니다.
imageCapture = ImageCapture.Builder()
.build()
마지막으로 새로운 use case를 포함하여 호출하도록 try블록 내의 bindToLifecycle()함수 호출을 수정합니다.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
이 시점에서 startCamera 함수의 전체 코드는 다음과 같습니다.
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
앱을 다시 실행하고 사진 찍기를 누릅니다 . 화면에 토스트가 표시되고 로그에 메시지가 표시됩니다.
사진보기
1. 로그 문을 확인하십시오. 사진 캡처가 성공했음을 알리는 로그가 표시됩니다.
2020-04-24 15:13:26.146 11981-11981/com.example.cameraxapp D/CameraXBasic: Photo capture succeeded: file:///storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
2. file:// prefix구문을 제외하고 사진이 저장된 파일 주소를 복사합니다.
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
3. Android Studio 터미널에서 다음 명령을 실행합니다.
adb shell
cp [2에서 복사한 파일 주소] /sdcard/Download/photo.jpg
4. 이 ADB 명령을 실행한 다음 셸을 종료합니다.
adb pull /sdcard/Download/photo.jpg
5. 현재 폴더의 photo.jpg 파일에 저장된 사진을 확인하실 수 있습니다.
6. ImageAnalysis use case 구현
Android 10 이하를 실행하는 경우 미리 보기, 이미지 캡처 및 이미지 분석을 구현하는 것은 Android Studio의 에뮬레이터에서 작동하지 않을 것입니다. 코드랩의 이 부분을 테스트하기 위해 물리적 장치를 사용하는 것을 추천합니다.
카메라 앱을 보다 흥미롭게 만드는 좋은 방법은 Image Analysis 기능을 사용하는 것입니다. ImageAnalysis를 구현하는 사용자 지정 클래스를 정의할 수 있습니다.분석기 인터페이스로, 들어오는 카메라 프레임과 함께 호출됩니다. 카메라 세션 상태를 관리하거나 이미지를 폐기할 필요가 없습니다. 다른 라이프사이클 인식 구성 요소와 마찬가지로 앱의 원하는 라이프사이클에 바인딩하는 것으로 충분합니다.
1. Analyzer 를 MainActivity.kt의 내부 클래스로 추가합니다. 다음 정의된 Analyzer는 이미지의 평균 광도를 기록합니다. Analyzer를 만들려면 ImageAnalysis.Analyzer 인터페이스를 상속하여 analyze 함수를 오버라이드 해야합니다.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener(luma)
image.close()
}
}
이 클래스는 ImageAnalysis.Analyzer 인터페이스를 구현하는 클래스입니다.
이제 우리가 해야할 일은 다른 use case와 마찬가지로 CameraX.bindToLifecycle()가 호출되기 전에 startCamera() 함수를 수정하여 ImageAnalysis를 상속하는LuminosityAnalyzer클래스를 포함하는 것입니다.
2. startCamera() 메서드의 imageCapture() 코드 아래에 다음과 같은 코드를 추가합니다.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
3. imageAnalyzer를 포함시키기 위해 cameraProvider의 bindToLivecycle() 함수를 다음과 같이 수정합니다.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
전체 메서드는 다음과 같습니다.
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
4. 지금 앱을 실행하세요! 이것은 대략 매 초마다 로그캣에서 이와 유사한 메시지를 생성합니다.
D/CameraXApp: Average luminosity: ...
'Development > Android' 카테고리의 다른 글
Android 배포자동화(fastlane + github actions) - Part.2 (0) | 2022.04.05 |
---|---|
Android 배포자동화(fastlane + github actions) - Part.1 (0) | 2022.03.30 |
[번역] CameraX 기초 예제 - Part2 (0) | 2022.02.11 |
[번역] CameraX 기초 예제 - Part1 (0) | 2022.02.11 |
[잡담] 안드로이드 디버깅 시, 에어팟 음질 저하 현상 (0) | 2022.02.09 |