이번 GDSC 솔루션 챌린지 / WinterCup에 나가게 되었고, 우리 팀은 카메라를 활용한 앱을 기획하게 되었다. 그 중 나는 Jetpack 라이브러리에 카메라 기능을 활용할 수 있는 CameraX가 생각났고, 해당 라이브러리를 활용하여 개발하기로 하였다.
Project SetUp
build.gradle(project)
buildscript {
ext{
camerax_version = "1.2.1"
}
}
2023.02.27을 기준으로 현재 CameraX의 latest version은 1.2.1이다.
build.gradle(module)
dependencies {
// Camerax
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// ML Kit
implementation 'com.google.mlkit:text-recognition-korean:16.0.0-beta6'
}
ML Kit의 경우 언어별로 다른 버전을 제공한다. 영어의 경우 안정화 버전이 제공되지만, 한국어의 경우에는 beta버전을 제공한다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
카메라의 사용을 위해 카메라 권한과 사진 저장을 위해 외부 저장소 작성을 허용해야 한다.
PermissionFragment.kt
private var PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)
class PermissionsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 버전 9 이하인 경우 외부 저장소 접근 권한 추가
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
val permissionList = PERMISSIONS_REQUIRED.toMutableList()
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
PERMISSIONS_REQUIRED = permissionList.toTypedArray()
}
if (!hasPermissions(requireContext())) {
// 카메라 권한 요청
activityResultLauncher.launch(PERMISSIONS_REQUIRED)
} else {
// 허용된 경우 카메라로 전환
navigateToCamera()
}
}
...
}
사용자에게 설정한 권한을 허용 받기 위해서 요청 화면을 구성한다. 안드로이드 버전에 따라서 요구되는 권한이 다름을 확인해야 한다.
카메라 구현하기
CameraFragment.kt
class CameraFragment : Fragment() {
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK // 후면 비율
private var screenAspectRatio = AspectRatio.RATIO_4_3 // 3 : 4비율
private fun initCamera(){
cameraProviderFuture = ProcessCameraProvider.getInstance(requireActivity())
cameraProviderFuture.addListener(
{
bindCameraUseCases()
},
ContextCompat.getMainExecutor(requireContext())
)
}
lensFacing과 screenAspectRatio를 따로 클래스 내에서 선언한다. 각각 카메라의 후면/전면카메라, 화면 비율을 의미하며, 해당 변수를 사용자의 이벤트에 의해 따로 변경할 수 있음을 의미한다. initCamera 함수에서 CameraProvider를 정상적으로 초기화 하였다면, bindCameraUseCases()를 호출한다.
CameraFragment.kt
class CameraFragment : Fragment() {
private lateinit var imageCapture : ImageCapture
private lateinit var imagePreview : Preview
private lateinit var imageAnalyzer : ImageAnalysis
private fun bindCameraUseCases(){
val cameraProvider = cameraProviderFuture.get()
val cameraSelector : CameraSelector = CameraSelector.Builder() // 카메라 옵션
.requireLensFacing(lensFacing) // 후면 카메라
.build()
imagePreview = Preview.Builder() // preview 객체 생성
.setTargetAspectRatio(screenAspectRatio) // 비율 설정
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 효율 최대치
.setTargetAspectRatio(screenAspectRatio)
.build()
cameraProvider.unbindAll() // for rebinding
imagePreview.setSurfaceProvider(binding.CameraPreview.surfaceProvider) // view 와 객체 결합
binding.CameraPreview.scaleType = PreviewView.ScaleType.FIT_CENTER
cameraProvider.bindToLifecycle(this, cameraSelector, imagePreview,
imageAnalyzer, imageCapture) // 라이프 사이클 바인딩
}
해당 함수에서 직접 cameraProvier에 종속된 ImagePreview와 ImageAnalyer, ImageCapture을 초기화 한다. Preview에서는 비치는 카메라 화면에 대한 설정(화면 비율)을 추가할 수 있다 Analyer는 카메라의 실시간 분석(텍스트 인식, 얼굴 인식)과 같은 기능을 위해 추가할 수 있으며, 해당 기능을 활용하지 않고 있어 버퍼에서 가장 적은 공간을 차지하는 옵션인KEEP_ONLY_LATEST.을 추가하였다. Capture의 경우는 찍히는 사진의 옵션을 설정할 수 있다. Layout의 Preview와 CameraProvier와 결합하고, 원하는 화면 및 사진 비율을 설정하기 위해 ScaleType을 따로 설정해야 한다.(Default는 꽉찬 화면)
CameraFragment.kt
private fun captureImage(){
val outputFileOptions = ImageCapture.OutputFileOptions
.Builder(requireContext().contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues().getCurrentFileName()
).build()
imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(requireContext()),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
navigateToImageProcess(outputFileResults.savedUri.toString())
}
override fun onError(exception: ImageCaptureException) {
Timber.d(exception.message)
}
}
)
}
사진을 캡처하는 함수로, 사진을 외부에 저장하기 위해 위의 OutPutFileOptions을 통해 접근한다. 파일 이름에 대한 정의는 따로 익스텐션을 활용하여 정의하였고, 다른 카메라앱들이 사진 이름을 날짜와 현재 시간을 통해 저장하는 것을 보고 "yyyyMMdd_HHmmss"로 설정하였다. 사진을 찍고, 저장된 uri를 argument로 ImageProcessFragment에 전달한다.
ML Kit Text Recognition
ImageProcessFragment.kt
private fun recognizeText(){
val image = InputImage.fromFilePath(requireContext(), args.imageUrl.toUri())
val textRecognizer = TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())
textRecognizer.process(image)
.addOnSuccessListener { text ->
val recognizedText = text.text.replace("\n", "")
viewModel.detectImage(recognizedText)
}
.addOnFailureListener {
Toast.makeText(requireContext(), "인식할 수 있는 글자가 없습니다.", Toast.LENGTH_SHORT).show()
}
.addOnCanceledListener {
Toast.makeText(requireContext(), "인식에 실패하였습니다. 다시 촬영 해주세요.", Toast.LENGTH_SHORT).show()
}
}
해당은 한글 텍스트 인식을 위한 함수이다. 영문판도 구현하였는데, 객체 이름만 차이가 나고 거의 유사하였다. 이전 Fragment에서 전달 받은 uri를 통해 Image 객체를 생성하고, 텍스트 인식을 위한 TextRecognition 객체를 생성한다.
TextRecognition의 process 메소드에 image객체를 전달하면 인식된 텍스트를 반환한다. 반환된 텍스트는 찍히는 사진의 구도에 따라 block, line, element로 구성되며, 전체 텍스트를 인식을 위해 개행문자를 제외한 나머진 반환값을 활용하였다.
끝으로
카메라와 관련된 프로젝트는 처음 구현해보아 다양한 레퍼런스와 예제를 보면서 공부했었다. 잘 설명된 레퍼런스와 사용하기 빠르고 편리하게 구현되어 있어, 앞으로의 카메라 프로젝트에 자주 도입할 생각이다. 마지막의 ML Kit의 경우는 AI파트 부분의 구현이 미완인 경우로 빠르게 MVP를 구현하기 위해 도입하였는데, 생각보다 한글 텍스트 인식률이 높아서 놀라웠다.
참고 문헌
https://developer.android.com/training/camerax?hl=ko
CameraX 개요 | Android 개발자 | Android Developers
컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. CameraX 개요 Android Jetpack의 구성요소 CameraX는 더 쉬운 카메라 앱 개발을 위해 빌드된 Jetpack 라이브러리입니
developer.android.com
https://github.com/android/camera-samples
GitHub - android/camera-samples: Multiple samples showing the best practices in camera APIs on Android.
Multiple samples showing the best practices in camera APIs on Android. - GitHub - android/camera-samples: Multiple samples showing the best practices in camera APIs on Android.
github.com