该文章参考《攻克视频技术》-- 李江的文章所写,是对其学习的总结和记录,有机会建议大家阅读原文章
基础概念
图像基础概念
- 像素:图像的基础单元
- 分辨率:图像的大小或尺寸,用像素个数表示
- 位深:存储每个像素所需的二进制位数
- 跨距(Stride):图像存储时内存中每行像素所占用的空间(为了满足字节对齐)
视频基础概念
- 帧率:1秒内图像的数量
- 码率:视频在单位时间内的数据量大小(Kb/s或Mb/s)
RGB和YUV
基础知识
RGB
基础介绍
- 每一个像素都有R、G、B三个值,三个值依次排序存储,组成一个像素点。
- 注意:RGB图像像素中R、G、B三个值并不一定按R、G、B排序,也有可能是B、G、R
YUV
基础介绍
- YUV图像将亮度信息Y,与色彩信息U、V分离开来,Y表示亮度,是图像的总体轮廓,称为Y分量。U、V表示色度,主要描绘图像的色彩等信息,分别称之为U分量和V分量。
YUV分类
- YUV444:每一个Y分量对应一组UV分量
- YUV422:每两个Y分量对应一组UV分量
- YUV420:每四个Y分量对应一组UV分量
YUV存储方式
- Planar:先存储所有像素点的Y分量,再存储所有像素点的U分量,最后存储所有像素点的V分量(U分量和V分量的顺序可以交换)
- Packed:先存储所有像素点的Y分量,然后U、V分量连续交错存储(U、V分量的顺序可以交换,不过要保证是交替存储)
-
不同YUV类型对应存储类型
YUV类型 存储类型 U、V分量存储顺序 YUV444
I444 (U、V顺序存储) 先U后V YV24 (U、V顺序存储) 先V后U YUV422
Packed
(U、V交替存储)
NV16 先U后V NV61 先V后U Planar
(U、V顺序存储)
YU16(I422) 先U后V YV16 先V后U YUV420
Packed
(U、V交替存储)
NV12 先U后V NV21 先V后U Planar
(U、V顺序存储)
YU12(I420) 先U后V YV12 先V后U
RGB和YUV之间的转换
Color Range
对于一个8bit位深的RGB图像,每个R、G、B分量的取值不一定都是0~255,这涉及到了Color Range的概念了,Color Range分为两种:
- Full Range:R、G、B取值范围是0~255
- Limited Range:R、G、B取值范围是16~235
RGB和YUV互转的标准规范
目前主要的标准有两个BT709和BT601(还有一个BT2020),其中BT601是标清的标准,BT709是高清的标准。
不同Color Range和标准下,RGB和YUV的互转:
BT601 | BT709 | ||
---|---|---|---|
Limited Range (16~235) | RGB转YUV | Y = 0.299 * R + 0.587 * G + 0.114 * B U = -0.172 * R - 0.339 * G + 0.511 * B + 128 V = 0.511 * R - 0.428 * G - 0.083 * B + 128 | Y = 0.213 * R + 0.715 * G + 0.072 * B U = -0.117 * R - 0.394 * G + 0.511 * B + 128 V = 0.511 * R - 0.464 * G - 0.047 * B + 128 |
YUV转RGB | R = Y + 1.371 * (V - 128) G = Y - 0.336 * (U - 128) - 0.698 * (V - 128) B = Y + 1.732 * (U - 128) | R = Y + 1.540 * (V - 128) G = Y - 0.183 * (U - 128) - 0.459 * (V - 128) B = Y + 1.816 * (U - 128) | |
Full Range (0~255) | RGB转YUV | Y = 0.257 * R + 0.504 * G + 0.098 * B + 16 U = -0.148 * R - 0.291 * G + 0.439 * B + 128 V = 0.439 * R - 0.368 * G - 0.071 * B + 128 | Y = 0.183 * R + 0.614 * G + 0.062 * B + 16 U = -0.101 * R - 0.339 * G + 0.439 * B + 128 V = 0.439 * R - 0.339 * G - 0.040 * B + 128 |
YUV转RGB | R = 1.164 * (Y - 16) + 1.596 * (V - 128) G = 1.164 * (Y - 16) - 0.392 * (U - 128) - 0.812 * (V - 128) B = 1.164 * (Y - 16) + 2.016 * (U - 128) | R = 1.164 * (Y - 16) + 1.792 * (V - 128) G = 1.164 * (Y - 16) - 0.213 * (U - 128) - 0.534 * (V - 128) B = 1.164 * (Y - 16) + 2.114 * (U - 128) |
注意:对于上诉公式中的常数,没有固定的标准和好坏,一般都是根据实际情况和场景,实验得出的一些经验值
想了解关于这方面的公式推导过程,可以自行网上搜索或者参考这几篇文章:
缩放算法
缩放的基本原理
图像的缩放就是将原图像的已有像素经过加权运算得到目标图像的目标像素。
假设原图像分辨率为W0 x H0,我们需要将其缩放到W1 x H1,那么我们只需要将目标图像中像素位置(x, y)映射到原图像的(x*W0/W1, Y*H0/H1),再通过插值算法就可以得到目标像素位置(x, y)的像素值了。
三种插值算法
最临近插值
原理
- 首先,将目标图像中的目标像素位置,映射到原图像的映射位置。
- 然后,找到原图像中映射位置周围的4个像素。
- 最后,取离映射位置最近的像素点的像素值作为目标图像像素位置对应的像素值。
例如:我们加个原图像720p放大到目标图像1080p,现在我们要计算目标图像1080p中坐标为(0, 1)位置的像素值:
第一步:将目标图像中目标像素位置映射到原图像中,通过公式计算可得映射坐标为(1*1280/1920, 0*720/1080)=(0.67, 0)
第二步:找到原图像映射位置周围的4个像素,分别是(0, 0)、(1, 0)、(0, 1)、(1, 1)
第三步:取离映射位置最近的像素点的像素值作为目标位置像素值,很明显是(1, 0)
所以目标图像1080p位置(0, 1)的像素值就是原图像中(1, 0)位置的像素值了,其他像素点依次类推,这就是最临近插值算法。
优缺点
- 优点:速度快,不需要太多计算。
- 缺点:缩放后的图像质量不好,很大概率会导致相邻两个插值像素相同,得到的放大图像大概率会出现块状效应,缩小图像容易出现锯齿。
双线性插值
原理
线性插值是一种以距离为权重的插值方式,距离越近权重—越大,距离越远权重越小。
例如:如下图所示,已知(x1, y1)和(x2, y2)两个点,求x对应的y值。
通过线性插值的方法,y值的计算公式如下:
双线性插值就是三次线性插值的过程,我们先通过两次线性插值得到两个中间值,再通过这两个中间值进行一次线程插值得到最终的结果。
例如:将原图像720p放大到目标图像1080p,求目标图像中(2, 2)位置的像素值。
第一步,将目标图像的目标位置(2, 2)通过公式映射到原图像中,坐标为(1.33, 1.33)
第二步:找到原图像中(1.33, 1.33)周围的4个像素,分别是(1, 1)、(2, 1)、(1, 2)和(2, 2),对应下图分别是a、 b、c和d
第三步:先通过a和b点线性插值得到点m(1.33, 1),c和d线性插值得到点n(1.33, 2),再通过m和n线性插值的到点p(1.33, 1.33)的像素值,计算公式如下:
然后将得到的像素值赋值给目标图像1080p的点(2, 2)位置的目标像素即可。
优缺点
- 优点:缩放后图像质量比最临近插值好
- 缺点:速度比最临近插值慢
双三次插值
原理
通过BiCubic基函数计算得到带插值像素周围16个像素的权重,然后将16个像素加权平均就可以得到最终的待插值像素了。
BiCubic基函数形式如下:
双三次插值的权重值是分水平和垂直两个方向分别求得的,计算公式都是通过BiCubic基函数求得的。对于周围16个点中的每一个点,其坐标为(x, y),而目标图像中的目标像素在原图像中的映射坐标为p(u, v),那么通过BiCubic基函数公式可以求其水平权重W(u - x),垂直权重W(v - y),将W(u - x)乘以W(v- y)得到最终的权重值,然后再用最终权重值乘以该点像素值,并对16个点分别做同样的操作并求和,就得到待插值的像素值了。公式如下:
例如:将原图像720p放大到目标图像1080p,求目标图像中目标像素点(2, 2)对应的像素值。
第一步:将目标像素点(2, 2)映射到原图像中为(1.33, 1.33)
第二步:找到原图像中(1.33, 1.33)周围的16个像素,如下图所示:
第三步:通过BiCubic函数求得每一个点的水平和垂直权重。如,求(0, 0)、(1, 2)和(3, 3)坐标点的水平和垂直权重如下图所示:
按照此方法,分别求出16个像素点的最终权重,最后每个像素用自己最终权重乘以自己的像素值再求和,就得到点(1.33, 1.33)的插值像素值,然后将其赋值给目标图像1080p的点(2, 2)即可。
优缺点
- 优点:缩放后图像质量是三个算法中最好的
- 缺点:速度是三个算法中最慢的
编码原理
视频编码的原理
图像的数据冗余
图像一般都是有数据冗余的,主要包括这四种:
- 空间冗余:比如说将一帧图像划分为一个个16x16的块之后,相邻的块很多都有比较明显的相似性。
- 时间冗余:一个帧率为25fps的视频中前后两帧图像相差只有40ms,两张图像的变化是比较小的,相似度很高。
- 视觉冗余:人眼是有视觉灵敏度的,对于图像中的高频信息的敏感度是小于低频信息的。有时候去除图像中的一些高频信息,人眼看起来和原图像差别不大。
- 信息熵冗余:可以使用压缩工具对图像进行压缩。
对于视频的编码,就是减少这几种冗余来实现的。
视频数据压缩编码过程
视频编码主要分为预测、DCT变换、量化、熵编码几个步骤:
- 帧内预测:为了提高熵编码的压缩率,先将当前编码块的相邻块像素经过帧内预测算法得到帧内预测块,再用当前编码块减去帧内预测块得到残差块,从而去掉空间冗余。
- 帧间预测:类似与帧内预测,在已经编码完成的帧中,先通过运动搜索得到帧间预测块,再与编码块相减得到残差块,从而去除时间冗余。
- DCT变换(离散余弦变换)和量化:将残差块变换到频域,分离高频和低频信息。由于高频信息数量多但高频系数较小,又由于人眼对高频信息相对不敏感,我们使用QStep(QP)对DCT系数进行量化,将大部分高频信息化为0(就是用高频系数除以QStep),达到去除视觉冗余的目的。(QP值越大图像越模糊)
- 熵编码(以行程编码为例):视频编码中真正实现“压缩”的步骤,主要是去除信息熵冗余。在出现连续多个0像素的时候压缩率会更高。
编码器的对比及选择
编码标准 | 块大小和划分方式 | 帧内编码 | 帧间编码 | 变换 | 熵编码 | 滤波和后处理 |
---|---|---|---|---|---|---|
H264 | 最大16x16, 可划分为8x16、16x8、8x8、4x8、8x4、4x4 | 8个方向模式+planar+DC模式 | 中值MVP | DCT 4x4/8x8 | CAVLC、CABAC | 去块滤波 |
H265 | 最大支持64x64,四叉树划分 | 33个方向模式+planar+DC模式 | Merge模式 AMVP模式 | DCT 4x4/8x8/16x16/32x32 DST 4x4 | CABAC | 去块滤波 SAO滤波 |
AV1 | 最大支持128x128,四叉树划分 | 56个方向模式+3个平滑模式+递归Filterintra模式+色度CFL模式+调色板模式+帧内块拷贝模式 | OBMC+扭曲运动补偿+高级复合预测+复合帧内预测 | 4x4 - 64x64正方形+1:2/2:1+1:4/4:1矩形DCT/ADST/flipADST/IDTX | 多符号算术编码 | 去块滤波CDEF LR滤波 Frame超分 Film Grain |
码流结构
H264的编码结构
帧类型
帧类型 | 预测方式 | 参考帧 | 优缺点 | |
---|---|---|---|---|
I帧 | 帧内编码帧 | 只进行帧内编码 | 无 | 优点:自身能独立完成编解码 缺点:压缩率小 |
P帧 | 前向编码帧 | 可以进行帧间预测和帧内预测 | 参考前面已编码的I帧和P帧 | 优点:压缩率比I帧高 缺点:必须要参考帧才能正确编解码 |
B帧 | 双向编码帧 | 可以进行帧间预测和帧内预测 | 参考前面或后面已编码的I帧和P帧 | 优点:压缩率最好 缺点:需要缓存帧,延时高。RTC场景不适合 |
IDR帧 | 特殊的I帧 | 只进行帧内编码 | 无 | 优点:自身能独立完成编解码 缺点:压缩率小 |
因为P帧的编解码可以参考前面的P帧,所以当有一个P帧出现错误的时候,错误就会一直往后传递,所以H264编码标准中规定,IDR帧(特殊的I帧,也叫立即刷新帧)之后的帧不能再参考IDR之前的帧。
GOP
GOP图像组:从一个IDR帧开始到下一个IDR帧的前一帧为止。
GOP越大,编码的I帧就会越少,相比而言,P帧、B帧的压缩率更高,因此整个视频的编码效率就会越高。
但是GOP也不是越大越好,太大的GOP最终可能导致参考帧丢失而出现解码错误,从而引起长时间花屏和卡顿。
所以,GOP不是越大越好,也不是越小越好,需要根据实际的场景来选择。
Slice
Slice也叫做“片”,图像的层次结构就是一帧图像可以划分成一个或多个Slice,而一个Slice包含多个宏块,且一个宏块又可以划分成多个不同尺寸的子块。如下图所示:
H264的码流结构
码流格式
H264码流有两种格式:一种是Annexb格式,一种是MP4格式
- Annexb格式:
- Annexb格式使用起始码来表示一个编码的开始,起始码有两种,一种是4字节的“00 00 00 01”,一种是3字节的“00 00 01”。由于图像的编码数据中有可能会存在和起始码一样的数据,所以为了防止出现这种情况,H264会将图像编码数据中的这几种字节串做特殊处理:
- “00 00 00”修改为“00 00 03 00”
- “00 00 01”修改为“00 00 03 01”
- “00 00 02”修改为“00 00 03 02”
- “00 00 03”修改为“00 00 03 03”
- 解码端,去掉起始码后,需要将对应的字节串转换回来。
- Annexb格式使用起始码来表示一个编码的开始,起始码有两种,一种是4字节的“00 00 00 01”,一种是3字节的“00 00 01”。由于图像的编码数据中有可能会存在和起始码一样的数据,所以为了防止出现这种情况,H264会将图像编码数据中的这几种字节串做特殊处理:
- MP4格式:
- MP4格式没有起始码,而是在图像编码数据的开始使用4字节作为长度标识,用来表示编码数据的长度。读取时,只需要先读取4字节编码数据长度信息,再按长度信息取出编码数据,再继续读取4字节长度,一直继续下去就可以取出所有的编码数据了。
- MP4格式图示
NALU
H264为了将图像数据和编码参数数据分离开,设计了两个重要的参数集:
- SPS(序列参数集):主要包含图像的宽、高、YUV格式和位深等基本信息。
- PPS(图像参数集):主要包含熵编码类型、基础QP和最大参考帧数量等基本编码信息。
所以真正的H264码流主要是由:SPS、PPS、I Slice、P Slice和B Slice组成的,如下图所示:
所以为了能够在码流中区分这几种不同的数据,H264设计了NALU(网络抽象层单元)。SPS是一个NALU,PPS是一个NALU,每一个Slice也是一个NALU。
每个NALU是由:NALU Header+NALU Data组成的,而对于每一个Slice NALU,其中的NALU Data又是由:Slice Header+Slice Data组成的,Slice Data又是由一个个MB Data(图像编码数据宏块)组成。其结构如下:
- NALU Header
- 总共占一个字节,具体组成如下:
- F:forbidden_zero_bit,占1bit,禁止位,H264码流必须为0;
- NRI:nal_unit_type,占5bit,表示NALU类型,取值如下所示:
需要注意的是,NALU类型只区分了IDR Slice和非IDR Slice,至于非IDR Slice 是普通的I Slice、P Slice还是B Slice,则需要继续解析Slice Header中的Slice Type字段; - 下面是一个实际的H264码流的例子,方便理解:
- 总共占一个字节,具体组成如下:
- 常见工程问题
- 多Slice时如何判断哪几个Slice是同一帧的呢?
- 在Slice Header中有一个first_mb_in_slice的字段,这个字段为0,则表示这是一帧中的第一个Slice,不为0则表示是一帧的其他Slice。
- 其中first_mb_in_slice是以无符号指数哥伦布编码的,需要对应的解码方式才能解码出来,但是可以通过其他方法判断此字段是否为0:
if((slice_header[0] & 0x80) == 1)则代表first_mb_in_slice为0 - 所以我们可以一直往后面的Slice Header中找这个字段,直到其变为0,则代表这是下一帧的Slice了,则前一个Slice就是前一帧的最后一个Slice了。
- 如何从SPS中获取图像的宽高
- 在SPS中有几个字段用来表示分辨率的大小,我们可以解码出这几个字段并通过一定的规则计算得到分辨率的大小。这几个字段分别是:
- 这几个字段都是通过无符号指数哥伦布编码的,需要先解码出来。解码得到具体值后,通过计算就可以得到分辨率了。注意:pic_height_in_map_units_minus1需要考虑帧编码和场编码的区别,其中场编码已经很少使用了,这里不再考虑。下面是计算方法:
- 在SPS中有几个字段用来表示分辨率的大小,我们可以解码出这几个字段并通过一定的规则计算得到分辨率的大小。这几个字段分别是:
- 如何计算得到QP值?
- 在对DCT变换进行量化的过程是引入失真最主要的环节,而量化最主要的参数就是QP值,并且QP值的大小严重影响到编码画面的清晰度,所以QP值非常重要
- 关于H264中的QP值,有三个不同的QP值:
- PPS中的全局基础QP:字段是pic_init_qp_minus26,当前序列中的所有依赖该PPS的Slice共用这个基础的QP。
- Slice Header中的QP:在Slice Header中有一个字段slice_qp_delta来表示在全局基础QP上调整的偏移量
- 宏块QP偏移量:H264允许宏块级别对QP做更进一步的精细化调节,这个字段在宏块数据里,叫做mb_qp_delta。
- 计算获取QP值:
- 多Slice时如何判断哪几个Slice是同一帧的呢?