Android: Image类浅析(结合YUV_420_888)
简介
Image
类在API 19中引入,但真正开始发挥作用还是在API 21引入CameraDevice
和MediaCodec
的增强后。API 21引入了Camera2
,deprecated掉了Camera
,确立Image
作为相机得到的原始帧数据的载体;硬件编解码的MediaCodec
类加入了对Image
和Image
的封装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分量的管理上。本篇算作对官方文档的一点补充,如果想要深入理解,还需从源码入手。
感谢楼主的讲解.
有没有方法可以把 YUV_420_888 的 Array 直接进行切割,然后得出一个缩小的图像?而我在处理 1280 * 720 的视频帧的时候,Image Plane[0]的容量是1105664,Plane[1]和Plane[2]是552703符合你说的 SemiPlan Y 和 UV 的关系.但是为什么 Y 的大小会多那么多 ?我想问一下楼主几个问题:
感激不尽.
哈喽,首先感谢博主的文章帮了我很大的忙。
但是在实践过程中发现 NV21格式有些迷惑, 因此到Wiki查阅了一下, 以下为Wiki原文:“NV21 is like NV12, but with U and V order reversed: it starts with V.” 因此NV21的格式应该为 YYYYYYYYVUVU。 看了好多博客都写着 YYYYYYYYUVUV 我自己都怀疑自己的判断了。
没有必要再找一份工作。 在线工作。
链接 - http://2olega.ru/go?https://hdredtube3.mobi/btsmart
作者牛逼
buffersize = 2088960;width = 1920;pixelStride = 1;为什么可以得出共有1080行呀?
因为pixelStride = 1,而且2088960/1920 = 1088,所以应该是有1088行才对,但是height又是1080,这个地方不理解,求博主指导一下。
还有对齐填充数据,每一行不止1920
审议匪浅,感谢博主
返回的rowstride的底层实现在哪里?可以改吗
SemiPlanar 格式,uv分量的值怎么是y分量的二分之一少一?
1920*1080
Y:2073600
UV:1036799
不应该是1036800 吗?
举例来说,Plane[1] 代表的是U分量,pixelStride 为2,取U分量索引为0, 2, 4, 6 ... 1036798,省略了最后一个没有必要的V值,size就是1036799
关于CropRect还真需要用到!
可是我设置了之后,没有效果,不知道
image.setCropRect(new Rect(500,500,previewWidth16/100,previewHeight11/100));
再做Image.Plane[] planes = image.getPlanes();
和没有设置一样,这块真心迷惑
我的理解。按照结论如何设置后只有rect内是有效的那么这个参数肯定是在硬件填充数据时发挥作用。这样也节省硬件性能。做不到这一点就没必要封装到image里。开发者自己随意记录下就行了
还有一点就是。如果数据是随机的那么很难区分正常数据和随机数据。有这么个框框就能知道只有框框里的数据才是真数据
您好,我想知道关于CropRect一些的官方文档,能给我分享一下吗?
在做这方面的研究还不太明白,谢谢
抱歉CropRect我之前看文档也没太弄明白,不太确定这样还需不需要额外的操作~
你好, 请问能否在三星galaxy S8 或S9 上测试你的代码? 我跑过这个例子,得到的jpg是绿色的乱码。
我看到你的代码考虑到了stride,也觉得处理正确,但机子上泡出来的却是乱掉的图。
附上Log
31987-32036 V: rect crop Rect(0, 0 - 560, 320)
31987-32036 V: get data from 3 planes
31987-32036 V: pixelStride 1
31987-32036 V: rowStride 640
31987-32036 V: width 560
31987-32036 V: height 320
31987-32036 V: buffer size 204720
31987-32036 V: Finished reading data from plane 0
31987-32036 V: pixelStride 2
31987-32036 V: rowStride 640
31987-32036 V: width 560
31987-32036 V: height 320
31987-32036 V: buffer size 102319
31987-32036 V: Finished reading data from plane 1
31987-32036 V: pixelStride 2
31987-32036 V: rowStride 640
31987-32036 V: width 560
31987-32036 V: height 320
31987-32036 V: buffer size 102319
31987-32036 V: Finished reading data from plane 2
请问试过DEMO么?之前代码里格式转换有个bug在GitHub的DEMO里修复了的,试试看?
"在我的设备上PackedSemiPlanar和SemiPlanar的表现是一致的"
请问你是如何调试设备输出不同YUV格式的呢?
我在使用camera2 API的时候指定imageReader为 YUV_420_888, 其中具体格式是不是设备决定的呢?谢谢。
这个在另一篇文章(你看左边的“相关文章”)里有相关的代码,可以获取芯片支持的格式列表,然后可以在MediaCodec里指定格式。
如果指定格式为YUV_420_888,那么具体的(比YUV420更具体)格式是不确定的,但Android确保了具体格式一定是YUV420的(意思就是芯片一定有格式对应到YUV420大类)。
关于CropRect那一部分
我真的遇到了!!!!!!!!!!!!!!!
我解码视频出来image.getheight得到的是1088!!!!
我真想大大地给android一个f狐狸浏览器k!!!
十分感谢博主这系列的博文,拯救了几周的工作时间orz
哈哈哈,留下一个手机型号吧,我有机会也能测试下
非常感谢博主关于图像方面的博文,可惜百度出来排名不高,基本都是google到你的干货的,获益良多
哈哈,之前专门给百度做过搜索优化,可惜等了一个月也没有啥起色,倒是Google我一直没管但排名还一直在上升,所以就放弃百度啦。
您好 Android Camera2预览实时捕获数据帧 之前yuv转图片一直不成功 直到用了您的yuv转换成图片的代码 才成功了 但是图片旋转了 请教怎样才能使得跟预览时的图像一致 在保存图片的时候也一致
这个是需要设置相机参数的,可以参考
https://www.polarxiong.com/archives/Android%E7%9B%B8%E6%9C%BA%E5%BC%80%E5%8F%91-%E5%9B%9B-%E6%97%8B%E8%BD%AC%E4%B8%8E%E7%BA%B5%E6%A8%AA%E6%AF%94.html
这篇里面的介绍,但这篇讲的是Camera的设置,你可以对照看看Camera2该怎么设置
好的 研究研究 谢谢
博主能指导一下Image里面的抽象方法close的具体应该怎么重写来实现回收Image资源,降低功耗么
这个我没有仔细看呢...不过你可以看看源码能不能看到Image存储数据的实现。如果涉及到native层,那你就没多少事情可以干了,除非你去写C++代码手动回收垃圾;如果全是Java代码,因为Java的垃圾自动回收机制,那你也只需要给不需要用到的数组赋值null,等待自动回收了。
如果你说的是比较高深的回收的话....那我就帮不到你咯
nv21uv分量排列顺序难道不是:vuvuvuvu
你说的是对的!
这个问题我前段时间遇到bug就发现了,只是这段时间太忙没来得及改过来,抱歉误导你了!
效率太低了,简直无法忍受,640*480 转换耗费 20MS sansung note5
你是指Image转成byte[]的I420或NV21效率太低?抱歉目前找不到更快速的方法了,I420可能会快一些,NV21还需要遍历交错插入。
如果你想绕过Image转byte[]这步,目前我只能说唯一的解决办法是绕过生成Image那步,直接生成byte[]。
如果你有更好的方法,欢迎反馈!
如何绕过image转byte啊?