Image
类在API 19中引入,但真正开始发挥作用还是在API 21引入CameraDevice
和MediaCodec
的增强后。API 21引入了Camera2
,deprecated掉了Camera
,确立Image
作为相机得到的原始帧数据的载体;硬件编解码的MediaCodec
类加入了对Image
和Image
的封装ImageReader
的全面支持。可以预见,Image
将会用来统一Android内部混乱的中间图片数据(这里中间图片数据指如各式YUV格式数据,在处理过程中产生和销毁)管理。
本文主要介绍YUV_420_888
格式的图片数据如何在Image
中存储和管理。
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
就这样横空出世了。
对于YUV来说图片的宽和高是必不可少的,因为YUV本身只存储颜色信息,想要还原出图片,必须知道图片的长宽。Image
保存有图片的宽和高,可以通过getWidth()
和getHeight()
得到。
每个Image
当然有自己的格式,这个格式由ImageFormat
确定。对于YUV420,ImageFormat
在API 21中新加入了YUV_420_888
类型,其表示YUV420格式的集合,888
表示Y、U、V分量中每个颜色占8bit。既然只能指定YUV420这个格式集合,那怎么知道具体的格式呢?马上就来回答这个问题。
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下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,此格式下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和SemiPlanar的表现是一致的,也就是说,可能Android已经帮我们解决了Packed的问题,只有Semi留给我们自己解决。
综上,我们只需要根据pixelStride和rowStride就能在对应的plane中获取到相应的颜色数据,而不必知道具体的YUV420格式。
根据官方文档的介绍是说,CropRect指定了图片内的一个矩形区域,只有这个区域内的像素才是有效的,但鉴于我目前还没碰到这个问题,也不好详细解释。不过有两点要注意,首先是坐标系的变换,一定要弄清楚Rect和图片的长和宽的关系;其次是U和V的偏移量问题,U和V中颜色点是Y的1/4,在Rect中要计算好U和V中数据的范围,避免发生错位等。
本篇主要介绍YUV_420_888
格式的图片数据如在Image
中的存储和管理,其中重点又放在U和V分量的管理上。本篇算作对官方文档的一点补充,如果想要深入理解,还需从源码入手。
转载自:https://www.polarxiong.com/archives/Android-Image%E7%B1%BB%E6%B5%85%E6%9E%90-%E7%BB%93%E5%90%88YUV_420_888.html