ffmpeg中的roi encoding介绍

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 AVFrameAVFrameSideData **side_data中,其type为AV_FRAME_DATA_REGIONS_OF_INTEREST。所以,对于使用FFmpeg API的开发者来说,要用上roi encoding功能,只需设置好AVFrame中的side_data即可。下面,分两种情况介绍如何设置。

  • 可变区域的roi encoding

这种情况下,每个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 encoding

这种情况下,因为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有其它需求的话,欢迎讨论,谢谢!

以上内容是本人业余时间兴趣之作,限于水平,差错难免,仅代表个人观点,和本人任职公司无关。

本文首发于微信公众号:那遁去的一

你可能感兴趣的:(FFmpeg)