简介

Image类在API 19中引入,但真正开始发挥作用还是在API 21引入CameraDeviceMediaCodec的增强后。API 21引入了Camera2,deprecated掉了Camera,确立Image作为相机得到的原始帧数据的载体;硬件编解码的MediaCodec类加入了对ImageImage的封装ImageReader的全面支持。可以预见,Image将会用来统一Android内部混乱的中间图片数据(这里中间图片数据指如各式YUV格式数据,在处理过程中产生和销毁)管理。

本文主要介绍YUV_420_888格式的图片数据如何在Image中存储和管理。

从YUV420谈起

YUV即通过Y、U和V三个分量表示颜色空间,其中Y表示亮度,U和V表示色度。不同于RGB中每个像素点都有独立的R、G和B三个颜色分量值,YUV根据U和V采样数目的不同,分为如YUV444、YUV422和YUV420等,而YUV420表示的就是每个像素点有一个独立的亮度表示,即Y分量;而色度,即U和V分量则由每4个像素点共享一个。举例来说,对于4x4的图片,在YUV420下,有16个Y值,4个U值和4个V值。

YUV420根据颜色数据的存储顺序不同,又分为了多种不同的格式,如YUV420Planar、YUV420PackedPlanar、YUV420SemiPlanar和YUV420PackedSemiPlanar,这些格式实际存储的信息还是完全一致的。举例来说,对于4x4的图片,在YUV420下,任何格式都有16个Y值,4个U值和4个V值,不同格式只是Y、U和V的排列顺序变化。I420(YUV420Planar的一种)则为YYYYYYYYYYYYYYYYUUUUVVVV,NV21(YUV420SemiPlanar)则为YYYYYYYYYYYYYYYYUVUVUVUV。也就是说,YUV420是一类格式的集合,YUV420并不能完全确定颜色数据的存储顺序。

Image

这么多眼花缭乱的格式名字自然是不利于程序开发的,Image就这样横空出世了。

长和宽

对于YUV来说图片的宽和高是必不可少的,因为YUV本身只存储颜色信息,想要还原出图片,必须知道图片的长宽。Image保存有图片的宽和高,可以通过getWidth()getHeight()得到。

图片格式

每个Image当然有自己的格式,这个格式由ImageFormat确定。对于YUV420,ImageFormat在API 21中新加入了YUV_420_888类型,其表示YUV420格式的集合,888表示Y、U、V分量中每个颜色占8bit。既然只能指定YUV420这个格式集合,那怎么知道具体的格式呢?马上就来回答这个问题。

YUV分量

Y、U和V三个分量的数据分别保存在三个Plane类中,可以通过getPlanes()得到。Plane实际是对ByteBuffer的封装。Image保证了plane #0一定是Y,#1一定是U,#2一定是V。且对于plane #0,Y分量数据一定是连续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值。

接下来看看U和V分量,我们考虑其中的三类格式:Planar,SemiPlanar和PackedSemiPlanar。

Planar

Planar下U和V分量是分开存放的,所以我们也应当能够一次性从plane #1和plane #2中获得所有的U和V分量值,事实也是如此。

下面是一段YUV420Planar格式Image解析的记录

image format: 35
get data from 3 planes
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2088960
Finished reading data from plane 0
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 522240
Finished reading data from plane 1
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 522240
Finished reading data from plane 2

Image中获得的图片格式是35,即YUV_420_888,一共有3个planes,图片分辨率为1920x1080,像素点个数为2073600;可以看到Y分量包含有全部的像素点,而U和V都只含有1/4的像素点,显然是YUV420。更为明显的是,Y分量中rowStride为1920,pixelStride代表行内颜色值间隔,取1表示无间隔,即对于一行1920个像素点每个都有独立的值,根据其buffer size可以得出共有1080行;而U分量中,一行1920个像素点只有960个值,即行内每两个像素点共用一个U值,根据其buffer size得出共有540行,即行间每两个像素点共用一个U值;这就是YUV420的采样了。

SemiPlanar

再来看看SemiPlanar,此格式下U和V分量交叉存储,Image并没有为我们将U和V分量分离出来

下面是一段YUV420SemiPlanar格式Image解析的记录

image format: 35
get data from 3 planes
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2088960
Finished reading data from plane 0
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1044479
Finished reading data from plane 1
pixelStride 2
rowStride 1920
width 1920
height 1080
buffer size 1044479
Finished reading data from plane 2

图片格式依然是YUV_420_888,Y分量与上述Planar中一样。但U和V分量出现了变化,buffer size是Y分量的1/2,如果说U分量只包含有U分量信息的话应当是1/4,多出来了1/4的内容,我们稍后再仔细看。注意到U中rowStride为1920,即U中每1920个数据代表一行,但pixelStride为2,代表行内颜色值间隔为1,就说是只有行内索引为0 2 4 6 ...才有U分量数据,这样来说还是行内每两个像素点共用一个U值,行间每两个像素点共用一个U值,即YUV420。

U和V的pixelStride都是2,我们从U和V中挑相同位置的20个byte值出来看看相互之间的关系。

124 -127 124 -127 123 -127 122 -127 122 -127 123 -127 123 -127 123 -127 122 -127 123 -127
-127 124 -127 123 -127 122 -127 122 -127 123 -127 123 -127 123 -127 122 -127 123 -127 123

上面一行来自U,下面一行来自V,最前面一个byte的索引值相同,且为偶数。可以明显发现U和V分量只是进行了一次移位,而这个移位就保证了从索引0开始间隔取值就一定能取到自己分量的值。所以可以简单来说U和V分量就是复制的UV交叉的数据。

这样想要获取U分量值的话只需要以pixelStride为间隔获取就好了,V分量也是一样。虽然你也可以只从U或V分量得到U和V分量的信息,但毕竟官方并没有保证这一点,多少有些风险。另外如果想要知道更多的细节,也可以去翻Android源码。

PackedSemiPlanar

这个简单点说,不知为何,在我的设备上PackedSemiPlanar和SemiPlanar的表现是一致的,也就是说,可能Android已经帮我们解决了Packed的问题,只有Semi留给我们自己解决。

综上,我们只需要根据pixelStride和rowStride就能在对应的plane中获取到相应的颜色数据,而不必知道具体的YUV420格式。

关于CropRect

根据官方文档的介绍是说,CropRect指定了图片内的一个矩形区域,只有这个区域内的像素才是有效的,但鉴于我目前还没碰到这个问题,也不好详细解释。不过有两点要注意,首先是坐标系的变换,一定要弄清楚Rect和图片的长和宽的关系;其次是U和V的偏移量问题,U和V中颜色点是Y的1/4,在Rect中要计算好U和V中数据的范围,避免发生错位等。

小结

本篇主要介绍YUV_420_888格式的图片数据如在Image中的存储和管理,其中重点又放在U和V分量的管理上。本篇算作对官方文档的一点补充,如果想要深入理解,还需从源码入手。

参考