点击上方“LiveVideoStack”关注我们

▲扫描图中二维码或点击阅读原文

了解音视频技术大会更多信息


作者:王伟

编辑:Alex

  引 言  

我们有一个平台来周期性地对线上的直播流数据进行某些检测,例如黑/白屏检测、静态画面检测……在检测中,我们会根据提取到的直播流的帧率来预估要计算的帧数量,例如,如果要检测5s的直播流,而该直播流的帧率为20fps,需要计算的帧数量则为100。忽然有一天,我们发现,平台开始大面积的超时,之前只需要2s就能完成的计算,现在却需要30+分钟。查了之后,我们发现,之所以计算超时是因为OpenCV计算的帧率为2000,从而导致需要计算的帧数量从之前的100变为了10000,进而引起了计算超时。

1

OpenCV 如何计算帧率

这个问题的具体描述可以参见 OpenCV Issues 21006[1]。该问题的模拟直播流片段test.ts可以点击链接下载:

https://pan.baidu.com/share/init?surl=RY0Zk5C_DOEwTXYe2SLFEg,下载提取码为x87m。

如果用如下的代码获取test.ts的fps,

const double FPS = cap.get(cv::CAP_PROP_FPS);
std::cout << "fps: " << FPS << std::endl;

可以得到:

$ fps: 2000

用ffprobe对视频进行分析,可以得到:

codec_name=h264
r_frame_rate=30/1
avg_frame_rate=0/0
……

从 opencv/modules/videoio/src/cap_ffmpeg_impl.hpp[2]中,我们发现fps由CvCapture_FFMPEG::get计算而来,其计算逻辑如下:

double fps = r2d(ic->streams[video_stream]->avg_frame_rate);
if (fps < eps_zero) {fps = 1.0 / r2d(ic->streams[video_stream]->codec->time_base);
}

2

为什么OpenCV得到的帧率是错的

利用test_time_base.cpp[3],我们可以得到:

time_base: 1/2000
framerate: 0/0
avg_framerate: 0/0
r2d(ic->streams[video_stream]->avg_frame_rate) = 0

所以OpenCV采用了:

1.0 / r2d(ic->streams[video_stream]->codec->time_base)

来计算该视频的fps。而此处的time_base = 1/2000,因此,最终得到的fps是2000。

也就是说,AVStream->codec->time_base的值导致了OpenCV得到一个看起来是错误的fps。那么,AVStream->codec->time_base为什么是这个值呢?FFmpeg是怎么计算这个字段的呢?

3

FFmpeg 如何计算

AVCodecContext.time_base

AVStream->codec->time_baseAVCodecContext中定义的 time_base字段,根据libavcodec/avcodec.h[4] 中的定义可知,对于解码而言,time_base已经被废弃,需要使用framerate来替换 time_base。并且,对于固定帧率而言,time_base = 1/framerate,但并非总是如此。

利用H264Naked[5]对test.ts对应的H.264码流进行分析,我们得到SPS.Vui信息:

timing_info_present_flag :1
num_units_in_tick :1
time_scale :2000
fixed_frame_rate_flag :0

从中可以看到,test.ts是非固定帧率视频。从test_time_base.cpp[3]的结果看,test.ts视频中,framerate = 0/0,而time_base = 1/2000

难道,对于非固定帧率视频而言,time_baseframerate之间没有关联?如果存在关联,那又是怎样的运算才能产生这种结果?这个 time_base究竟是怎么计算的呢?究竟和framerate有没有关系呢?一连串的问题随之而来……

源码面前,了无秘密。接下来,带着这个问题,我们来一起分析一下FFmpeg究竟是如何处理time_base的。

3.1  avformat_find_stream_info

在 FFmpeg中,avformat_find_stream_info() 对ic->streams[video_stream]->codec进行初始化,因此我们可以从 avformat_find_stream_info() 开始分析。

从 libavformat/avformat.h[6]中,可以得知avformat_open_input()会打开视频流,从中读取相关的信息,然后存储在AVFormatContext中,但是有时候,此处获取的信息并不完整,因此需要调用avformat_find_stream_info()来获取更多的信息。

需要注意的是:

avformat_find_stream_info()会尝试通过解码部分视频帧来获取需要的信息。

/**
* Read packets of a media file to get stream information. This
* is useful for file formats with no headers such as MPEG. This
* function also computes the real framerate in case of MPEG-2 repeat
* frame mode.
* The logical file position is not changed by this function;
* examined packets may be buffered for later processing.
*
* @param ic media file handle
* @param options  If non-NULL, an ic.nb_streams long array of pointers to
*                 dictionaries, where i-th member contains options for
*                 codec corresponding to i-th stream.
*                 On return each dictionary will be filled with options that
*                 were not found.
* @return >=0 if OK, AVERROR_xxx on error
*
* @note this function isn't guaranteed to open all the codecs, so
*       options being non-empty at return is a perfectly normal behavior.
*
* @todo Let the user decide somehow what information is needed so that
*       we do not waste time getting stuff the user does not need.
*/
int avformat_find_stream_info(AVFormatContext*ic, AVDictionary **options);

