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

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
}

问题根源及解决方案

问题原因

  1. 忽略YUV Plane的行间距(Row Stride):不同设备内存对齐策略不同,三星S22 Ultra这类高端机型会采用内存对齐优化(如16字节对齐),导致Plane的rowStride大于实际图像宽度。旧代码直接用imageWidth作为行宽拷贝数据,会造成数据偏移,引发图像错乱;而三星A14未做严格对齐,rowStride等于图像宽度,所以旧代码能正常运行。
  2. 裁剪参数未适配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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 13:19:52