神奇的H.264算法
很开眼的一篇文章:https://sidbala.com/h-264-is-magic/
本文是它的译文,不会逐字翻译,只挑有趣或重要的部分。
H.264是视频压缩编解码器标准,它的目标只有一个:减少传输视频所需的宽带。网络视频、蓝光视频、手机、无人机等等,都会使用H.264,可以说,它无处不在。
为何需要压缩
视频由帧构成,而帧其实就是图片,而图片其实就是一个三维矩阵(用opencv2读取一张图片,打印一下便知道其结构)。
一个 60fps 的视频,如果未压缩,那么它每一秒的大小为:,即一个50GB的蓝光光盘只能存2分钟左右的视频,这几乎是不可处理的。
所以,我们需要压缩。
为什么选择H.264
上图是我从苹果官网截图获得的图片,基于这个图片,我制作了2个文件:
苹果官网主页截图PNG文件,大小为1015KB 基于同一张苹果官网主页截图,利用H.264制作的时长5秒,帧率60fps的视频文件,大小为175KB
没错,5秒的视频文件体积更小。
视频每秒60fps,总共5秒,即有300帧(300个图片矩阵),即视频比PNG图片多存在了300倍的数据,但文件大小是PNG图片的五分之一。
怎么做到的?H.264会将视频中不重要的数据全部丢掉,只留下重要的数据,即H.264是一种有损数据压缩算法,而PNG是一种无损压缩算法。
那H.264怎么判断哪些数据是重要的?哪些是不重要的?嗯,这便是H.264的关键。
H.264实际怎么做的
左边是原图,右边是有损压缩后的图片,观察右边图片,苹果笔记本的音响孔,这些孔在原图(左边)是清晰的,而有损压缩后,就模糊了。这图,经过有损压缩后,大小变为原本的7%,而类似音响孔这种影响,肉眼要仔细观察,才能看出差异。
频域(Frequency Domain)
在信息论中,熵用于量化一段内容所含的信息量。
很多时候,内容表现形式不同,但信息量却一样,即熵没有变化,一个典型的例子,我们可以使用二进制来表示一段数据,也可以实验十六进制来表示同一段数据,虽然二进制与十六进制形式不同,将两者的熵没有改变,我们可以将任何数据的这种形式转换称为完美的无损转换。
没错,图像这类数据也可以进行无损转换。
想象一下,你可以将任何随空间或时间变化的数据集(比如图像的亮度值)都转为不同的坐标空间,假设现在我们有频率坐标系(不是x-y坐标系),现在FreqX与FreqY是坐标轴,我们可以将图像无损的转换为频率坐标系中,即将图像换了一种表示形式且熵没有改变。
如下图,我们将苹果笔记本图片转为频率坐标的表示
看到频率坐标系中的内容,其中高亮的点是图像中具有高信息含量的数据,我们可以基于频率坐标的展示,看出图像中重要的部分与不太重要的部分,将不太重要的部分丢弃,则实现图像的有损压缩,如下图所示:
从图中可知,我们丢弃频率坐标中边缘信息越多,图像就越小,从而实现了图像的压缩。
H.264利用了这个技巧来实现视频中帧图像的压缩,但这还不够。
色度抽样
基于科学研究发现,人类眼睛相应的脑区不太擅长分辨颜色的细节,我们对亮度变化敏感,但对颜色的变化不敏感,如果我们丢弃图像中的部分颜色,我们是感受不出变化的,所以我们需要一些方法来合理的丢弃图像中的部分颜色信息,以此进一步压缩图像。
在电视信号中,R+G+B颜色数据被无损转为Y+Cb+Cr的形式来展示数据,其中Y表示亮度(本质是黑白亮度),Cb和Cr的颜色成分,RGB转YCbCr,其信息熵是没有变化的。
为啥电视不直接使用RGB,而转一层,使用YCbCr呢?其实是历史原因。
早期只有黑白电视,我们只需要使用Y信号便可完成数据的传输,随后,彩色电视问世,进入彩色电视与黑白电视共存的年代,如果彩色电视使用RGB,那么就需要弄2个独立的数据流,这很麻烦(成本、维护上都麻烦)。
聪明的工程师决定将颜色信息编码进Cb和Cr中,并将其与Y信息一同传输,这样黑白电视只能看Y信息,而彩色电视内部将YCbCr转为RGB显示则可。
因为人眼对亮度变化敏感,但对颜色变化不敏感,所以我们可以将Cb、Cr压缩,从而将图像大小再减少一半,而人眼却看不出差别。
运动估计与补偿
前面几种压缩方式都是针对帧内的,即针对图像的,而视频还有另外一个大的压缩空间,那便是帧间信息(帧与帧之间)。
H.264采用运动估计与补偿的方式来压缩帧间信息,从而极大减小视频的大小。
首先,我们知道,视频由一系列帧按顺序排列而成,而这些帧,有很大一部分信息是冗余的,比如我们拍摄30 fps的视频,那么抽出视频中的1秒,可以发现有30张图像,这30张图像,内容通常是相近的,是否有办法将这些冗余的信息丢弃掉来减少视频大小呢?
当然有,运动估计与补偿便是解决这个问题的一种方案,为了理解,我从其他文章中(译文之外的文章)选了一个例子。
现在有一个视频,记录了台球的运动。
一个视频,展开后,就是一系列的帧:
有了视频后,第一步,要对视频中的相似帧分组,那怎么判断某些帧相似呢?
H.264编码器会按顺序逐次抽取两个相邻帧,然后按宏块进行对比,计算两帧相似度。
这里涉及到宏块这个新概念,什么是宏块?其实就是H.264处理图像时的窗口大小,比如有一张图:
H.264默认会按大小划分出一个宏块,当然,你也可以按、等大小来划分。
H.264编码器通过宏块扫描与宏块搜索来判断两个帧之间的相似度,然后将相似的帧分为一组。
对同一组帧,会做运动估计与补偿。
首先,拿出同一组的相邻两帧。通过宏块扫描,发现图像中有物体,便在另一帧图像相同位置的周围进行搜索,如果在另外一帧的图像中,也找到该物体,则可以计算出物体的运行矢量。
如上图,计算出相邻两帧图像中台球的相差位置,计算出台球的运行的方向和距离。
H.264编码器依次把每一帧中球移动的距离和方向都记录下来,如下图:
计算出运动矢量后,将相同部分的数据丢弃,剩余的数据便是补偿数据,对于这一组帧,我们只需要存储完整的第一帧数据(称为I帧)和补偿数据便可以还原出完整原始数据了,通过这种做法,帧间大量冗余数据被压缩。
运动估计与补偿算法压缩了视频大小,但也会带来一些小问题,比如:
当我们在视频网站上浏览视频时,如果错过了一段内容,想往回点击,再次播放时,网站通常会停顿几秒。这些内容刚刚已经缓存下来了,只是我没看,想倒回去看,为何还会停顿,直接读缓存信息不就行了?
当你跳转视频到某个任意帧时,H.264的解码器必须重新做所有的计算,从而得到运动矢量和补偿数据,并将这些数据加到你当前帧中。这个计算压力是比较大的,所以会停顿一下,从而影响你的体验。
熵编码器(Entory Coder)
刚刚的描述中,我们简化了获取运动矢量和补偿数据的过程,实际上,H.264编码器将帧分组后,会将组中的第一帧作为I帧,然后使用两种方式去获取运动矢量,一种是P帧,一种是B帧。
P帧只会与前一帧进行对比来获得数据,而B帧会对前后的帧都进行对比来获得数据。
在实际的视频中,帧变化时,很可能会出现几个宏块扫描对比得出的运动矢量是相同的情况,这便造成了数据冗余。
熵编码器会处理这种数据冗余的情况,这是一种无损转换,不会损失数据,但这种转换减少了存储相同数据所需要的空间,从而减小视频大小。
结尾
如果原始视频的分辨率是,时长5秒,每秒60帧,那么原始视频的大小为:,而压缩后,视频会变为175kb,真让人惊叹,真的就是魔法。
H.264有几十年的研究历史,本文只是简单化的描述了其中的工作,但有很多细节并没有展示,H.264也是经过多年发展,慢慢优化成当前形态的。
我是二两,我们下篇文章见。
本文参考:
[H.264 is Magic] https://sidbala.com/h-264-is-magic/ [视频压缩原理] https://github.com/733gh/Android-Notes/blob/master/android/%E8%A7%86%E9%A2%91%E5%8E%8B%E7%BC%A9%E5%8E%9F%E7%90%86.md
扫码即可加我微信
--End-- 1、想领取赠书,加我微信,朋友圈不定期送书;
2、想咨询学习,加我微信,每次咨询仅9.9元;
3、更多需求(学习 代码 视频剪辑),都可以加我微信,欢迎咨询。
扫码即可加我微信
分享
收藏
点赞
在看