avformat_find_stream_info()的整体逻辑大致如下图所示,其中特别需要关注图中所示的 7 个步骤:

3.2  avformat_find_stream_info()的重要步骤说明

 STEP 1  设置线程数,避免H.264多线程解码时没有把SPS/PPS信息提取到extradata

 STEP 2  设置AVStream *stst会在后续的函数调用中一直透传到 try_decode_frame()

 STEP 3  比较简单,这里不再赘述。

 STEP 4  设置AVCodecContext *avctx为透传的st->internal->avctx,在后续的解码函数调用中,一直透传的就是这个avctx,因此,从这里开始的执行流程,FFmpeg使用的全部都是st->internal->avctx,而不是st->codec,这里要特别的注意。此处同时会设置解码的线程数,其目的和STEP 1是一致的。

 STEP 5  因为之前设置了解码线程数为1,所以此处会调用

ret = avctx->codec->decode(avctx, frame, &got_frame, pkt)

来解码并计算avctx->framerate。注意,此处的avctx实际上是透传而来的st->internal->avctx。计算 framerate的逻辑会在如何计算framerate部分介绍。

 STEP 6  根据解码器得到的framerate信息来计算 avctx->time_base,注意此处实际上是st->internal->avctx->time_base。根据 如何计算framerate可知,此处framerate = {1000, 1}。根据 AVCodecContext.ticks_per_frame的介绍可知,ticks_per_frame = 2。因此,此处avctx->time_base = {1, 2000}

avctx->time_base = av_inv_q(av_mul_q({1000, 1}, {2, 1})) = {1, 2000}

 STEP 7  这一步可谓是“瞒天过海,明修栈道暗度陈仓”。这一步为了解决API的前向兼容,做了一个替换,把st->internal->avctx->time_base 赋值给了st->codec->time_base,而把st->avg_frame_rate 赋值给了 st->codec->framerate。因此:

st->codec->time_base = {1, 2000}
st->codec->framerate = {0, 0}

st->codec->time_base 的计算和 st->codec->framerate 之间没有任何关系,而是和 st->internal->avctx->framerate 有关。究其本质,是和sps.time_scale,sps.num_units_in_tick有关。

st->internal->avctx->time_base.num =
sps->num_units_in_tick *
st->internal->avctx->ticks_per_framest->internal->avctx->time_base.den = sps->time_scale * st->internal->avctx->ticks_per_frame;st->internal->avctx->time_base = {sps->num_units_in_tick,
sps->time_scale}

3.3  internal->avctx->time_base & internal->framerate

所以实际上,internal->avctx->time_base为:

avctx->time_base = sps->num_units_in_tick /
sps->time_scale

internal->avctx->framerate则是:

avctx->framerate = sps->time_scale /
(sps->num_units_in_tick * avctx->ticks_per_frame)

因此,对于 H.264 码流而言,time_base = 1 / (2 * framerate),而不是1 / framerate

这也就是为什么

libavcodec/avcodec.h[4] 中说:

* This often, but not always is the inverse of the frame rate or field rate
* for video.

从如上的分析可以知道:

avctx->framerate = 1 / (avctx->time_base * avctx->ticks_per_frame)

因此,当st->avg_frame_rate = 0 时,OpenCV计算fps的逻辑是错误的。

在H.265中,ticks_per_frame = 1,因此对于H.265的编码,OpenCV是没有这个问题的。可以使用Zond 265 [7]工具来分析一个 H.265的视频码流,然后对照OpenCV以及FFmpeg的结果来验证。

同时,正是如上所示的STEP 7中的移花接木导致了 test_time_base.cpp[3] 的结果:

st->codec->framerate: 0/0
st->codec->time_base: 1/2000

3.4 ff_h264_decoder

libavcodec/decode.c [8]中的

decode_simple_internal()会调用对应的解码器来进行解码(STEP 5)。而正如前所示,test.ts为H.264 编码的视频流,因此此处会调用 H.264 解码器来进行解码。在FFmpeg中,H.264解码器位于 libavcodec/h264dec.c[9] 中定义的

const AVCodec ff_h264_decoder

const AVCodec ff_h264_decoder = {.name                  = "h264",.type                  = AVMEDIA_TYPE_VIDEO,.id                    = AV_CODEC_ID_H264,.priv_data_size        = sizeof(H264Context),.init                  = h264_decode_init,.close                 = h264_decode_end,.decode                = h264_decode_frame,......
};

