Jetpack Compose에서 CameraX + MLKit으로 OCR을 구현해보자

seoyoon·2025년 7월 15일
1

텍스트 인식(OCR)은 이미지에서 문자를 추출해주는 기능으로, 영수증이나 책, 기타 이미지 등 텍스트를 인식하고 추출하는 기능에 주로 사용된다.

이번 프로젝트에서 책 내용을 추출하는 기능을 구현하기에 앞서, CameraX와 MLKit 라이브러리를 활용해 실시간 OCR 기능을 Jetpack Compose 기반으로 구현한 데모 앱을 만들어보았다.

🧠 ML Kit (Text Recognition)

ML Kit은 Google에서 제공하는 머신러닝 라이브러리로 OCR, 바코드 스캔, 얼굴 인식 등 다양한 기능을 제공한다. 이번 글에서는 그 중 Text Recognition 기능을 사용할 예정이다. 자세한 사용 방법은아래 공식 링크를 참고하면 된다.
https://developers.google.com/ml-kit/vision/text-recognition/v2/android?hl=ko

📷 CameraX

CameraX는 Jetpack 라이브러리 중 하나로, Android의 복잡한 카메라 API를 추상화해 쉽게 사용할 수 있도록 도와주며, 다음과 같은 기능을 제공한다.

  • Preview: 미리보기 화면 제공
  • ImageCapture: 사진 촬영
  • ImageAnalysis: 이미지 분석 (ML 등에 활용 가능)

본 프로젝트에서는 Preview를 통해 화면에 카메라 미리보기를 띄우고ImageAnalysis를 이용하여 실시간 프레임을 분석할 예정이다.

환경 세팅

프로젝트의 build.gradle파일에 다음 의존성을 추가한다.

// CameraX
implementation("androidx.camera:camera-core:1.4.2")
implementation("androidx.camera:camera-camera2:1.4.2")
implementation("androidx.camera:camera-lifecycle:1.4.2")

// ML Kit - Text Recognition (한국어 인식 가능)
implementation("com.google.mlkit:text-recognition-korean:16.0.1")

Manifest에 카메라 권한을 추가한다.
(권한 요청은 accompanist-permission 라이브러리를 사용했다)

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
    android:name="android.hardware.camera"
    android:required="false" />

TextRecognitionAnalyzer 클래스

텍스트를 분석하고 결과를 TextRecognitionAnalyzer 클래스는 카메라에서 실시간으로 들어오는 프레임을 ML Kit으로 전달하여 텍스트를 분석하고, 분석된 텍스트를 특정 콜백으로 전달하는 역할을 한다.

어떻게 구현해야할지 고민하던 와중 괜찮은 소스를 발견해서 해당 코드를 사용해보려고 한다.

참고 소스코드 : https://github.com/YanneckReiss/JetpackComposeMLKitTutorial

class TextRecognitionAnalyzer(
    private val onDetectedTextUpdated: (String) -> Unit
) : ImageAnalysis.Analyzer {

    companion object {
        const val THROTTLE_TIMEOUT_MS = 1_000L
    }

    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val textRecognizer: TextRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

    @OptIn(ExperimentalGetImage::class)
    override fun analyze(imageProxy: ImageProxy) {
        scope.launch {
            val mediaImage: Image = imageProxy.image ?: run { imageProxy.close(); return@launch }
            val inputImage: InputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

            suspendCoroutine { continuation ->
                textRecognizer.process(inputImage)
                    .addOnSuccessListener { visionText: Text ->
                        val detectedText: String = visionText.text
                        if (detectedText.isNotBlank()) {
                            onDetectedTextUpdated(detectedText)
                        }
                    }
                    .addOnCompleteListener {
                        continuation.resume(Unit)
                    }
            }

            delay(THROTTLE_TIMEOUT_MS)
        }.invokeOnCompletion { exception ->
            exception?.printStackTrace()
            imageProxy.close()
        }
    }
}

⭐️ TextRecognitionAnalyzer 클래스의 핵심 로직은 아래와 같다.

  • ImageAnalysis.Analyzer 인터페이스를 구현, CameraX로부터 프레임을 받아서 처리
  • ImageProxy 객체를 MLKit에서 사용할 수 있는 InputImage로 변환
  • 변환된 이미지를 MLKit 텍스트 인식기(TextRecognizer)로 전달하여 분석
  • 분석된 텍스트 결과를 콜백으로 전달

다음으로 코드를 하나씩 살펴보면,

1. 코루틴 스코프 (CoroutineScope) 정의

private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val textRecognizer: TextRecognizer = TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())

MLKit의 TextRecognizer.process() 메서드는 비동기 API이기 때문에 작업을 코루틴 내에서 실행할 수 있게 scope를 정의했다.

여기서 중요한 점은 저번 포스팅에서 얘기했던 SuperviorJob을 사용했다는 것이다!! CameraX는 매 프레임마다 analyze()를 호출하는데, 분석 작업인 process()가 실패하더라도 다음 프레임은 정상적으로 처리될 수 있게 하는 장치로 사용되었다.

2. suspendCoroutine 블록으로 래핑

suspendCoroutine { continuation ->
    textRecognizer.process(inputImage)
        .addOnSuccessListener { visionText: Text ->
            val detectedText: String = visionText.text
            if (detectedText.isNotBlank()) {
                onDetectedTextUpdated(detectedText)
            }
        }
        .addOnCompleteListener {
            continuation.resume(Unit)
        }
}
delay(THROTTLE_TIMEOUT_MS)

suspendCoroutine 블록은 코루틴 안에서 콜백 기반 비동기 작업을 코루틴의 일시 중지(suspend) 가능한 함수처럼 사용할 수 있게 해주는 중단점 함수이다.

  • suspendCoroutine : 코루틴 안에서 콜백 기반 비동기 API를 일시 중지(suspend)하고, 콜백 완료 시점에 직접 resume()을 호출해 다시 흐름을 이어주는 중단점 함수
  • continuation : 일시 중지된 코루틴의 다음 실행 지점을 담고 있는 객체

예를 들어, 위 코드에서 textRecognizer.process() 작업이 끝나기 전까지 코루틴은 suspendCoroutine에서 멈춰있는다. 그리고 addOnCompleteListener의 콜백이 호출되면서 continuation.resume(Unit)이 실행되면, 그제야 코루틴은 다시 재개되어 delay(...)부터 이후 코드가 실행되는 방식이다.

3. 스로틀링(delay)을 통한 성능 최적화

delay(THROTTLE_TIMEOUT_MS)

카메라의 프레임 속도에 맞춰 매 초 수십 프레임을 분석할 수 있기 때문에 오버헤드 방지를 위해 1초간 딜레이를 준다.

결과

마무리

소스코드를 통해 코루틴의 활용법도 깊이 있게 다뤄볼 수 있었고, SupervisorJob이 실시간 이미지 분석처럼 독립적인 작업이 연속적으로 발생하는 환경에서, 일부 실패가 전체 흐름을 중단시키지 않도록 보장하는 데 얼마나 중요한지도 체감할 수 있었다.

이제 이 코드를 프로젝트에 어떻게 자연스럽게 녹여낼지 고민해보려 한다.

profile
seoyoon's development blog

0개의 댓글