ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Compose] ML kit으로 바코드 읽고, OCR을 해보자
    Android📱 2023. 5. 4. 22:46

     

    CameraX x Compose x ML kit을 이용한 OCR 

     

    구글에서 제공하는 ML Kit에는 다양한 기능들이 있다.

    그중 OCR 기능과 바코드 스캔 기능이 존재한다.

     

    *OCR이란

    Optical Character Recognition의 약자로 광학 문자 인식이라고 해석할 수 있다.

    광학 문자 인식(OCR)은 텍스트 이미지를 기계가 읽을 수 있는 텍스트 포맷으로 변환하는 과정이다.

    이미지에 있는 텍스트를 인식해서 추출해 내는 기능이라고 생각하면 된다.

     

    이번에 이 기능들을 써야 할 일이 생겨서 예제를 만들어보며 기능을 적용시켜 보았다.

     

    이번에 사용해 볼 기능들을 보면 제일 필요한 것은 카메라이다.

     

    카메라 같은 경우는 CameraX를 사용해 주면 더 수월하게 진행이 가능해진다.

     

    1. dependencies 추가

      // To recognize Latin script
      implementation 'com.google.mlkit:text-recognition:16.0.0'
    
      // To recognize Chinese script
      implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
    
      // To recognize Devanagari script
      implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0'
    
      // To recognize Japanese script
      implementation 'com.google.mlkit:text-recognition-japanese:16.0.0'
    
      // To recognize Korean script
      implementation 'com.google.mlkit:text-recognition-korean:16.0.0'

    ML Visiond에 Text 인식을 사용하려면 위에 dependency를 추가해 주면 된다.

    각 언어에 맞게 추가해 주면 되고 기본으로 추가하면 한글인식이 잘 안 된다.

     

    // Use this dependency to bundle the model with your app
      implementation 'com.google.mlkit:barcode-scanning:17.1.0'

    바코드, QR 인식은 위에 dependency를 추가해주어야 한다.

     

    2. Manifest

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="barcode|ocr" >
          <!-- To use multiple models: android:value="barcode,model2,model3" -->
    </application>

    매니페스트에도 메타 데이터를 추가해 주어야 하는데

    텍스트 인식과 바코드를 둘 다 사용할 거라서 barcode | ocr 이런 식으로 추가해 주면 둘 다 사용이 가능해진다.

     

    그리고 마지막 남은 CameraX까지 추가해 주면 된다.

    // CameraX
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-view:${camerax_version}"
    implementation "androidx.camera:camera-extensions:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"

    카메라를 사용하려면 당연히 권한 추가를 해주어야 한다.

    퍼미션은 Compose다 보니 accompanist를 이용했다.

    이 글에서는 카메라에 관한 설명은 안 할 거라서

    퍼미션 관련내용은 Accompanist를 참고하면 된다.

     

    3. 퍼미션 화면

    카메라를 쓰기 위해서는 퍼미션을 확인해야 하니 권한 화면을 만들어서 권한을 확인해 준다.

    이 부분은 Accompanist 참조

     

    4. Preview화면

     

    CameraX를 사용하면 카메라 화면을 Preview로 보아야 한다.

    Compose에서는 AndroidView를 사용해서 Preview를 만들어 주어야 한다

     

    @Composable
    fun CameraPreViewScreen(
        state: CameraScreenState,
        navToResult: (String) -> Unit
    ) {
        val lifecycleOwner = LocalLifecycleOwner.current
        val context = LocalContext.current
        val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    
        Column(
            modifier = Modifier,
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            AndroidView(
                factory = { ctx ->
                    val previewView = PreviewView(ctx)
                    val executor = ContextCompat.getMainExecutor(ctx)
    
                    cameraProviderFuture.addListener({
                        val cameraProvider = cameraProviderFuture.get()
                        val preview = Preview.Builder().build().apply {
                            setSurfaceProvider(previewView.surfaceProvider)
                        }
    
                        val cameraSelector = CameraSelector.Builder()
                            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                            .build()
    
                        cameraProvider.unbindAll()
                        cameraProvider.bindToLifecycle(
                            lifecycleOwner,
                            cameraSelector,
                            state.imageCapture,
                            preview
                        )
                    }, executor)
                    previewView
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(9f),
            )
            Button(
                onClick = {
                    takePicture(
                        context = context,
                        imageCapture = state.imageCapture,
                        executorService = state.cameraExecutor,
                        navToResult = navToResult
                    )
                },
                modifier = Modifier
            ) {
                Icon(imageVector = Icons.Default.Done, contentDescription = null)
            }
        }
    }

    takePicture 함수

    fun takePicture(
        context: Context,
        imageCapture: ImageCapture,
        executorService: ExecutorService,
        navToResult: (String) -> Unit
    ) : String {
        MediaActionSound().play(MediaActionSound.SHUTTER_CLICK) // 셔터 소리
        val outputDirectory = context.getOutputDirectory()
        // Create output file to hold the image
        val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputDirectory).build()
        var value = ""
        imageCapture.takePicture(outputFileOptions, executorService,
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(error: ImageCaptureException) {
                    value = "fail"
                }
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    outputFileResults.savedUri?.let {
                        recognizeText(InputImage.fromFilePath(context, it))
                            .addOnCompleteListener { task -> navToResult(task.result.text) }
                    }
                }
            })
        return value
    }

    사진을 촬영하고, 이미지에서 텍스트를 추출한다.

     

    5. ML kit text 추출 함수

    fun recognizeText(image: InputImage): Task<Text> {
        val koreanRecognizer = TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())
        Log.i("흥수", "rr")
        return koreanRecognizer.process(image)
            .addOnSuccessListener { visionText ->
                for (block in visionText.textBlocks) {
                    val boundingBox = block.boundingBox
                    val cornerPoints = block.cornerPoints
                    val text = block.text
                    Log.i("흥수 ko 1", text)
                    processTextBlock(visionText)
                }
            }.addOnFailureListener { e ->
                Log.e("$e", "koreanRecognizer 실패 ${e.message}")
            }
    }
    
    private fun processTextBlock(result: Text) {
        // [START mlkit_process_text_block]
        val resultText = result.text
        Log.i("흥수2", resultText)
        for (block in result.textBlocks) {
            val blockText = block.text
            val blockCornerPoints = block.cornerPoints
            val blockFrame = block.boundingBox
            Log.i("흥수3", blockText)
            for (line in block.lines) {
                val lineText = line.text
                val lineCornerPoints = line.cornerPoints
                val lineFrame = line.boundingBox
                Log.i("흥수4", lineText)
                for (element in line.elements) {
                    val elementText = element.text
                    val elementCornerPoints = element.cornerPoints
                    val elementFrame = element.boundingBox
                    Log.i("흥수5", elementText)
                }
            }
        }
    }

    텍스트 추출은 간단하다. TextRecognition의 getClient에 분석하고 싶은 언어팩이 있는 옵션을 넣어어서 인스턴스를 생성하고,

    process로 InputImage를 넘겨주면 콜백으로 성공, 실패 여부와 데이트를 반환해 준다.

    (InputImage.fromFilePath에 저장된 이미지 경로만 넘겨주면 InputImage를 만들 수 있다.)

    그 데이터로 해주고 싶은 액션을 해주면 끝!

     

    6. 바코드 스캔 

     

    바코드 스캔은 ImageAnalysis.Analyzer를 이용

    카메라에 잡히는 이미지를 바로 분석해서 바코드의 정보를 가져오는 방식으로 진행해 보자

     

    @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
    private fun getImageAnalyzer(): ImageAnalysis.Analyzer {
        val scanner = getScanner()
        return ImageAnalysis.Analyzer { imageProxy ->
            val mediaImage = imageProxy.image
            mediaImage?.let {
                val image = InputImage.fromMediaImage(
                    mediaImage,
                    imageProxy.imageInfo.rotationDegrees
                )
                scanner.process(image).addOnSuccessListener { list ->
                    list.forEach { barcode ->
                        when (barcode.valueType) {
                            Barcode.TYPE_WIFI -> {
                                val ssid = barcode.wifi!!.ssid
                                val password = barcode.wifi!!.password
                                val type = barcode.wifi!!.encryptionType
                                Timber.tag("CardScanner").i(ssid)
                                Timber.tag("CardScanner").i(password)
                                Timber.tag("CardScanner").i(type.toString())
                                barcode.wifi?.let { callBacks[CallBackType.ON_SUCCESS]?.invoke(it.toString()) }
                            }
    
                            Barcode.TYPE_URL -> {
                                val title = barcode.url!!.title
                                val url = barcode.url!!.url
                                Timber.tag("CardScanner").i("title %s", title)
                                Timber.tag("CardScanner").i("url %s", url)
                                url?.let { callBacks[CallBackType.ON_SUCCESS]?.invoke(it) }
                            }
                        }
                    }
                }.addOnCompleteListener {
                    imageProxy.close()
                    mediaImage.close()
                }.addOnFailureListener {
                    callBacks[CallBackType.ON_FAIL]?.invoke("바코드 스캔에 실패하였습니다.")
                }
            }
        }
    }
    val imageAnalysis = ImageAnalysis.Builder()
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build().apply {
            setAnalyzer(
                executor,
                getImageAnalyzer()
            )
        }

    위 코드를 프리뷰를 설정하는 AndroidView 안에 addListener에 추가해 주고

     

    cameraProvider.bindToLifecycle(
        lifecycleOwner,
        cameraSelector,
        preview,
        imageCapture,
        analysis
    )

    바인딩할 때 analysis를 추가해 주면 ImageAnalysis.Analyzer를 사용할 수 있다.

    Analyzer는 ImageProxy를 반환해 준다

    ImageProxy안에 image를 받아서 InputImage로 전환하고, 바코드 분석 함수에 넘겨주면

    텍스트 추출처럼 바코드의 정보도 받아올 수 있다.

     

    7. 바코드 스캔 함수

     

    val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
            Barcode.FORMAT_QR_CODE
        ).enableAllPotentialBarcodes()
        .build()
    val client = BarcodeScanning.getClient(options)

    바코드 스캔 함수도 텍스트 추출과 같다

    getClient를 사용해서 client를 만들고,

    process에 InputImage를 넘겨주면 분석해서 바코드의 정보를 반환해 준다.

    option을 줘서 바코드의 타입도 정할 수 있음

     

     

    여기까지 작성을 하면 사진을 찍어서 텍스트를 추출하고,

    바코드를 스캔하는 간단한 앱을 만들 수 있다!

     

    참고 : https://github.com/ese111/CardScanner

     

    GitHub - ese111/CardScanner

    Contribute to ese111/CardScanner development by creating an account on GitHub.

    github.com

     

    * 계속 업데이트 중이라 코드가 설명과 많이 다를 수 있습니다. 먼저 간단한 앱을 만들어서 CameraX와 ML kit 사용법을 익히신 후 확인하시는 걸 추천드립니다.

Designed by Tistory.