在上文图中的STEP 5中,

ret = avctx->codec->decode(avctx, frame, &got_frame, pkt);

实际调用的就是:

ff_h264_decoder->h264_decode_frame(avctx, frame, &got_frame, pkt);

而此处的avctx也就是

try_decode_frame()中透传下来的st->internal->avctx,即上文图中的STEP 4。

3.5  h264_decode_frame

h264_decode_frame()的整体逻辑如下图所示:

3.6  AVCodecContext.ticks_per_frame

后面会用到ticks_per_frame来计算framerate。在STEP 6中计算 time_base的时候也用到了该值。因此,有必要做一下特殊说明。在H.264解码器中,ticks_per_frame=2,其具体的取值可以从如下几处得知:

  • libavcodec/avcodec.h [4]中的字段说明:

/**
* For some codecs, the time base is closer to the field rate than the frame rate.
* Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
* if no telecine is used ...
*
* Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.*/int ticks_per_frame;
  • libavcodec/h264dec.c [9]中的 h264_decode_init()

avctx->ticks_per_frame = 2;

4

如何计算framerate

 STEP 1   根据整体的计算流程可知,此处的h实际上就是

avformat_find_stream_info() 中的

st->internal->avctx->priv_datah会一直透传到之后的所有流程,这个务必要注意。

 STEP 2   此处会首先获取到sps的相关信息,以备后续的计算使用,我们可以再次看一下test.ts sps[10] 的相关信息。

timing_info_present_flag :1
num_units_in_tick :1
time_scale :2000
fixed_frame_rate_flag :0

 STEP 3  根据sps的相关信息计算framerate,在上文的STEP 6中计算 time_base用到的framerate就是在此处计算的。因为 timing_info_present_flag = 1,因此会执行计算framerate的逻辑:

avctx->framerate.den = sps->num_units_in_tick * h->avctx->ticks_per_frame = 1 * 2 = 2
avctx->framerate.num = sps->time_scale = 2000
avctx->framerate = (AVRational){1000, 1}

因此,

st->internal->avctx->framerate = {1000, 1}

但是,因为avctx->time_base={1,2000},所以OpenCV计算出来的帧率结果为2000。导致这种不一致的原因在于,OpenCV在使用codec->time_base计算帧率的时候没有考虑ticks_per_frame。因此,对于OpenCV而言,正确的计算帧率的方式应该为:

double fps = r2d(ic->streams[video_stream]->avg_frame_rate);
if (fps < eps_zero) {
fps = 1.0 / r2d(ic->streams[video_stream]->codec->time_base * ic->streams[video_stream]->codec->ticks_per_frame);
}

  结 论  

通过上面的分析我们可以知道:

  • FFmpeg在计算 AVCodecContex 中的frameratetime_base的时候,会用到:

    o sps.time_scale

    o sps.num_units_in_tick

    o AVCodecContex.ticks_per_frame

  • 在 FFmpeg 中,frameratetime_base的关系为:

    o framerate = 1 / (time_base * ticks_per_frame)

    o time_base = 1 / (framerate * ticks_per_frame)

  • 对于非 H.264/MPEG-2,

    ticks_per_frame=1,因此frameratetime_base是互为倒数的关系。而对于H.264/MPEG-2 而言,ticks_per_frame=2,因此,此时二者并非是互为倒数的关系。因而,FFmpeg 中才说,frameratetime_base通常是互为倒数的关系,但并非总是如此。

  • 在OpenCV中,对于H.264/MPEG-2视频而言,当

    AVStream.avg_frame_rate=0时,其计算fps的逻辑存在BUG。

  • 因为在解码时,

    AVCodecContex.time_base已经废弃,同时 AVStream.avctx也已经废弃,而

    avformat_find_stream_info() 中为了兼容老的API,因此会利用 AVStream.internal.avctx和其他的信息来设置AVStream.avctx。而AVStream.avctx.time_base取自AVStream.internal.avctxAVStream.avctx.framerate 则取自 AVStream.framerate

注释:

[1] https://github.com/opencv/opencv/issues/21006

[2] https://github.com/opencv/opencv/blob/4.x/modules/videoio/src/cap_FFmpeg_impl.hpp

[3] https://github.com/wangwei1237/wangwei1237.github.io_src/blob/master/source/_posts/Why-OpenCV-Get-the-Wrong-FPS/test_time_base.cpp

[4]https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.h

[5] https://github.com/shi-yan/H264Naked

[6] https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/avformat.h

[7] https://www.dektec.com/products/applications/Zond/

[8]https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/decode.c

[9]https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/h264dec.c

[10] https://wangwei1237.github.io/2021/11/26/Why-OpenCV-Get-the-Wrong-FPS/#sps

作者简介:

