YUV_420_888图像裁剪跨设备异常求助(三星S22 Ultra/A14)
YUV_420_888图像裁剪跨设备兼容性问题
在处理YUV_420_888格式图像裁剪时,遇到跨设备兼容问题:三星S22 Ultra上裁剪后图像错乱,但三星A14上表现正常。
示例图像
原始图像

裁剪后异常图像

接收相机帧并裁剪的代码
override fun analyzeImageProxy(imageProxy: ImageProxy) { val mediaImage = imageProxy.image if (mediaImage != null) { val bitmap = ImageConvertUtils.getInstance().getUpRightBitmap(InputImage.fromMediaImage(mediaImage, 0)) println(bitmap) // 此时图像正常,对应原始图像 val croppedImageByteArray = cropYUV420( imageToByteArray(mediaImage), mediaImage.width, mediaImage.height, imageProxy.cropRect.left, imageProxy.cropRect.top, imageProxy.cropRect.width(), imageProxy.cropRect.height() ) val inputImage = InputImage.fromByteArray( croppedImageByteArray, imageProxy.cropRect.width(), imageProxy.cropRect.height(), imageProxy.imageInfo.rotationDegrees, InputImage.IMAGE_FORMAT_NV21 ) val bitmap2 = ImageConvertUtils.getInstance().getUpRightBitmap(inputImage) println(bitmap2) // 此时图像错乱,对应异常图像 imageScanner.scanFrame(imageProxy, inputImage) } }
裁剪相关代码
private fun cropYUV420( yuvData: ByteArray, imageWidth: Int, imageHeight: Int, cropX: Int, cropY: Int, cropWidth: Int, cropHeight: Int ): ByteArray { val croppedYUV = ByteArray(cropWidth * cropHeight * 3 / 2) // 裁剪Y平面 for (row in 0 until cropHeight) { val srcPos = (cropY + row) * imageWidth + cropX val destPos = row * cropWidth System.arraycopy(yuvData, srcPos, croppedYUV, destPos, cropWidth) } // 计算UV平面的起始位置 val uvHeight = imageHeight / 2 val uvCropHeight = cropHeight / 2 val uvImageWidth = imageWidth / 2 val uvCropWidth = cropWidth / 2 val uStart = imageWidth * imageHeight val vStart = uStart + (uvHeight * uvImageWidth) // 裁剪U平面 for (row in 0 until uvCropHeight) { val srcPos = uStart + (cropY / 2 + row) * uvImageWidth + cropX / 2 val destPos = cropWidth * cropHeight + row * uvCropWidth System.arraycopy(yuvData, srcPos, croppedYUV, destPos, uvCropWidth) } // 裁剪V平面 for (row in 0 until uvCropHeight) { val srcPos = vStart + (cropY / 2 + row) * uvImageWidth + cropX / 2 val destPos = cropWidth * cropHeight + uvCropHeight * uvCropWidth + row * uvCropWidth System.arraycopy(yuvData, srcPos, croppedYUV, destPos, uvCropWidth) } return croppedYUV } private fun imageToByteArray(image: Image): ByteArray { val yBuffer = image.planes[0].buffer // Y val uBuffer = image.planes[1].buffer // U val vBuffer = image.planes[2].buffer // V val ySize = yBuffer.remaining() val uSize = uBuffer.remaining() val vSize = vBuffer.remaining() val nv21 = ByteArray(ySize + uSize + vSize) // YUV420格式中U和V需要交换顺序 yBuffer.get(nv21, 0, ySize) vBuffer.get(nv21, ySize, vSize) uBuffer.get(nv21, ySize + vSize, uSize) return nv21 }
问题根源及解决方案
问题原因
- 忽略YUV Plane的行间距(Row Stride):不同设备内存对齐策略不同,三星S22 Ultra这类高端机型会采用内存对齐优化(如16字节对齐),导致Plane的
rowStride大于实际图像宽度。旧代码直接用imageWidth作为行宽拷贝数据,会造成数据偏移,引发图像错乱;而三星A14未做严格对齐,rowStride等于图像宽度,所以旧代码能正常运行。 - 裁剪参数未适配YUV420格式:YUV420的UV通道是2x2像素对应一个Y块,裁剪的坐标(cropX、cropY)和尺寸(cropWidth、cropHeight)必须为偶数,否则会导致UV通道与Y通道对齐错误。
修正代码
1. 修正imageToByteArray函数(处理Row Stride)
private fun imageToByteArray(image: Image): ByteArray { val yPlane = image.planes[0] val uPlane = image.planes[1] val vPlane = image.planes[2] val yBuffer = yPlane.buffer val uBuffer = uPlane.buffer val vBuffer = vPlane.buffer val yRowStride = yPlane.rowStride val yPixelStride = yPlane.pixelStride val uvRowStride = uPlane.rowStride val uvPixelStride = uPlane.pixelStride val width = image.width val height = image.height val nv21 = ByteArray(width * height * 3 / 2) // 拷贝Y通道,处理行间距 var yIndex = 0 for (row in 0 until height) { for (col in 0 until width) { nv21[yIndex++] = yBuffer.get(row * yRowStride + col * yPixelStride) } } // 拷贝UV通道(NV21格式为Y+V+U交错存储) var uvIndex = width * height for (row in 0 until height / 2) { for (col in 0 until width / 2) { nv21[uvIndex++] = vBuffer.get(row * uvRowStride + col * uvPixelStride) nv21[uvIndex++] = uBuffer.get(row * uvRowStride + col * uvPixelStride) } } return nv21 }
2. 调整裁剪参数为偶数(适配YUV420格式)
在analyzeImageProxy中修改裁剪参数:
override fun analyzeImageProxy(imageProxy: ImageProxy) { val mediaImage = imageProxy.image if (mediaImage != null) { val bitmap = ImageConvertUtils.getInstance().getUpRightBitmap(InputImage.fromMediaImage(mediaImage, 0)) println(bitmap) val cropRect = imageProxy.cropRect // 调整裁剪参数为偶数,避免UV通道对齐错误 val adjustedCropX = cropRect.left - (cropRect.left % 2) val adjustedCropY = cropRect.top - (cropRect.top % 2) val adjustedCropWidth = cropRect.width() - (cropRect.width() % 2) val adjustedCropHeight = cropRect.height() - (cropRect.height() % 2) val croppedImageByteArray = cropYUV420( imageToByteArray(mediaImage), mediaImage.width, mediaImage.height, adjustedCropX, adjustedCropY, adjustedCropWidth, adjustedCropHeight ) val inputImage = InputImage.fromByteArray( croppedImageByteArray, adjustedCropWidth, adjustedCropHeight, imageProxy.imageInfo.rotationDegrees, InputImage.IMAGE_FORMAT_NV21 ) val bitmap2 = ImageConvertUtils.getInstance().getUpRightBitmap(inputImage) println(bitmap2) imageScanner.scanFrame(imageProxy, inputImage) } }
3. 保留原cropYUV420函数
经过上述修正后,imageToByteArray输出的是紧凑的NV21格式(无行间距),原cropYUV420函数可以直接使用,无需修改。
内容的提问来源于stack exchange,提问作者Bugs Happen
相关产品推荐
相关产品推荐

