-
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
'회사 생활 > 여권 NFC (CameraX + OCR + NFC)' 카테고리의 다른 글
CameraX api 5-2편 : OCR + 여권(MRZ + NFC) (1) 2023.11.25 CameraX api 5-1편 : OCR + 여권(MRZ + NFC) (0) 2023.11.24 CameraX api 4-2편 : Analysis + OCR + TextGraphic (1) 2023.11.18 CameraX api 4편 : Anlysis와 text 분석 라이브러리(OCR) (0) 2023.11.14 CameraX api 3편 : takePicture 메소드 (0) 2023.11.13