基础
H.265与H.264是ITU-T VCEG 制定的视频编码标准。H.265是H.264升级版,保留原来的某些技术。H264可以低于1Mbps的速度实现标清数字图像传送;H265则可以实现利用1~2Mbps的传输速度传送720P(分辨率1280720)普通高清音视频传送。以Bitmap大小算,一秒钟视频24720* 1080 * 3 * 8 / 1024/1024= 427.1484375 相对于Bitmap序列压缩了400倍
要注意Android各个平台的实现可能不一样
YUV分类
- YUV和RGB一样,也是一种图像像素存储格式,起源于电视机产业,其特点是存储占用更小,Y代表亮度,U代表色度,V代表浓度
- 同样的由于多样性,YUV内部又产生了许多的分支,就像RGB的带A不带A,半精度等
为了维持人的肉眼观感,通常需要每个像素点保存8bit的y亮度,每2x2个点保存至少一个u和v值,如下所示,要理解它的排列就要知道,它在量化8bit之后,每个像素占用大小:
YUV的分类主要依据三个方面: 是按照通道顺序存储还是按照像素顺序 planar格式:连续存储所有像素点Y,然后是所有像素点U,接着是V packed格式:所有像素点的YUV信息连续交错存储,类似于BItmap uv的字节大小, uv的顺序,哪个前,哪个后,话说这玩意有啥意义?分个前后?
YUV,YCbCr,YPbPr写法的含义 它们分别代表在不同领域时使用的名称,总的大类都是一致的。
YUV444采样,4个y,4个uv。
四个像素点: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3] 存放码流: Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
YUV422采样,4个y,两个u两个v
四个像素点: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3] 存放码流: Y0 U0 Y1 V1 Y2 U2 Y3 V3
YUV411采样,每4个Y对应一组UV。
四个像素点: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3] 存放码流: Y0 U0 Y1 Y2 V2 Y3
YUV420采样,每4个Y对应一组UV,
实际上这个应该是411,但是命名为420了 每个由2x2个2行2列相邻的像素组成的宏像素需要占用6字节内存。,亮度4个字节,两个色度各1个字节。
四个像素点: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3] [Y5 U5 V5] [Y6 U6 V6] [Y7 U7 V7] [Y8 U8 V8] 存放码流: Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8
YUV422—包含如:YUYV、UYVY、YUV422P
YUV422,大多数的Android机 sensor出来的视频流都是这个格式 YUYV: 像素形式
YVYU:
YUV422P:2个Y对应一对UV,即Y00 Y01对应U00 V00 这个是通道格式
YUV420—包含如:YV12,YU12、NV12、NV21、YUV420SP、I420
这个420应该是411更好吧 在Android Camera框架中,setPreviewFormat中可传入的格式,API给出的2个可供选择的格式分别是ImageFormat.NV21和ImageFormat.YV12
YV12和YU12都属于YUV420p,其中Y\U\V分别对应一个plane,区别在于UV的位置对调,下面是YU12的存储示意图:
NV12和NV21,其中NV12就是我们Android常见的YUV420SP,他们不像上一个YV12,有3个plane,而是由Y和UV分别两个Plane组成,UV交替排列,U在前的是NV12,V在前为NV21.
YUV中stride跨距的含义? 跨距的由来,因为需要内存对齐,便于CPU处理,图像实际存储的一行长度通常是cpu字长的倍数,比如32的倍数,64的倍数, 在Android中,setPreviewFormat中就有标注YV12的跨距计算方式:
{@link android.graphics.ImageFormat#YV12}. For camera callback data,
* it can be assumed that the stride of the Y and UV data is the
* smallest possible that meets the alignment requirements. That is, if
* the preview size is <var>width x height</var>, then the following
* equations describe the buffer index for the beginning of row
* <var>y</var> for the Y plane and row <var>c</var> for the U and V
* planes:
*
* <pre>{@code
* yStride = (int) ceil(width / 16.0) * 16;
* uvStride = (int) ceil( (yStride / 2) / 16.0) * 16;
* ySize = yStride * height;
* uvSize = uvStride * height / 2;
* yRowIndex = yStride * y;
* uRowIndex = ySize + uvSize + uvStride * c;
* vRowIndex = ySize + uvStride * c;
* size = ySize + uvSize * 2;
* }
Android硬编码对YUV格式的要求
部分Android硬件平台对stride也有要求,比如MTK
// Note: the stride of resolution must be set as 16x for hard encoding with some chip like MTK // Since Y component is quadruple size as U and V component, the stride must be set as 32x if (!useSoftEncoder && vOutWidth % 32 != 0 || vOutHeight % 32 != 0) { if (vmci.getName().contains(“MTK”)) { throw new AssertionError(“MTK encoding revolution stride must be 32x”); } }
API使用
Android MediaFormat.KEY_COLOR_FORMAT代表的含义
常用的YUV ColorFormat项如下, 推荐我们去使用COLOR_FormatYUV420Flexible, COLOR_FormatYUV422Flexible, COLOR_FormatYUV444Flexible
COLOR_FormatYUV420Flexible 它大体意思是,哥们,我是个万能钥匙,对应了ImageFormat中的YUV_420_888,可以代替 COLOR_FormatYUV411PackedPlanar,COLOR_FormatYUV420Planar,COLOR_FormatYUV420PackedPlanar以及COLOR_FormatYUV420SemiPlanar,COLOR_FormatYUV420PackedSemiPlanar使用
PlaneProxy/Plane
Y、U和V三个分量的数据分别保存在三个Plane类中,即通过 getPlanes()得到的数组。 Plane 实际是对ByteBuffer的封装。 Image保证了planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且对于plane [0],Y分量数据一定是连续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值。 但是对于UV数据,可能存在以下两种情况:
- planes[1] = {UUUU…},planes[2] = {VVVV…}; //I420
- planes[1] = {UVUV…},planes[2] = {VUVU…}。
PixelStride
所以在我么取数据时需要在根据Plane中的另一个信息来确定如何取对应的U或者V数据。 // 行内数据值间隔 // 1:表示无间隔取值,即为上面的第一种情况 // 2: 表示需要间隔一个数据取值,即为上面的第二种情况 var pixelStride = plane.pixelStride
根据这个属性,我们将确定数据如何存储,因此如果需要取出代表I420格式的byte[],则为:YUV420中,Y数据长度为: width*height , 而U、V都为:width / 2 * height / 2。
val planes = image.planes
val yPixelStride = planes[0].pixelStride
val uPixelStride = planes[1].pixelStride
Log.e("xiao", "yPixelStride: $yPixelStride uPixelStride: $uPixelStride")
val pixelStride = planes[0].pixelStride
val y = planes[0].buffer
val u = ByteArray(image.width / 2 * image.height / 2)
val pixelStride = planes[1].pixelStride
if (pixelStride == 1) {
planes[1].buffer
} else if (pixelStride == 2) {
val uBuffer = planes[1].buffer
for (i in 0 until uBuffer.remaining() step 2) {
u[i] = uBuffer.get();
uBuffer.get()
}
}
RowStride
但是如果使用上面的代码去获取YUV数据,可能你会惊奇的发现,并不是在所有你设置的Width与 Height(分辨率)下都能够正常运行。我们忽略了什么,为什么会出现问题呢? 因为前面讲的内存对齐 getRowStride没有用到. 代码
val planes = image.planes
val size = image.width * image.height * 3 / 2
val yuv420 = ByteBuffer.allocate(size)
val plane = planes[0]
val pixelStride = plane.pixelStride
val rowStride = plane.rowStride
val buffer = plane.buffer
val row = ByteArray(image.width)
val skipRow = ByteArray(rowStride - image.width)
for (i in 0 until image.height) {
buffer[row]
yuv420.put(row)
if (i < image.height - 1) {
buffer[skipRow]
}
}
而对于U与V数据,对应的行步长可能为: 等于Width; 大于Width; 等于Width/2; 大于Width/2 等于Width 这表示,我们获得planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2。 详见 https://juejin.cn/post/6996678928018440223
转换公式 YUV to RGB
|