王伟,17哥,百度资深测试工程师。百度视频质量评测技术负责人,在解决视频质量评测的标准化、置信度方面有丰富的实践经验,搭建了百度首个体系化的视频质量评测服务平台,并服务于多个视频业务。


喜欢我们的内容就点个“在看”吧!

为什么OpenCV计算的帧率是错误的?相关推荐

  1. 使用Python、OpenCV计算轮廓的中心

    1. 使用Python.OpenCV计算轮廓的中心并标记 2. 使用Python.OpenCV检测轮廓的形状并标记 3. 使用颜色通道统计信息来标记形状的实际颜色并标记 本博客的目标:(1)检测图像中 ...

  2. 使用Python,OpenCV计算图像直方图(cv2.calcHist)

    使用Python,OpenCV计算图像直方图(cv2.calcHist 1. 效果图 2. 原理 2.1 什么是图像直方图? 2.2 计算直方图 2.3 可视化蒙版区域 3. 源码 参考 这篇博客将介 ...

  3. OpenCV计算时刻calculate moments的实例(附完整代码)

    OpenCV计算时刻calculate moments的实例 OpenCV计算时刻calculate moments的实例 OpenCV计算时刻calculate moments的实例 #includ ...

  4. OpenCV计算均值和方差

    OpenCV 计算均值和标准方差: 计算矩阵均值,src是1-4个通道,返回Scalar,保存了1-4通道的均值,结果值位double,Scalar[0],Scalar[1],Scalar[2],Sc ...

  5. OpenCV - 计算相机和视频的帧速率FPS

    原文:OpenCV - 计算相机和视频的帧速率FPS[译] - AIUAI 原文:How to find frame rate or frames per second (fps) in OpenCV ...

  6. opencv计算指定区域黑白像素占比

    如下在我们拿到二值化的图像之后,想要计算红框内黑白像素在该区域的占比.注意单通道的图像无法出现黑白以外的颜色. 上图为我们拿到的红色区域二值化之后的图像.我们通过遍历该区域所有像素来判断.若值为255 ...

  7. python图片相似度计算_python Opencv计算图像相似度过程解析

    这篇文章主要介绍了python Opencv计算图像相似度过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.相关概念 一般我们人区分谁是谁 ...

  8. 智力题论证(有一道选择题,ABC三个选项,一个正确答案, 假设你选择了A,但是通过计算知道C是错误答案,问B是正确答案的概率是多少)

    question 有一道选择题,ABC三个选项,一个正确答案, 假设你选择了A,但是通过计算知道C是错误答案,问B是正确答案的概率是多少. 这道题有人认为A应该和B,C分开, 利用部分整体思想, 此时 ...

  9. 基本矩阵 双目 matlab,MATLAB双目标定与OpenCV计算矫正参数矩阵

    Intrinsic parameters of left camera: Focal Length: fc_left = [ 320.53138 321.62046 ] ?[ 1.32444 1.28 ...

最新文章

  1. 成功解决Instructions for updating:  Use `tf.global_variables_initializer` instead.
  2. GDCM:gdcm::FileChangeTransferSyntax的测试程序
  3. Opportunity text creation tool
  4. RocketMQ中主从复制
  5. 《OpenCL异构计算》新版中译本派送中!
  6. activity 变成后台进程后被杀死_Android 后台运行白名单,优雅实现保活
  7. 【系统】CentOS、Ubuntu、Debian三个linux比较异同
  8. 安装、配置文件-配置 CACTI 监控 MySQL 数据库状态-by小雨
  9. java第一季_Java入门第一季
  10. pandas创建series三种方法
  11. 计算机编程语言分类与区别
  12. 简单动画(过渡)与复杂动画
  13. 等级保护第三级安全扩展要求笔记(2019)
  14. 中兴盒子B860AV2.1-A-B-M-U,通刷,线刷刷机固件
  15. Jmeter教程(3) - - Badboy的安装及脚本录制
  16. 推荐一个python库:Vaex
  17. 华为防火墙黑白名单网址过滤设置
  18. 关于“书是人类进步的阶梯”的一点点看法
  19. 工薪阶层买房八大实用秘笈
  20. 推荐两个搜索本地文件和文档内容的小工具

热门文章

  1. Python语言练习
  2. 鲍尔默:我可能说过Linux是“恶性肿瘤” 但现在我爱它
  3. 如何在cmd中运行PHP
  4. Doxygen 配置
  5. Win10 如何禁用BitLocker加密cmd命令
  6. 用jmeter压测tcp
  7. c语言数据错误 英语怎么说,C语言程序设计 教小学生英语 里面有代码运行错误,怎么改...
  8. 聚合签名、门限签名、Multisigs 和多签名
  9. 微观经济学知识点(七)
  10. C#中的递归APS和CPS模式详解(转载)