ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CameraX api 4-2편 : Analysis + OCR + TextGraphic
    회사 생활/여권 NFC (CameraX + OCR + NFC) 2023. 11. 18. 14:42

    4편에서는 CameraX api와 구글 MLKIT을 이용해서 Preview의 실시간 화면과 캡쳐된 화면에서의 Text 화면에서 Text를 불러왔다.

     

    이거는 CameraX api 활용하는 것과는 별로 상관 없지만 화면에 그림이 그려지는 것에 관심이 생겨서 읽어온 화면에서 Text가 그려지는 코드를 만들어 보려고 한다. 

     

    4편에서 추가된 코드 :

     

    GraphicOverlay.kt

    더보기
    import android.content.Context
    import android.content.res.Configuration
    import android.graphics.Canvas
    import android.graphics.Color
    import android.graphics.Paint
    import android.graphics.Rect
    import android.graphics.RectF
    import android.util.AttributeSet
    import android.view.View
    import androidx.camera.core.CameraSelector
    import com.google.mlkit.vision.text.Text
    import kotlin.math.ceil
    
    class GraphicOverlay(context: Context?, attrs: AttributeSet?) :
        View(context, attrs) {
    
        private val lock = Any()
        private val paint = Paint()
        private val customTexts: MutableList<CustomText> = ArrayList()
    
        private var mScale: Float? = null
        private var mOffsetX: Float? = null
        private var mOffsetY: Float? = null
        private var cameraSelector: Int = CameraSelector.LENS_FACING_BACK
    
        //    private lateinit var overlay:GraphicOverlay
        private val ROUND_RECT_CORNER:Float
    
        init {
            paint.color = Color.BLACK
            paint.textSize = 54.0f
            ROUND_RECT_CORNER = 1F
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            for (customText in customTexts) {
    
                customText.customTextBlock.boundingBox?.let { box ->
                    val rect = calculateRect(
                        customText.imageRect.height().toFloat(),
                        customText.imageRect.width().toFloat(),
                        box
                    )
    //                canvas.drawRoundRect(rect, ROUND_RECT_CORNER, ROUND_RECT_CORNER, paint)
                    canvas.drawText(
                        customText.customTextBlock.text,
                        rect.left,
                        rect.bottom,
                        paint
                    )
    
                }
            }
        }
    
        fun calculateRect(height: Float, width: Float, boundingBoxT: Rect): RectF {
    
            // for land scape
            fun isLandScapeMode(): Boolean {
                return this.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
            }
    
            fun whenLandScapeModeWidth(): Float {
                return when (isLandScapeMode()) {
                    true -> width
                    false -> height
                }
            }
    
            fun whenLandScapeModeHeight(): Float {
                return when (isLandScapeMode()) {
                    true -> height
                    false -> width
                }
            }
    
            val scaleX = this.width.toFloat() / whenLandScapeModeWidth()
            val scaleY = this.height.toFloat() / whenLandScapeModeHeight()
            val scale = scaleX.coerceAtLeast(scaleY)
            this.mScale = scale
    
            // Calculate offset (we need to center the overlay on the target)
            val offsetX = (this.width.toFloat() - ceil(whenLandScapeModeWidth() * scale)) / 2.0f
            val offsetY = (this.height.toFloat() - ceil(whenLandScapeModeHeight() * scale)) / 2.0f
    
            this.mOffsetX = offsetX
            this.mOffsetY = offsetY
    
            val mappedBox = RectF().apply {
                left = boundingBoxT.right * scale + offsetX
                top = boundingBoxT.top * scale + offsetY
                right = boundingBoxT.left * scale + offsetX
                bottom = boundingBoxT.bottom * scale + offsetY
            }
    
            // for front mode
            if (this.isFrontMode()) {
                val centerX = this.width.toFloat() / 2
                mappedBox.apply {
                    left = centerX + (centerX - left)
                    right = centerX - (right - centerX)
                }
            }
            return mappedBox
        }
    
    
        fun isFrontMode() = cameraSelector == CameraSelector.LENS_FACING_FRONT
    
        fun clear() {
            synchronized(lock) { customTexts.clear() }
            postInvalidate()
        }
    
        fun add(customText: CustomText) {
            synchronized(lock) { customTexts.add(customText) }
        }
        class CustomText constructor(var customTextBlock: Text.TextBlock, var imageRect: Rect)
    }

     

    activity_image_analysis.xml

    더보기
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ImageAnalysisActivity">
    
        <androidx.camera.view.PreviewView
            android:id="@+id/viewFinder"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintTop_toTopOf="parent">
    
        </androidx.camera.view.PreviewView>
    
        <w2022v9o12.simple.camerax_analysis.GraphicOverlay
            android:id="@+id/graphicOverlay_finder"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintTop_toTopOf="parent" />
    
    	
        ... 생략 ...
        
    </androidx.constraintlayout.widget.ConstraintLayout>

     

    ImageAnalysisActivity.kt

    더보기
    class ImageAnalysisActivity : AppCompatActivity() {
    
        ... 생략 ...
        private fun recognizeText(imageProxy: ImageProxy) {
            @ExperimentalGetImage
            val mediaImage: Image? = imageProxy.image
            if (mediaImage != null) {
                val image = InputImage.fromMediaImage(
                    mediaImage,
                    imageProxy.imageInfo.rotationDegrees
                )
    
                // [START run_detector]
                textRecognizer!!.process(image)
                    .addOnSuccessListener { visionText ->
                        processTextBlock(
                            visionText,
                            mediaImage.cropRect
                        )
                    }
                    .addOnCompleteListener { imageProxy.close() }
                    .addOnFailureListener { e ->
                        Log.d(
                            TAG,
                            "Text detection failed.$e"
                        )
                    }
                // [END run_detector]
            }
        }
    
    
        private fun processTextBlock(result: Text, rect: Rect) {
            var curText: String = ""
    
            viewBinding.graphicOverlayFinder.clear()
            result.textBlocks.forEach {
                val customText = GraphicOverlay.CustomText(it, rect)
                viewBinding.graphicOverlayFinder.add(customText)
                viewBinding.textView.text = it.text
    
                val lines = it.lines
                for (j in lines.indices) {
                    val elements = lines[j].elements
                    for (k in elements.indices) {
                        curText += elements[k].text
                    }
                }
            }
            viewBinding.textView.text = curText
            curText = ""
    
            viewBinding.graphicOverlayFinder.postInvalidate()
    
        }
        ... 생략 ...
    }

     

     

    기본 개념은 PreviewView 화면위에 위에 투명한 View를 하나 추가하고 그곳에 오버라이딩 된 onDraw를 해서 글씨를 그려주는 것이다.

     

    onDraw는 View를 Canvas 처럼 활용해서 그림, 글씨 뿐만 아니라 다양한 요소(Text, Verticle, RoundRect, Rect, Picture, Circle, Bitmap) 등을 나타낼 수 있다.

     

    Text를 화면에 그려주기 위해서는 2가지 요소가 필요하다.

    • Text
    • 좌표 (x, y)

     

    Text좌표 (x, y) 모두 ImageProxy에서 가져올 수 있다. 

     

    (1) 먼저 ImageAnalysis.Analyzer에서 ImageProxy를 가져온다.

        private fun setAnalysis(): ImageAnalysis.Analyzer {
            @ExperimentalGetImage
            val analysis = ImageAnalysis.Analyzer {
                recognizeText(it)
            }
            return analysis
        }

     

    (2) 그 다음 ImageProxy에서 Image 데이터 타입을 얻어온다. 

     

    (3) ImageProxy를 통해 Rect 타입의 데이터를 가져올 수 있고 이것으로 이미지에서 Text의 좌표를 얻어 올 수 있다.

     

    (4) 그리고 Image 타입의 데이터에서 Rect과 InputImage를 얻어온다.

     

    (5) InputImage에서는 mlkit(textRecognizer!!.process)을 활용해 Text를 얻어온다.

       private fun recognizeText(imageProxy: ImageProxy) {
            @ExperimentalGetImage
            val mediaImage: Image? = imageProxy.image
            if (mediaImage != null) {
                val image = InputImage.fromMediaImage(
                    mediaImage,
                    imageProxy.imageInfo.rotationDegrees
                )
    
                // [START run_detector]
                textRecognizer!!.process(image)
                    .addOnSuccessListener { visionText ->
                        processTextBlock(
                            visionText,
                            mediaImage.cropRect
                        )
                    }
                    .addOnCompleteListener { imageProxy.close() }
                    .addOnFailureListener { e ->
                        Log.d(
                            TAG,
                            "Text detection failed.$e"
                        )
                    }
                // [END run_detector]
            }
        }

     

     

     

    얻어온 Text 좌표 (x, y) 는 GraphicOverlay에 넘겨주고

     

    fun calculateRect(height: Float, width: Float, boundingBoxT: Rect): RectF

     

     

    calculateRect() 메소드로 정확한 좌표를 구하고 drawText로 Text를 표시해주면 끝!!!

    canvas.drawText(
        customText.customTextBlock.text,
        rect.left,
        rect.bottom,
        paint
    )

     

    postInvalidate() 는 View 위에 그려주는 것을 없애주는 것 같다.

     

    스크린샷 :

    더보기

     

    읽혀진 Textext가 완전히 같은 위치에 보여지지는 않아서 이 부분은 나중에 시간이 된다면 한번 정확한 위치에 표시 될수 있는지 한번 체크해봐야 할 것 같다. 

     

    Github 주소 :

    https://github.com/tvroom88/AIO_Android_Kotlin_Support_Material/tree/main/CameraX/CameraX_4_OCR_TextGraphic

Designed by Tistory.