ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CameraX api 번외편 : guideline에 따라 이미지 자르기
    회사 생활/여권 NFC (CameraX + OCR + NFC) 2023. 11. 20. 18:37
    더보기

    카메라를 사용하면서 camera preview 안에 사각형 모양의 가이드 라인이 있고 가이드 만큼만 잘라서 사용해야 하는 상황을 만났다.

     

    구체적으로 얘기하자면 아래 여권사진에서 mrz라고 하는 부분만 잘라서 해당 문자열을 이용해서 데이터를 추출하고 활용해야 하는 상황을 만났다. 그래서 이미지 자르는 방법을 공부해 봤다.

     

    프로젝트 스크린샷

    더보기
    폴더블 접힌 스크린샷
    폴더블 열린 스크린샷

     

    하지만 이게 만만치 않다.

     

    그냥 생각 없이 Preview를 전체화면으로 놓으니 Preview화면과 실제 이미지가 저장되는 사진의 영역이 다르다.

     

    그래서 화면을 16:9나 4:3처럼 카메라가 지원하는 비율로 맞춰야 사진이 Preview 영역과 똑같이 나온다.

     

     

    해결 방법 :

    그래서 강제로 16:9를 맞춰줬다.

     

    CameraX의 Preview 화면을 xml로 추가하지 않고 화면 크기를 구하고 나중에 동적으로 Activity에 추가해 줬다.

        <FrameLayout
            android:id="@+id/frameLayout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

     

            //스크린 화면을 기준으로 16:9를 맞출 생각이다. 그래서 동적으로 넣어주기 위한 부분
            frameLayout = findViewById(R.id.frameLayout)
            val displayMetrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(displayMetrics)
    
            val screenWidth = displayMetrics.widthPixels
            val screenHeight = displayMetrics.heightPixels
    
            mCustomPreview = PreviewView(this)
            val frameLayoutParams: FrameLayout.LayoutParams?
            val flag: Boolean = (screenWidth / 9 * 16) < screenHeight
    
            if (flag) {
                frameLayoutParams = FrameLayout.LayoutParams(
                    screenWidth,
                    screenWidth / 9 * 16
                )
            } else {
                frameLayoutParams = FrameLayout.LayoutParams(
                    screenHeight / 16 * 9,
                    screenHeight
                )
            }
    
            mCustomPreview.layoutParams = frameLayoutParams
            frameLayout.addView(mCustomPreview)
    
            cameraExecutor = Executors.newSingleThreadExecutor()
    
            captureBtn.setOnClickListener {
                takeAndProcessImage()
            }

     

    일반적인 폰 같은 경우 왠만하면 세로의 길이가 가로의 16/9 보다 크다.

     

    그래서 화면에 벗어나지 않게 하기 위해 가로 기준으로 16:9를 세팅해주지만

     

    폴더블 폰 같은 경우 거의 1:1 수준이기 때문에 세로기준으로 16:9를 세팅해줬다. 

     

     

    그 다음은 이미지를 자르는 부분이다.  이미지를 자르기전에 이미지 크기를 PreviewView 크기에 맞춰주고

    mBitamp =
        Bitmap.createScaledBitmap(mBitamp, mCustomPreview.width, mCustomPreview.height, false);

     

    시작 좌표와 넓이 높이를 구해서 잘라주면 된다. 다만 guideline의 시작 좌표 같은 경우 PreviewView 기준이 아니라 현재 화면 기준이라서 FrameLayout의 좌표 만큼 빼줘야 한다.

        private fun cropImage(bitmap: Bitmap, frame: View, reference: View): Bitmap {
    
            val xInitialPos = (reference.x - mX).toInt()
            val yInitialPos = (reference.y - mY).toInt()
    
            Log.d("position", "mX : " + mX);
            Log.d("position", "mY : " + mY);
    
            val bitmapFinal = Bitmap.createBitmap(
                bitmap,
                xInitialPos, yInitialPos, reference.width, reference.height
            )
    
            val stream = ByteArrayOutputStream()
            bitmapFinal.compress(
                Bitmap.CompressFormat.PNG,
                100,
                stream
            ) //100 is the best quality possibe
            return bitmapFinal
        }

     

     

    결과 스크린샷 : 

     

    전체 코드 :

    더보기
    import android.Manifest
    import android.content.Intent
    import android.content.pm.PackageManager
    import android.graphics.Bitmap
    import android.graphics.Matrix
    import android.os.Build
    import android.os.Bundle
    import android.util.DisplayMetrics
    import android.util.Log
    import android.util.Rational
    import android.view.View
    import android.view.ViewTreeObserver.OnGlobalLayoutListener
    import android.widget.FrameLayout
    import android.widget.ImageView
    import android.widget.Toast
    import androidx.appcompat.app.AppCompatActivity
    import androidx.camera.core.*
    import androidx.camera.core.ImageCapture.OnImageCapturedCallback
    import androidx.camera.lifecycle.ProcessCameraProvider
    import androidx.camera.view.PreviewView
    import androidx.constraintlayout.widget.ConstraintLayout
    import androidx.core.app.ActivityCompat
    import androidx.core.content.ContextCompat
    import java.io.ByteArrayOutputStream
    import java.util.concurrent.Executor
    import java.util.concurrent.ExecutorService
    import java.util.concurrent.Executors
    
    class MainActivity : AppCompatActivity() {
    
        private val TAG = "MainActivity"
    
        private lateinit var imageCapture: ImageCapture
        private lateinit var guideline: ImageView
    
        private lateinit var cameraExecutor: ExecutorService
    
        private val ratio = AspectRatio.RATIO_16_9
        private lateinit var captureBtn: ImageView
    
        private lateinit var constraintLayout: ConstraintLayout
        private lateinit var frameLayout: FrameLayout
        private lateinit var mCustomPreview: PreviewView
    
        private var mX: Float = 0F
        private var mY: Float = 0F
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            frameLayout = findViewById(R.id.frameLayout)
            captureBtn = findViewById(R.id.capture_btn)
            guideline = findViewById(R.id.guideline)
            constraintLayout = findViewById(R.id.constraintLayout)
    
    
            //스크린 화면을 기준으로 16:9를 맞출 생각이다. 그래서 동적으로 넣어주기 위한 부분
            frameLayout = findViewById(R.id.frameLayout)
            val displayMetrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(displayMetrics)
    
            val screenWidth = displayMetrics.widthPixels
            val screenHeight = displayMetrics.heightPixels
    
            mCustomPreview = PreviewView(this)
            val frameLayoutParams: FrameLayout.LayoutParams?
            val flag: Boolean = (screenWidth / 9 * 16) < screenHeight
    
            if (flag) {
                frameLayoutParams = FrameLayout.LayoutParams(
                    screenWidth,
                    screenWidth / 9 * 16
                )
            } else {
                frameLayoutParams = FrameLayout.LayoutParams(
                    screenHeight / 16 * 9,
                    screenHeight
                )
            }
    
            mCustomPreview.layoutParams = frameLayoutParams
            frameLayout.addView(mCustomPreview)
    
            cameraExecutor = Executors.newSingleThreadExecutor()
    
            captureBtn.setOnClickListener {
                takeAndProcessImage()
            }
    
            // Request camera permissions
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
                )
            }
    
            frameLayout.viewTreeObserver
                .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
                    override fun onGlobalLayout() {
                        // 좌표 가져오기
                        mX = frameLayout.x
                        mY = frameLayout.y
    
                        frameLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
                    }
                })
        }
    
    
        //이미지를 저장하지 않고 데이터만 가져온다.
        private fun takeAndProcessImage() {
            val imageCapture = imageCapture
            val newExecutor: Executor = Executors.newSingleThreadExecutor()
    
            // 이미지 캡처 콜백 등록
            imageCapture.takePicture(
                newExecutor,
                object : OnImageCapturedCallback() {
                    override fun onCaptureSuccess(image: ImageProxy) {
                        // 이미지 데이터 추출 및 처리
                        processImage(image.toBitmap())
                        image.close()
                    }
    
                    override fun onError(exception: ImageCaptureException) {
                        // 캡처 에러 처리
                    }
                })
        }
    
        private fun processImage(bitmap: Bitmap) {
            // 이미지 데이터 추출
    
            var mBitamp = bitmap
            // 안드로이드는 돌려서 나오기 때문에 수정
            if (bitmap.width > bitmap.height) {
                mBitamp = rotateTheImage(90, bitmap)
            }
    
            mBitamp =
                Bitmap.createScaledBitmap(mBitamp, mCustomPreview.width, mCustomPreview.height, false);
    
            val smallBitmap = cropImage(mBitamp, mCustomPreview, guideline)
    
    
            val app = application as MyApplication
            app.fullImageBitmap = mBitamp
            app.smallImageBitmap = smallBitmap
    
            val intent = Intent(this, ResultActivity::class.java)
            startActivity(intent)
        }
    
        private fun cropImage(bitmap: Bitmap, frame: View, reference: View): Bitmap {
    
            val xInitialPos = (reference.x - mX).toInt()
            val yInitialPos = (reference.y - mY).toInt()
    
            Log.d("position", "mX : " + mX);
            Log.d("position", "mY : " + mY);
    
            val bitmapFinal = Bitmap.createBitmap(
                bitmap,
                xInitialPos, yInitialPos, reference.width, reference.height
            )
    
            val stream = ByteArrayOutputStream()
            bitmapFinal.compress(
                Bitmap.CompressFormat.PNG,
                100,
                stream
            ) //100 is the best quality possibe
            return bitmapFinal
        }
    
    
        private fun rotateTheImage(orientation: Int, oldBitmap: Bitmap): Bitmap {
            val newBitmap: Bitmap
            if (orientation != 0) {
                val matrix = Matrix()
                matrix.postRotate(orientation.toFloat())
                newBitmap = Bitmap.createBitmap(
                    oldBitmap,
                    0,
                    0,
                    oldBitmap.width,
                    oldBitmap.height,
                    matrix,
                    false
                )
                return newBitmap
            }
            return oldBitmap
        }
    
        private fun startCamera() {
            val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
    
            cameraProviderFuture.addListener({
                // Used to bind the lifecycle of cameras to the lifecycle owner
                val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
    
                // Preview
                val preview = Preview.Builder()
                    .setTargetAspectRatio(ratio)
                    .build()
                    .also {
                        it.setSurfaceProvider(mCustomPreview.surfaceProvider)
                    }
    
                imageCapture = ImageCapture.Builder()
                    .setTargetAspectRatio(ratio)
                    .build()
    
    
                // Select back camera as a default
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    
                val aspectRatio = Rational(9, 16)
    
                val viewPort: ViewPort = ViewPort.Builder(
                    aspectRatio,
                    mCustomPreview.display.rotation
                ).setScaleType(ViewPort.FIT).build()
    
    
                val useCaseGroupBuilder: UseCaseGroup.Builder = UseCaseGroup.Builder()
                    .setViewPort(viewPort)
                    .addUseCase(preview)
                    .addUseCase(imageCapture)
    
    
                try {
                    // Unbind use cases before rebinding
                    cameraProvider.unbindAll()
    
                    // Bind use cases to camera
                    val mCamera = cameraProvider.bindToLifecycle(
                        this, cameraSelector,
                        useCaseGroupBuilder.build()
                    )
    
    
                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }
    
            }, ContextCompat.getMainExecutor(this))
        }
    
        private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
            ContextCompat.checkSelfPermission(
                baseContext, it
            ) == PackageManager.PERMISSION_GRANTED
        }
    
    
        override fun onRequestPermissionsResult(
            requestCode: Int, permissions: Array<String>, grantResults:
            IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            if (requestCode == REQUEST_CODE_PERMISSIONS) {
                if (allPermissionsGranted()) {
                    startCamera()
                } else {
                    Toast.makeText(
                        this,
                        "Permissions not granted by the user.",
                        Toast.LENGTH_SHORT
                    ).show()
                    finish()
                }
            }
        }
    
        override fun onDestroy() {
            super.onDestroy()
            cameraExecutor.shutdown()
        }
    
        companion object {
            private const val REQUEST_CODE_PERMISSIONS = 10
            private val REQUIRED_PERMISSIONS =
                mutableListOf(
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO
                ).apply {
                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                        add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    }
                }.toTypedArray()
        }
    }

     

     

    github 주소 :

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

Designed by Tistory.