Android中使用CameraX无法正确裁剪指定矩形区域图像的问题
Android CameraX 预览框区域裁剪偏移问题
我尝试在Android中使用CameraX捕获预览界面矩形框内的图像,实现思路是计算屏幕与捕获图像的比例,将预览中可见的矩形映射到捕获图像的对应区域,再裁剪Bitmap。但裁剪后的结果图像位置偏下,需要解决这个偏移问题。
布局文件 fragment_scan_doc.xml
<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:id="@+id/clRoot" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.constraintlayout.widget.Guideline android:id="@+id/gdl_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_begin="28dp" /> <TextView android:id="@+id/tvTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_25dp" android:text="@string/app_name" android:textColor="@color/black" android:textSize="@dimen/text_size_24sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="@id/gdl_right" app:layout_constraintStart_toStartOf="@id/gdl_left" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tvDesc" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_16dp" android:text="@string/place_doc" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="@+id/gdl_right" app:layout_constraintStart_toStartOf="@+id/gdl_left" app:layout_constraintTop_toBottomOf="@+id/tvTitle" /> <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginTop="@dimen/dimen_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvDesc" /> <TextView android:id="@+id/tvCamera" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="13dp" android:background="@drawable/bg_round_white_solid_yellow_stripe" android:gravity="center" android:paddingStart="@dimen/dimen_25dp" android:paddingTop="@dimen/dimen_10dp" android:paddingEnd="@dimen/dimen_25dp" android:paddingBottom="@dimen/dimen_10dp" android:text="Front" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="@id/gdl_right" app:layout_constraintStart_toStartOf="@id/gdl_left" app:layout_constraintTop_toTopOf="@+id/previewView" /> <View android:id="@+id/border_view" android:layout_width="325dp" android:layout_height="202.5010436dp" android:background="@drawable/bg_round_grey_stripe" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/gdl_right" app:layout_constraintStart_toStartOf="@id/gdl_left" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tvPoweredBy" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:text="Powered by" android:textColor="@color/white" app:layout_constraintEnd_toEndOf="@id/gdl_right" app:layout_constraintStart_toStartOf="@id/gdl_left" app:layout_constraintTop_toBottomOf="@+id/border_view" /> <View android:id="@+id/vTakePhoto" android:layout_width="80dp" android:layout_height="80dp" android:layout_marginBottom="20dp" android:background="@drawable/bg_round_white_solid_stripe" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <ImageView android:id="@+id/ivFlash" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:background="@drawable/bg_circle_white_solid" android:padding="10dp" android:src="@drawable/ic_baseline_flash_off_24" app:layout_constraintBottom_toBottomOf="@+id/vTakePhoto" app:layout_constraintStart_toEndOf="@+id/vTakePhoto" app:layout_constraintTop_toTopOf="@+id/vTakePhoto" /> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:elevation="50dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/gdl_right" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_end="28dp" /> </androidx.constraintlayout.widget.ConstraintLayout>
拍照与图像捕获代码
private fun takePhoto() { imageCapture?.takePicture(ContextCompat.getMainExecutor(requireContext()), object : ImageCapture.OnImageCapturedCallback() { @SuppressLint("UnsafeOptInUsageError") override fun onCaptureSuccess(imageProxy: ImageProxy) { val image = imageProxy.image?.let { image -> InputImage.fromMediaImage( image, imageProxy.imageInfo.rotationDegrees ) } cropImage(imageProxy) } override fun onError(exception: ImageCaptureException) { exception.printStackTrace() } }) }
裁剪函数代码(原实现)
private fun cropImage(imageProxy: ImageProxy) { val ori = imageProxy.convertImageProxyToBitmap().rotate(90f) val rect = Rect() mViewBinding.borderView.getGlobalVisibleRect(rect) val screenWidth = mViewBinding.root.width val screenHeight = mViewBinding.root.height val ratioW = screenWidth.toFloat() / ori.width // 计算屏幕与图像宽度的比例 val ratioH = screenHeight.toFloat() / ori.height val x1 = rect.left / ratioW val y1 = rect.top / ratioH val x1W = rect.right / ratioW val x1H = rect.bottom / ratioH val rect1 = Rect(x1.toInt(), y1.toInt(), x1W.toInt(), x1H.toInt()) val cropedBm = Bitmap.createBitmap( ori, rect1.left, rect1.top, rect1.right - rect1.left, rect1.bottom - rect1.top ) processNewBitmap(cropedBm) // 显示裁剪后的Bitmap,结果位置偏下 }
问题原因与解决方案
问题根源
- 硬编码旋转图像:直接将图像旋转90度,未考虑
imageProxy.imageInfo.rotationDegrees的实际值,不同设备传感器方向不同会导致坐标映射错误。 - 比例计算错误:用屏幕整体宽高计算比例,未考虑
PreviewView按相机比例缩放后可能存在的黑边,导致坐标映射偏差。 - 坐标转换错误:
getGlobalVisibleRect获取的是屏幕绝对坐标,未转换为相对于PreviewView的相对坐标就进行映射。
修正后的裁剪函数
private fun cropImage(imageProxy: ImageProxy) { // 1. 按实际旋转角度处理图像 val rotationDegrees = imageProxy.imageInfo.rotationDegrees val oriBitmap = imageProxy.convertImageProxyToBitmap() val rotatedBitmap = when (rotationDegrees) { 90 -> oriBitmap.rotate(90f) 180 -> oriBitmap.rotate(180f) 270 -> oriBitmap.rotate(270f) else -> oriBitmap } // 2. 获取PreviewView在屏幕中的区域 val previewView = mViewBinding.previewView val previewRect = Rect() previewView.getGlobalVisibleRect(previewRect) // 3. 将边框坐标转换为PreviewView内部的相对坐标 val borderRect = Rect() mViewBinding.borderView.getGlobalVisibleRect(borderRect) val relativeLeft = borderRect.left - previewRect.left val relativeTop = borderRect.top - previewRect.top val relativeRight = borderRect.right - previewRect.left val relativeBottom = borderRect.bottom - previewRect.top // 4. 计算预览与捕获图像的正确映射比例 val previewWidth = previewView.width.toFloat() val previewHeight = previewView.height.toFloat() val imageWidth = rotatedBitmap.width.toFloat() val imageHeight = rotatedBitmap.height.toFloat() val scaleX = imageWidth / previewWidth val scaleY = imageHeight / previewHeight // 5. 计算图像中的目标裁剪区域 val cropLeft = (relativeLeft * scaleX).toInt() val cropTop = (relativeTop * scaleY).toInt() val cropRight = (relativeRight * scaleX).toInt() val cropBottom = (relativeBottom * scaleY).toInt() // 确保裁剪区域在图像范围内 val safeLeft = cropLeft.coerceAtLeast(0) val safeTop = cropTop.coerceAtLeast(0) val safeRight = cropRight.coerceAtMost(rotatedBitmap.width) val safeBottom = cropBottom.coerceAtMost(rotatedBitmap.height) // 6. 执行裁剪 val cropedBm = Bitmap.createBitmap( rotatedBitmap, safeLeft, safeTop, safeRight - safeLeft, safeBottom - safeTop ) processNewBitmap(cropedBm) // 释放资源避免内存泄漏 imageProxy.close() oriBitmap.recycle() rotatedBitmap.recycle() }
额外优化建议
- 确保
convertImageProxyToBitmap方法正确处理YUV到RGB的转换,避免图像失真。 - 在
PreviewView的SurfaceProvider回调中获取预览实际尺寸,进一步提升坐标映射精度。 - 裁剪后及时回收Bitmap资源。
内容的提问来源于stack exchange,提问作者theanilpaudel
相关产品推荐
相关产品推荐

