-
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 주소 :
'회사 생활 > 여권 NFC (CameraX + OCR + NFC)' 카테고리의 다른 글
CameraX api 5-1편 : OCR + 여권(MRZ + NFC) (0) 2023.11.24 CameraX api 번외편 : guideline에 따라 이미지 자르기 (1) 2023.11.20 CameraX api 4편 : Anlysis와 text 분석 라이브러리(OCR) (0) 2023.11.14 CameraX api 3편 : takePicture 메소드 (0) 2023.11.13 CameraX api 2 : CameraX 기본 코드 분석 (0) 2023.11.10