Skip to content

⚙ [기술 분석] CameraX API

박봉팔 edited this page Dec 4, 2024 · 2 revisions

CameraX는 Camera2를 모든 기기에 공통적으로 맞춰 사용할 수 있게 추상화한 라이브러리이다.

image



Camera

카메라의 정보를 담은 interface

CameraControl, CameraInfo 등 카메라 관련 정보들이 심플하게 들어 있다. ProcessCameraProvider 로 카메라를 설정하게 된다면 이 Interface 타입을 받을 수 있다.

public interface Camera {

    /**
     * Returns the {@link CameraControl} for the {@link Camera}.
     *
     * <p>The {@link CameraControl} provides various asynchronous operations like zoom, focus and
     * metering. {@link CameraControl} is ready to start operations immediately after use cases
     * are bound to the {@link Camera}. When all {@link UseCase}s are unbound, or when camera is
     * closing or closed because lifecycle onStop happens, the {@link CameraControl} will reject
     * all operations.
     *
     */
    @NonNull
    CameraControl getCameraControl();

    @NonNull
    CameraInfo getCameraInfo();

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @NonNull
    CameraConfig getExtendedConfig();

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    default boolean isUseCasesCombinationSupported(@NonNull UseCase... useCases) {
        return isUseCasesCombinationSupported(true, useCases);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    default boolean isUseCasesCombinationSupportedByFramework(@NonNull UseCase... useCases) {
        return isUseCasesCombinationSupported(false, useCases);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    default boolean isUseCasesCombinationSupported(boolean withStreamSharing,
            @NonNull UseCase... useCases) {
        return true;
    }



CameraController

CameraController는 Capture, Zoom, Focus, ImageAnalyze등 카메라 관련 컨트롤을 하게 할 수 있는

**“추상 클래스”**이다. LifecycleCameraController 로 카메라를 세팅한다면 이 interface를 받아올 수 있다.

앞선 카메라 Interface가 내부에 field로 있다.

image




UseCase

UseCase는 카메라를 세팅할 때, 자주 사용하는 기능들을 손쉽게 세팅할 수 있도록 미리 정의된 타입이다.

UseCase 이름 그대로 활동 명세이며 카메라가 보통 사용하는 활동들을 미리 정의해 놓았다.

아래의 4가지가 있다.(버전이 업데이트 됨에 따라 4가지로 늘었다. 버전이 낮다면 3가지 일 수 있다.)

  • Preview: 카메라가 현재 보고 있는 곳을 화면에 띄울 때 필요하다. Surface를 제공하며, 보통 이 Surface를 이용한 View인 PreviewView에 연결할 때 사용한다. (OpenGL로 조작할 때도 Surface를 이용할 수 있다.)
  • ImageCapture: 이미지를 캡쳐할 때(사진 찍기) 사용된다.
  • VideoCapture: 1.1.0-alpha12에 추가된 UseCase. 최종으로 정해진 버전이 아니라 추가될 수도, 삭제될 수도 있다. 비디오를 캡쳐할 때 사용된다.
  • ImageAnalysis: 이미지를 분석할 때 사용된다. 이미지 처리는 기본적으로 GPU가 담당하게 된다.(우리가 직접 관여할 수 없다.) 하지만 우리가 이미지를 분석할 때는 반드시 CPU에서 사용해야 한다. 이를 Proxy로 다룰수 있게 제공해 주는 것이 ImageAnalysis이다. 이미지 캡쳐, 동영상 캡쳐와는 관련이 없으며, 현재 카메라가 보고있는 곳의 이미지를 실시간으로 매 프레임 마다 받아와 이미지를 분석할 수 있게하는, 일종의 callback이다. Bitmap으로 바로 받아오지는 않고, ImageProxy라는 타입으로 반환해 준다,(내부에 toBitma() 메서드가 있다.)



ProcessCameraProvider

ProcessCameraProvider 는 카메라를 세팅하는 방법중 하나이다.

먼저 Java의 Future를 받아 카메라가 준비되면 Callback을 통해 받아올 수 있다.

아래는 사용 예시이다.

        //카메라가 준비되면 콜백될 Future를 받아온다.
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        //자주 사용하는 Java의 Future이다.
        cameraProviderFuture.addListener({
            //준비되면 받아올 수 있다.(마찬가지로 Future의 get 함수.)
            val cameraProvider = cameraProviderFuture.get()
            
            //다른 앱들이 카메라에 bind되어 있을 수 있다. 이를 해제한다.
            cameraProvider.unbindAll()
            
            //앞선 UseCase중 Preview
            val preview = Preview.Builder()
                .build()
            
            //어떤 카메라를 사용할지 결정한다.
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            
            //앞선 UseCase중 ImageAnalysis
            val imageAnalysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
                .build()
            imageAnalysis.setAnalyzer(cameraExecutor) {
                Log.e("test", "analyze")
                it.close()
            }
            
            //앞선 UseCase중 ImageCapture
            val imageCapture = ImageCapture.Builder().build()

            //카메라와 UseCase를 연결한다. 카메라를 어느 LifeCycle에 맞춰 사용할지 결정하기 위해 Lifecycle을 넣어 준다.
            cameraProvider.bindToLifecycle(this, cameraSelector, imageCapture, imageAnalysis)
        }, ContextCompat.getMainExecutor(this)) // 카메라 전용 스레드를 실행한다.

bindToLifecycle은 Camera Interface를 반환한다.

image

Future는 Java의 Callback이기 때문에 Kotlin의 스타일과는 맞지 않을 수 있다.(코루틴으로 사용할 수 없다.)

그래서 아래의 suspendCoroutine으로 Future의 Callback을 suspend하게 만들 수 있다.

suspend fun ListenableFuture<ProcessCameraProvider>.getCameraProvider(context: Context): ProcessCameraProvider =
    suspendCoroutine { continuation ->
        this.addListener({
            continuation.resume(this.get())
        }, ContextCompat.getMainExecutor(context))
    }



LifecycleCameraController

위의 ProcessCameraProvider 를 간단하게 만든 객체이다.

모든 UseCase가 기본으로 들어가 있으며, LifecycleCameraController(context) 로 만들 수 있다.

image

또한 CameraController를 상속받고 있기 때문에 이 객체를 직접 사용하여 4가지 활동 명세를 사용할 수 있다.

사용하려면 반드시 bindToLifecycle을 호출해 주어야 한다.

그리고 주의 점은 위의 bindToLifecycle과는 다른 메서드이다.(Camera 객체를 반환하지 않는다.)

image

사용 예시는 아래와 같다.

        LifecycleCameraController(context).apply {
            //어떤 카메라를 사용할 지 선택한다.
            cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            //이미지, 비디오 캡쳐를 위한 설정을 한다.(UseCase를 활성화 한다.)
            setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
            //이미지 분석을 위한 설정을 한다.
            setImageAnalysisAnalyzer(cameraExecutor) { imageProxy ->
                imageProxy.close()
            }
            bindToLifecycle(lifecycle)
            
            //기타 세팅을 설정해 준다.
            isTapToFocusEnabled = true
            isPinchToZoomEnabled = true
        }

ProcessCameraProvider 에서 UseCase의 Instance를 생성하여 bind시킨 것과는 다르게 내부에 UseCase들이 미리 들어있어서 세팅만 해주면 간편하게 사용할 수 있다.

또한 Preview UseCase가 디폴트로 들어있다.(단점은 Preview를 사용하지 않으면 모든 UseCase가 동작하지 않는다.)

ProcessCameraProvider vs LifecycleCameraController

LifecycleCameraController는 내부에 UseCase를 가지고 있는 CameraController를 반환한다.

따라서 UseCase 객체를 관리할 필요 없이 CameraController를 사용하여 비디오 캡쳐, 이미지 캡쳐, Preivew, 이미지 분석을 할 수 있다.

하지만 ProcessCameraProvider는 UseCase를 binding할 뿐, 사용하려면 UseCase객체에서 직접 함수를 호출해야 한다.

아래는 ImageCapture UseCase를 사용하려면 어떻게 해야 하는지를 보여준다.

class MainActivity : AppCompatActivity() {
//... 중략

		//따로 imageCapture UseCase를 관리해야 한다.
    private lateinit var imageCapture: ImageCapture
       
    override fun onCreate(savedInstanceState: Bundle?) {
        
        //... 중략
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            cameraProvider.unbindAll()

            val preview = Preview.Builder()
                .build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            val imageAnalysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
                .build()
            imageAnalysis.setAnalyzer(cameraExecutor) {
                Log.e("test", "analyze")
                it.close()
            }

						//future로 준비되었을 시 instance를 초기화 한다.
            imageCapture = ImageCapture.Builder().build()

						//바인딩 한다.
            cameraProvider.bindToLifecycle(this, cameraSelector, imageCapture, imageAnalysis)
        }, ContextCompat.getMainExecutor(this))
    }
    
    private fun takePhoto(){
    
		    //앞선 activity field instance로 imageCapture UseCase를 실행시켜야 한다.
		    
        //자세한 것은 생략.
        imageCapture.takePicture()
    }
    
}


반대로 LifecycleCameraController(context) 는 CameraController에서 직접 takePicture를 호출할 수 있다. (내부에 UseCase가 있기 때문에)

fun CameraController.takePhoto(
    context: Context,
    onPhotoTaken: (Bitmap) -> Unit
) {
    this.takePicture(
        ContextCompat.getMainExecutor(context),
        object : ImageCapture.OnImageCapturedCallback() {
            override fun onCaptureSuccess(image: ImageProxy) {
                super.onCaptureSuccess(image)
            }

            override fun onError(exception: ImageCaptureException) {
                super.onError(exception)
                Log.e("testing", "Couldn't take photo: ", exception)
            }
        }
    )
}



CameraState

카메라를 안전하게 사용하기 위해서는 카메라의 여러 상태에 따라 사용할 지, 말아야 할 지를 결정해야 한다.

그래서 CameraX의 LifeCycleCameraController에는 이러한 상태를 관찰할 수 있는 LiveData를 제공해 준다.

val cameraStateLiveData = lifeCycleCameraController.cameraInfo?.cameraState

(cameraState는 Livedata이다.)

위의 코드 그대로 사용한다면, 아마 null을 반환하는 것을 알 수 있다.

왜냐하면 카메라가 준비되어 있는 시점인지 아닌지 알 수 없기 때문이다.

따라서 언제 준비되는지 알아야 하는데, 이때 LifeCycleCameraController에는 initializationFuture가 존재한다.

이 Future를 통해 어느 시점에 init되는지 callback으로 알 수 있다.

callback은 Kotlin스럽지 않으니, 이를 suspend하게 바꿀 수 있는 함수를 먼저 준비했다.

suspend fun <T>ListenableFuture<T>.getSuspendedResult(context: Context):T= suspendCoroutine{ continuation ->
    this.addListener({
        try {
            continuation.resume(get())
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        }
    },ContextCompat.getMainExecutor(context))
}

이제 위의 확장함수를 사용해서 카메라가 준비되는 것을 기다리면 된다.

suspend 함수이니, Fragment라면 viewModelLifecycleOwner.lifecycleScpoe,

Activity라면 lifecycleScope를 사용하여 launch(혹은 async)를 하면 된다.



이번 프로젝트에서는 Compose를 사용했기 때문에, LaunchedEffect를 사용하였다.

//livedata 저장
var cameraStateLiveData: LiveData<CameraState>? = null

//카메라 상태 저장
var cameraState: CameraState? by remember { mutableStateOf(null) }

//observe할 동안 할 일
val observer = remember {
    Observer<CameraState> { state ->
        cameraState = state
    }
}


LaunchedEffect(Unit) {
    //카메라가 완료될 때 까지 대기
    cameraController.initializationFuture.getSuspendedResult(context)
    //livedata 저장
    cameraStateLiveData = cameraController.cameraInfo?.cameraState
    //observing
    cameraStateLiveData?.observeForever(observer)
}

livedata를 observeForever를 해 주었는데, 이렇게 되면 LifeCycle과 관계 없이 계속 observing 할 것이다.

그래서 DisposableEffect로 removeObserver를 하여 해제해 주었다.

DisposableEffect(Unit) {
    //dispose 될 시 해제
    onDispose {
        cameraStateLiveData?.removeObserver(observer)
    }
}

❓ 왜 DisposableEffect에서 모두 처리 하지 않았나?

DisposableEffect는 의외로 lambda 내부가 suspend가 아니다. 따라서 suspend 함수를 실행할 수 없다.

스크린샷 2024-12-03 오후 8 46 11

LaunchedEffect는 block이 suspend이기 때문에 suspend 함수를 실행할 수 있다.

스크린샷 2024-12-03 오후 8 46 34

Clone this wiki locally