java yuv转bitmap_Android的YUV_420_888图片转换Bitmap时的rowStride问题

郎羽
2023-12-01

简介

在Android上开发目标检测识别的程序时,需要最终传入Bitmap格式的数据给Native Library的接口,完成C/C++端代码的调用,所以,不能将Android Camera拿到的图像数据Image或ImageProxy直接传入。

这里牵涉到一个图像处理的细节,Android的Camera相关开发一直是一个难点,特别是牵涉到各种摄像头硬件时,所以Android也不停改善,有了Camera,又提供了Camera2,但是Camera2的API比较复杂,让人难以集中于业务逻辑的开发,所以在推出Jetpack时,也包含了CameraX组件。当然,这不是本文的重点。

重点是,一般的图形图像处理时,从性能考虑,拿到的图像信息都是以YUV方式编码的,这种方式确实有很多好处,可以节省存储空间(也意味着降低了传输成本),对黑白屏的支持也是天然的,不需要再转码。YUV的具体知识,可以参见: 一文读懂 YUV 的采样与格式

但是使得不得不加入了Image转Bitmap的操作,但是不知道为什么,在我看来是一个挺常用的操作,但是Android却没有提供原生支持,由于Android的碎片化,实现起来还挺繁琐的。

问题

由于目标检测的代码,是C++实现的,接受的参数是一个bitmap对象,这样就需要图像格式转码。由上文的一文读懂 YUV 的采样与格式可以看出,YUV和RGB的转换公式其实挺简单的,但问题在于这个如果用Java来实现的话,就会造成性能问题(这里可以考虑引入Kotlin Native来完成),所以网上找了这个转码代码,在自己手机上测试一下,完美!

谁知,到了同事手机上之后,竟然闪退!一看代码,就是在这个ImageUtil的代码中,数组越界造成的。接着就是具体的调研。

原因

经过调试,发现确实越界了,为什么呢?一开始这件事是难以理解的,因为按照YUV_420_888的定义,每4个像素,每个像素有一个Y(明暗)数据外,共享1个U和1个V,所以平均一个像素占用的字节数是1+1/4+1/4=3/2,所以创建的buffer大小就是width*height*3/2,没错啊,但是现实又是明明白白越界访问了。经过单步调试,发现原来原始数据的大小并不是width*height*3/2,图形的width明明是720,但是图像对应的rowStride值却是768,多出了48来,而在我自己的手机上,width和rowStride却是相同的,这是怎么回事呢?

首先,所谓的rowStride,就是每行数据的跨度,理论上不是应该720像素宽的图像,它的rowStride也是720吗?rowStride的要求,只要是16的倍数就OK可,720也是16的倍数啊。原来这个竟然是和摄像头硬件绑定的,估计是摄像头本身的硬件特性,造成了同事的手机的图像rowStride值为768,个人感觉这个应该是类似C语言时的对齐操作一样的概念,可以在得到目标图像时,尽可能少的操作数据。

解决

知道了这个问题后,那么就是要解决了。其实知道了原因后,解决方案还是挺简单的。首先是检测rowStride是否与width一致,一致的话,就直接用原来的代码,不一致呢,只要做一个判断即可,因为所谓的rowStride大于width,类似以下的情况

YYYYYYYYRRRR

YYYYYYYYRRRR

YYYYYYYYRRRR

YYYYYYYYRRRR

YYYYYYYYRRRR

YYYYYYYYRRRR

UVUVUVUVRRRR

UVUVUVUVRRRR

UVUVUVUV

所以解决方法就是

之前因为Y是连续的,所以可以用width*height获得长度后,一次性memcpy到目标buffer,现在不行了,你得每次只copy width长度的数据,然后跳过rowStride-width个字符,这样重复height次

同理,之前U和V的读取操作,也是认为UV区域是连续的,现在每次读取完width长度后,也要跳过rowStride-width长度,并重复

其实上面的示意图还说明了一个情况,就是rowStride比width长的场景下,整个图像的size并不是rowStride*height,而是rowStride*height-(rowStride-width),因为最后一行数只占width的长度,所以rowStride是768,height是480的场景下,image下的buffer size并不是768*480=368640,而是768*480-(768-720)=368592,了解这个算是一个知识点吧

相关代码

fun imageToBitmap(image: ImageProxy): Bitmap? {

val yBuffer = image.planes[0].buffer

val uBuffer = image.planes[1].buffer

val uStride = image.planes[1].pixelStride

val vBuffer = image.planes[2].buffer

val vStride = image.planes[2].pixelStride

val buffer = ByteArray(image.width * image.height * 3 / 2)

val rowStride = image.planes[0].rowStride

val padding = rowStride - image.width

var pos = 0

if (padding == 0) {

pos = yBuffer.remaining()

yBuffer.get(buffer, 0, pos)

} else {

var yBufferPos = 0

for (row in 0 until image.height) {

yBuffer.position(yBufferPos)

yBuffer.get(buffer, pos, image.width)

yBufferPos += rowStride

pos += image.width

}

}

var i = 0

val uRemaining = uBuffer.remaining()

while (i < uRemaining) {

buffer[pos++] = uBuffer[i]

i += uStride

if (padding == 0) continue

val rowLen = i % rowStride

if (rowLen >= image.width) {

i += padding

}

}

i = 0

val vRemaining = vBuffer.remaining()

while (i < vRemaining) {

buffer[pos++] = vBuffer[i]

i += vStride

if (padding == 0) continue

val rowLen = i % rowStride

if (rowLen >= image.width) {

i += padding

}

}

return ColorConvertUtil.yuv420pToBitmap(buffer, image.width, image.height)

}

上述ColorConvertUtil的实现,参考这里

 类似资料: