You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android中使用CameraX拍摄相机屏幕指定区域的照片?

解决CameraX拍摄指定区域的问题

问题背景

你需要拍摄相机预览界面中camera_frame(250dp×250dp)框选的区域,之前尝试计算裁剪和第三方库都未成功,核心问题是预览界面坐标与相机实际输出图片的坐标不匹配,以及缩放、屏幕旋转带来的转换误差。

下面提供两种可靠的解决方案:


方案一:拍摄后精准裁剪(适配所有场景)

先拍摄完整照片,再根据预览框位置计算出需要裁剪的区域,彻底解决坐标映射问题。

步骤1:修改photoCapture()方法

替换你原有的photoCapture()方法,整合坐标转换与裁剪逻辑:

private fun photoCapture() {
    imageCapture ?: return

    val photoFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), SimpleDateFormat("yyyyMMddHHmmSS", Locale.US).format(System.currentTimeMillis()) + ".jpeg")
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

    imageCapture!!.takePicture(
        outputOptions, ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                try {
                    photoFile.delete()
                } catch (e: Exception) {
                    e.printStackTrace()
                }
                Toast.makeText(this@CameraActivity, "拍摄失败!", Toast.LENGTH_LONG).show()
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // 1. 获取预览框的屏幕坐标
                val frameLocation = IntArray(2)
                binding.cameraFrame.getLocationOnScreen(frameLocation)
                val frameLeft = frameLocation[0]
                val frameTop = frameLocation[1]
                val frameWidth = binding.cameraFrame.width
                val frameHeight = binding.cameraFrame.height

                // 2. 获取预览到图片的转换矩阵,解决缩放/比例差异
                val previewView = binding.previewView
                val matrix = Matrix()
                previewView.getPreviewMatrix(matrix)
                matrix.invert(matrix) // 反转矩阵,将屏幕坐标转成图片坐标

                // 3. 计算图片上对应的裁剪区域
                val frameRect = RectF(frameLeft.toFloat(), frameTop.toFloat(), (frameLeft + frameWidth).toFloat(), (frameTop + frameHeight).toFloat())
                matrix.mapRect(frameRect)

                // 4. 读取并裁剪图片
                val originalBitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
                // 确保裁剪坐标不超出图片范围
                val cropLeft = frameRect.left.toInt().coerceAtLeast(0)
                val cropTop = frameRect.top.toInt().coerceAtLeast(0)
                val cropWidth = frameRect.width().toInt().coerceAtMost(originalBitmap.width - cropLeft)
                val cropHeight = frameRect.height().toInt().coerceAtMost(originalBitmap.height - cropTop)

                val croppedBitmap = Bitmap.createBitmap(originalBitmap, cropLeft, cropTop, cropWidth, cropHeight)
                originalBitmap.recycle() // 释放原图片内存

                // 5. 保存裁剪后的图片
                val croppedFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "cropped_${System.currentTimeMillis()}.jpeg")
                try {
                    FileOutputStream(croppedFile).use { out ->
                        croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
                    }
                    croppedBitmap.recycle()
                    photoFile.delete() // 删除原全图(可选)
                    // 跳转到预览界面
                    CameraPreviewActivity.path = croppedFile.absolutePath
                    startActivity(Intent(this@CameraActivity, CameraPreviewActivity::class.java))
                } catch (e: Exception) {
                    e.printStackTrace()
                    Toast.makeText(this@CameraActivity, "裁剪失败!", Toast.LENGTH_LONG).show()
                }
            }
        }
    )
}

方案二:让CameraX直接拍摄指定区域(更高效)

通过设置ImageCapture的裁剪区域,让相机直接输出指定区域的照片,无需后续裁剪,性能更好。

步骤1:修改initializeCamera()方法

在相机初始化时计算传感器坐标系下的裁剪区域,并设置给ImageCapture:

private fun initializeCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
    cameraProviderFuture.addListener({
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(binding.previewView.surfaceProvider)
            }

        // 等待PreviewView加载完成后计算裁剪区域
        binding.previewView.post {
            // 1. 获取相机传感器信息
            val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
            val cameraInfo = camera.cameraInfo
            val sensorSize = cameraInfo.sensorResolution
            val previewSize = binding.previewView.width to binding.previewView.height

            // 2. 获取预览框的屏幕坐标
            val frameLocation = IntArray(2)
            binding.cameraFrame.getLocationOnScreen(frameLocation)
            val frameLeft = frameLocation[0]
            val frameTop = frameLocation[1]
            val frameWidth = binding.cameraFrame.width
            val frameHeight = binding.cameraFrame.height

            // 3. 计算屏幕到传感器的缩放比例
            val scaleX = sensorSize.width / previewSize.first.toFloat()
            val scaleY = sensorSize.height / previewSize.second.toFloat()
            val scale = max(scaleX, scaleY)

            // 4. 根据屏幕方向转换坐标(传感器坐标系为横向)
            val frameCenterX = frameLeft + frameWidth / 2
            val frameCenterY = frameTop + frameHeight / 2
            val (sensorCenterX, sensorCenterY) = when (resources.configuration.orientation) {
                Configuration.ORIENTATION_PORTRAIT -> {
                    val x = sensorSize.height - (frameCenterY * scale)
                    val y = frameCenterX * scale
                    x to y
                }
                else -> {
                    val x = frameCenterX * scale
                    val y = sensorSize.height - (frameCenterY * scale)
                    x to y
                }
            }

            // 5. 构建传感器坐标系下的裁剪矩形
            val cropWidth = (frameWidth * scale).toInt()
            val cropHeight = (frameHeight * scale).toInt()
            val cropRect = Rect(
                (sensorCenterX - cropWidth / 2).toInt(),
                (sensorCenterY - cropHeight / 2).toInt(),
                (sensorCenterX + cropWidth / 2).toInt(),
                (sensorCenterY + cropHeight / 2).toInt()
            )

            // 6. 创建带裁剪的ImageCapture实例
            imageCapture = ImageCapture.Builder()
                .setCropRect(cropRect)
                .build()

            // 重新绑定相机
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
        }
    }, ContextCompat.getMainExecutor(this))
}

注意事项

  • 部分设备可能不支持硬件裁剪,此时会自动降级为软件裁剪,效果一致但性能稍差。
  • 屏幕旋转后需要重新计算裁剪区域,可在onConfigurationChanged()中重新调用初始化逻辑。

关键优化点

  1. 坐标映射:通过PreviewView的转换矩阵解决预览与拍摄图片的比例差异,避免裁剪错位。
  2. 边界校验:强制裁剪坐标在图片范围内,防止崩溃。
  3. 内存管理:及时回收Bitmap,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Kaljal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 02:47:09