H264是一种常用的图像编码格式。
视频图像每秒有几十帧,未压缩的情况下,每帧都有好几M,一部1小时的影片更是多达几十上百G。
对视频进行编码的主要目的就是为了压缩视频存储大小。
视频图像在时间、空间上存在冗余,这一冗余是正是各类编码的压缩依据。
下面将从初学者的角度介绍如何去学习一个完全陌生的编码格式的解析。
了解各种概念
当阅读官方文档,或是网络资料的时候,不免被排山倒海的概念淹没。
建议静下心先过一遍一些数学和信号处理的基础知识,比如什么是熵,什么是算术编码,什么是时域频域,什么是数字图像等等;推荐这位作者的H264系列文章:https://www.jianshu.com/p/9c4f51d4c3ff
然后过一遍H264中的常见术语,如NAL,VCL,Slice,Macroblock等,IDR/I/P/B,图像/场/帧等;推荐这篇文章:
https://blog.csdn.net/andywang201001/article/details/80274886
理解语法
H264官方文档(我看的是《T-REC-H.264-200503》)以表格形式定义了解析语法(在7.1节介绍了它的语法含义)
不过直接看英文的官方文档可能会有点受挫,建议先找网上写的比较全的格式解析看看,比如这篇:http://mamicode.com/info-detail-1215990.html
因为网络资料的作者一般有自己的专注领域,写的文章会有侧重,此时,就需要我们结合自己的工作内容看官方文档深入研究了。
动手实践
虽然我们有很多成熟的开源项目支持H264的解析,如官方的JM,如VLC的x264,但是如果要观察和理解每个字段的含义、变化规律,从最细粒度剖析一个视频文件,还是能自己Coding解析的比较好(何况造轮子是程序员的快乐呢!)
在自己动手”造轮子“的过程中,我突然有一个想法,是不是可以做这样一个工具:
- 对于任意的(编码)格式,可以按官方文档的语法写几乎逐行对应的代码,完成一个解析插件
- 这个工具可以同时展示二进制的数据、插件解析的结果、各个字段对应的解释文档
- 还能对于多次解析的结果建立透视图,查看多次解析过程某个字段是如何变化的
于是我造了一个用于造轮子的工具:
bitinsightgithub.com比如:
我们学到H264的nal_unit
中有一个nal_unit_type
表示的当前nalu
的类型,它的值指示了这个unit中的数据是什么。
有了工具,就可以根据表格逐行翻译:
def nal_unit(d:Table, bs:BitStream):
d.add_field('forbidden_zero_bit', bs.read_bits,
count=1)
d.add_field('nal_ref_idc', bs.read_bits, count=2)
d.add_field('nal_unit_type', bs.read_bits, count=5)
把文档中关于nal_unit_type
的解释摘抄下来,运行工具:
就可以很有成就感地完成了nal_unit_type的解析了,而且还可以实时对照文档查看含义。(图中告诉我们0到6byte的范围是一个nal_unit_type为9的nalu,也就是一个Access unit delimeter
)
当然,如果只是造造轮子,还不尽兴。
在学习slice_header
中的frame_num
的时候有没有被绕晕?那不妨实际拿个文件,看看nal_unit_type
、slice_type
和frame_num
的关系吧:
我们再写个slice_header
的解析代码:
def slice_header(d:Table, bs:BitStream):
d.add_field('first_mb_in_slice', bs.read_ue_golomb)
d.add_field('slice_type', bs.read_ue_golomb)
d.add_field('pic_parameter_set_id', bs.read_ue_golomb)
pps = __find_pps_in_ctx(d.pic_parameter_set_id, d.context)
if pps == None:
raise Exception('no pps in ctx, abort!')
sps = __find_sps_in_ctx(pps.seq_parameter_set_id, d.context)
if pps == None:
raise Exception('no sps in ctx, abort!')
if sps.get_value('separate_colour_plane_flag', 0):
d.add_field('colour_plane_id', bs.read_bits, count=2)
try:
nal_unit = d.context.nal_unit
except AttributeError as e:
raise Exception('no nal_unit in ctx, abort!')
d.add_field('frame_num', bs.read_bits, count = (sps.log2_max_frame_num_minus4 + 4))
比之前的nal_unit
略显复杂,因为我们需要引用sps/pps/nal_unit的header
.
接下来就可以借助工具对多个nalu创建一个透视图了:
是不是一目了然了?
不知不觉从方法论写出了工具安利,收尾~