ROI (region of interest) encoding是一项基于感兴趣区域的视频编码技术,对图像中感兴趣的区域减少量化参数值(qp:quantization parameter),从而分配更多码率以提升画面质量,而对不感兴趣的区域则增加量化参数值(qp),从而分配更少码率(这部分区域的画面质量会因此有所下降),这样,在不损失图像整体质量的前提下,可以节省网络带宽占用和视频存储空间,或者,在不增加网络带宽占用和存储空间的前提下,可以提高视频的整体质量。这在监控、窄带高清等领域都有较大的应用。
ROI encoding并不是一个新技术,诸如libx264、libvpx等软件编码器早已经提供了相应的支持,基于Intel GPU的libva也早已实现了相应接口。但是,每个编码器提供的接口都不一样,这给开发者带来了一定的麻烦。所以,建立在这些编码器之上的FFmpeg,完全可以定义一个统一的接口,然后,在FFmpeg的内部,将这个统一接口的参数翻译为相应编码器的API调用。我在2019年为FFmpeg增加了这样的接口,以支持ROI encoding,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agjVmHw3-1587431473547)(https://graph.baidu.com/resource/2220b9367743e59b0732301582037105.png)]
此接口的关键数据结构是:
typedef struct AVRegionOfInterest {
uint32_t self_size;
int top;
int bottom;
int left;
int right;
AVRational qoffset;
} AVRegionOfInterest;
其中,self_size必须等于sizeof(AVRegionOfInterest),为了以后万一struct AVRegionOfInterest需要增加数据成员的时候,新老版本兼容用。top、bottom、left和right的单位是像素,以图像左上角为坐标原点,定义一个矩形像素区域,根据当前所用编码器的要求,此区域可能会被对齐处理。qoffset则是对量化参数值(qp)在此矩形区域中的调整,qoffset的取值范围是[-1.0, 1.0],在传递给编码器的时候,还会先乘以当前编码器qp的取值范围,使得它和qp在数值上具有可加性,最后的加法发生在编码器内部。如果qoffset的值是0,则表示不改变qp值。如果qoffset是正数,将使得最终qp值变大,从而使得图像质量变差;如果qoffset是1.0,则按最差质量编码。如果qoffset是负数,将使得qp值减小,从而图像质量变好;如果qoffset为-1.0,这个区域内的图像将按编码器的最佳图像质量编码。之所以用AVRational而不是float来定义qoffset,是因为IEEE float无法精确表示小数值,不利于跨平台一致性。
可以支持多个这样的roi区域,每个区域的qoffset值可以都不相同,多个矩形区域也可以组成复杂形状。在FFmpeg中用 struct AVRegionOfInterest的数组来表示多个区域,有些编码器对区域个数有限制,此时,假如存在区域重叠的情况,那么,重叠区域的qoffset采用更小数组索引的qoffset值,换句话说,数组中应先放高优先级的roi数据,再放低优先级的roi数据。这个数组最后被放到struct AVFrame
的AVFrameSideData **side_data
中,其type为AV_FRAME_DATA_REGIONS_OF_INTEREST。所以,对于使用FFmpeg API的开发者来说,要用上roi encoding功能,只需设置好AVFrame中的side_data即可。下面,分两种情况介绍如何设置。
这种情况下,每个frame中的roi区域都可能会发生变化,包括区域个数、区域位置和大小、还有qoffset的值。所以,每次都需要重新申请side_data空间,并且填好数值。主要代码如下所示,省略了错误返回值的处理。
while () {
// 首先解码得到AVFrame
AVFrame *frame = decode();
// 申请side_data空间,假设有2个区域。
// 不需要显式释放申请到的空间,
// 在AVFrame的清理函数中会自动处理。
AVFrameSideData *sd =
av_frame_new_side_data(frame,
AV_FRAME_DATA_REGIONS_OF_INTEREST,
2*sizeof(AVRegionOfInterest));
// 获取刚申请到的内存地址
AVRegionOfInterest* roi =
(AVRegionOfInterest*)sd->data;
// 设置第一个区域
roi[0].self_size = sizeof(*roi);
roi[0].top = ...;
roi[0].bottom = ...;
roi[0].left = ...;
roi[0].right = ...;
roi[0].qoffset = ...;
// 设置第二个区域
roi[1].self_size = sizeof(*roi);
roi[1].top = ...;
roi[1].bottom = ...;
roi[1].left = ...;
roi[1].right = ...;
roi[1].qoffset = ...;
// 然后调用视频编码器进行编码
encode(frame);
}
这种情况下,因为roi区域不变,所以,我们没有必要每次都重新申请side_data并且填入数值,只要在一开始的时候准备好,后续继续使用即可,可以用下面的代码来实现。
// 首先准备好存放roi数据的buffer,假设只有1个roi区域
AVBufferRef *roi_buf_ref =
av_buffer_alloc(sizeof(AVRegionOfInterest));
// 获取刚申请到的内存地址
AVRegionOfInterest* roi =
(AVRegionOfInterest*)roi_buf_ref->data;
// 填入数值
*roi = (AVRegionOfInterest) {
.self_size = sizeof(*roi),
.top = ...,
.bottom = ...,
.left = ...,
.right = ...,
.qoffset = ...,
};
while () {
// 首先解码得到AVFrame
AVFrame *frame = decode();
// 将之前准备好的roi数据(指针)放入frame的side_data中
AVBufferRef *ref = av_buffer_ref(roi_buf_ref);
av_frame_new_side_data_from_buf(frame,
AV_FRAME_DATA_REGIONS_OF_INTEREST,
ref);
// 然后调用视频编码器进行编码
encode(frame);
}
// 最后,释放一开始申请的内存
av_buffer_unref(&roi_buf_ref);
如果您对FFmpeg中ROI encoding有其它需求的话,欢迎讨论,谢谢!
以上内容是本人业余时间兴趣之作,限于水平,差错难免,仅代表个人观点,和本人任职公司无关。
本文首发于微信公众号:那遁去的一