Android音视频-YUV学习

基础

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
这个是通道格式
Android音视频-YUV学习_第1张图片

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 width x height, then the following
     * equations describe the buffer index for the beginning of row
     * y for the Y plane and row c for the U and V
     * planes:
     *
     * 
{@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数据,可能存在以下两种情况:

  1. planes[1] = {UUUU…},planes[2] = {VVVV…}; //I420
  2. 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
//y数据的这个值只能是:1
val yPixelStride = planes[0].pixelStride
val uPixelStride = planes[1].pixelStride
Log.e("xiao", "yPixelStride: $yPixelStride uPixelStride: $uPixelStride") 
// Y数据 pixelStride一定为1
val pixelStride = planes[0].pixelStride
val y = planes[0].buffer // Y数据
val u = ByteArray(image.width / 2 * image.height / 2)
val pixelStride = planes[1].pixelStride
if (pixelStride == 1) {
    planes[1].buffer // U数据
} else if (pixelStride == 2) {
    val uBuffer = planes[1].buffer
    for (i in 0 until uBuffer.remaining() step 2) {
        u[i] = uBuffer.get();
        //丢弃一个数据,这个数据其实是V数据,但是我们还是到planes[2]中获取V数据
        uBuffer.get()
    }
}


RowStride

但是如果使用上面的代码去获取YUV数据,可能你会惊奇的发现,并不是在所有你设置的Width与 Height(分辨率)下都能够正常运行。我们忽略了什么,为什么会出现问题呢?
因为前面讲的内存对齐
getRowStride没有用到.
代码

val planes = image.planes
val size = image.width * image.height * 3 / 2
val  yuv420 = ByteBuffer.allocate(size)
/**
 * Y数据
 */
val plane = planes[0] //y数据

//pixelStride = 1 : 取值无间隔
//pixelStride = 2 : 间隔1个字节取值
// y的此数据应该都是1
val pixelStride = plane.pixelStride //Y的肯定为1

//大于等于宽, 表示连续的两行数据的间隔
//  如:640x480的数据,
//  可能得到640
//  可能得到650,表示每行最后10个字节为补位的数据
val rowStride = plane.rowStride //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 数据,没有无效占位数据,不需要丢弃
    }
}

而对于U与V数据,对应的行步长可能为:
等于Width;
大于Width;
等于Width/2;
大于Width/2
等于Width
这表示,我们获得planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2。
详见 https://juejin.cn/post/6996678928018440223

转换公式
YUV to RGB
Android音视频-YUV学习_第2张图片
yuv转换ncnn的mat
参考 https://zhuanlan.zhihu.com/p/336359747

ncnn的mat内存结构,针对图像,只考虑三维的
Android音视频-YUV学习_第3张图片

作者回答的:
mat shape w=3 h=2 c=4
internal memory layout
a00 a01 a02 a10 a11 a12 pad pad
b00 b01 b02 b10 b11 b12 pad pad
c00 c01 c02 c10 c11 c12 pad pad
d00 d01 d02 d10 d11 d12 pad pad

each channel is 16byte aligned, padding values may be filled in channel gaps
ncnn里面把用一行保存一个通道的数据,这一行必须是16的整数倍
mat.data -> address of a00
mat.row(1) -> address of a10
mat.channel(0).row(1) -> address of a10
mat.channel(1).row(1) -> address of b10

构造函数和指针结构
Android音视频-YUV学习_第4张图片
data指针是一个char*指针,也就是byte级别的指针,可以按照byte进行操作。

你可能感兴趣的:(机器学习,python)