为什么 OpenCV 计算的视频 FPS 是错的

news2024/11/28 4:44:43

在这里插入图片描述

作者 | 王伟、刘一卓

导读

网络直播功能作为一项互联网基本能力已经越来越重要,手机中的直播功能也越来越完善,电商直播、新闻直播、娱乐直播等多种直播类型为用户提供了丰富的直播内容。

随着直播的普及,为用户提供极速、流畅的直播观看体验我们有一个平台来周期性的对线上的直播流数据进行某些检测,例如黑/白屏检测、静态画面检测……

在检测中,我们会根据提取到的直播流的帧率来预估要计算的帧数量,例如如果要检测 5s 的直播流,而该直播流的帧率为 20 fps,需要计算的帧数量则为 100。忽然有一天,我们发现,平台开始大面积的超时,之前只需要 2s 就能完成的计算,现在却需要 30+ 分钟。

查了之后,我们发现,之所以计算超时是因为 OpenCV 计算的帧率为 2000,从而导致需要计算的帧数量从之前的 100 变为了 10000,进而引起了计算超时。

全文9288字,预计阅读时间24分钟。

01 OpenCV 如何计算帧率

这个问题的具体描述可以参见 OpenCV Issues 21006。该问题的模拟直播流片段 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 对视频进行分析,

$ ffprobe -select_streams v -show_streams test.ts

可以得到:

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

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

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);
}

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

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

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 是怎么计算这个字段的呢?

03 FFMpeg 如何计算 AVCodecContext.time_base

AVStream->codec->time_baseAVCodecContext 中定义的 time_base 字段,根据 libavcodec/avcodec.h中的定义,该字段的解释如下:

/**
  * This is the fundamental unit of time (in seconds) in terms
  * of which frame timestamps are represented. For fixed-fps content,
  * timebase should be 1/framerate and timestamp increments should be
  * identically 1.
  * This often, but not always is the inverse of the frame rate or field rate
  * for video. 1/time_base is not the average frame rate if the frame rate is not
  * constant.
  *
  * Like containers, elementary streams also can store timestamps, 1/time_base
  * is the unit in which these timestamps are specified.
  * As example of such codec time base see ISO/IEC 14496-2:2001(E)
  * vop_time_increment_resolution and fixed_vop_rate
  * (fixed_vop_rate == 0 implies that it is different from the framerate)
  *
  * - encoding: MUST be set by user.
  * - decoding: the use of this field for decoding is deprecated.
  *             Use framerate instead.
  */
AVRational time_base;

从中可以看出,对于解码而言,time_base 已经被废弃,需要使用 framerate 来替换 time_base。并且,对于固定帧率而言,time_base = 1/framerate,但是,并非总是如此。

利用 H264Naked 对 test.ts 对应的 H264 码流进行分析,我们得到 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 的结果看,test.ts 视频中,framerate = 0/0,而 time_base = 1/2000

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

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

04 avformat_find_stream_info

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

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

* @section lavf_decoding_open Opening a media file
* The minimum information required to open a file is its URL, which
* is passed to avformat_open_input(), as in the following code:
* @code
* const char    *url = "file:in.mp3";
* AVFormatContext *s = NULL;
* int ret = avformat_open_input(&s, url, NULL, NULL);
* if (ret < 0)
*     abort();
* @endcode
* The above code attempts to allocate an AVFormatContext, open the
* specified file (autodetecting the format) and read the header, exporting the
* information stored there into s. Some formats do not have a header or do not
* store enough information there, so it is recommended that you call the
* avformat_find_stream_info() function which tries to read and decode a few
* frames to find missing information.

需要注意的是: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 个步骤:

图片

avformat_find_stream_info() 的重要步骤说明

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

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

  • 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_fram****e = 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_scalesps.num_units_in_tick 有关。

st->internal->avctx->time_base.num = sps->num_units_in_tick * 
    st->internal->avctx->ticks_per_frame

st->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}

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->time_base = sps->num_units_in_tick / sps->time_scale
    

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

    这也就是为什么 libavcodec/avcodec.h 中说:

    * 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 的逻辑 是错误的。

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

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

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

05 ff_h264_decoder

libavcodec/decode.c 中的 decode_simple_internal() 中会调用对应的解码器来进行解码(STPE 5)。而正如前所示,test.ts 为 H264 编码的视频流,因此,此处会调用 H264 解码器来进行解码。在 FFMpeg 中,H264 解码器位于 libavcodec/h264dec.c 中定义的 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,
    ......
};

在上文图中的 STPE 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。

06 h264_decode_frame

h264_decode\frame() 的整体逻辑如下图所示:

图片

AVCodecContext.ticks_per_frame

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

libavcodec/avcodec.h 中的字段说明:

/**
 * 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 中的 h264_decode_init()

avctx->ticks_per_frame = 2;

07 如何计算 framerate

如何计算 st->internal->avctx->framerate

  • STEP 1. 根据整体的计算流程可知,此处的 h 实际上就是 avformat_find_stream_info() 中的 st->internal->avctx->priv_data。h 会一直透传到之后的所有流程,这个务必要注意。

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

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}

08 结论

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

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

  • sps.time_scale

  • sps.num_units_in_tick

  • AVCodecContex.ticks_per_frame

  • 在 FFMpeg 中,frameratetime_base 的关系为:

  • framerate = 1 / (time_base * ticks_per_frame)

  • 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.avctx,AVStream.avctx.framerate 则取自 AVStream.framerate

————END————

推荐阅读:

百度 Android 直播秒开体验优化

iOS SIGKILL 信号量崩溃抓取以及优化实践

如何在几百万qps的网关服务中实现灵活调度策略

深入浅出DDD编程

百度APP iOS端内存优化实践-内存管控方案

Ernie-SimCSE对比学习在内容反作弊上应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/85649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过商业智能(BI)可视化数据分析了解布洛芬的产销情况

我们都知道&#xff0c;在我们的生活中&#xff0c;处处都是数据。但是只有数据&#xff0c;比如1、2、45、68、137.5&#xff0c;这些数据单一来看并不能反映任何问题。必须通过数据的分析才能将这些单一、无意义的数字变成我们能了解的信息。简单来说&#xff0c;就是数据≠信…

将多个Word表格中的指定值提取到Excel中,方便查看、统计、汇总。Word精灵

01需求说明 图1是简历样&#xff0c;简历中各项数据都放在表格中。现要求将图2中所有简历表的姓名、性别、出生日期、学历、籍贯、民族等等信息逐一提取出来&#xff0c;整理到Excel中&#xff0c;方便查看及汇总。 图1 简历表 图2 要汇总的简历表 02操作步骤 要提取所有简…

C#启程—开发环境搭建

文章目录ideRider下载和安装创建C#基础工程&#xff08;.Net_Desktop_Form&#xff09;Rider去除语法警告C#笔记namespace找不到某个class&#xff08;命名空间&#xff09;ide Rider ide我们选择Rider 为何不选vs&#xff1f;vs占硬盘内存太高了&#xff08;20多G&#xff0…

DATAKIT CrossManager 2022.4 Crack

CrossManager 是一款独立软件&#xff0c;可让您转换大多数 CAD 格式的文件。 使用 Cross Manager&#xff0c;您只需选择一个或多个 CAD 文件&#xff0c;即可将它们自动翻译成您想要的格式。 DATAKIT CrossManager是一款独立软件&#xff0c;可让您转换大多数 CAD 格式的文件…

java对接打码平台用selenium实现对图片验证码识别(对接文档看这一个就够了)

在很多平台软件中&#xff0c;咱们登录之后都有一些验证&#xff0c;例如图片数字验证&#xff0c;还有现在流行的滑块验证码&#xff0c;点选验证码&#xff0c;这么复杂的事情&#xff0c;我们程序员当然要用程序的方式解决啦&#xff0c;所以也有一些平台提供了快捷验证的方…

ElasticSearch的读写更新数据流程

读数据流程 客户端向 Node1&#xff08;协调节点&#xff09; 发送获取请求。节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。为了负载均衡&#xff0c;可以轮询所有节点&#xff0c;最后它将请求转发到 Node 2 。Node 2 将文档返回给 N…

通达信接口最新版wind量化特征

通达信接口最新版wind量化特征 1、通达信接口最新版交易接口用于什么&#xff1f; MetaTrade.dll它是一个股票交易接口&#xff0c;可以用于股票程序交易。通过将你的交易策略编写为代码&#xff0c;并通过调用接口股票、撤单、查询&#xff0c;从而实现股票自动交易的程序化。…

【开发工具】Office Tool Plus 安装 Office

一、安装Office&#xff1a; 第一步&#xff1a;打开Office Tool Plus&#xff0c;没有的去官网下载&#xff1a; Office Tool Plus 官方网站 - 一键部署 OfficeOffice Tool Plus 是一个用于部署、激活 Office、Visio、Project 的小工具。借助本工具&#xff0c;你可以快速地…

web前端期末大作业——开心旅游网站设计与实现(HTML+CSS+JavaScript)

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

STM32F4 | SYSTEM文件夹介绍 | delay文件夹 | sys文件夹 | usart文件夹

文章目录一、delay 文件夹代码介绍1.delay_init 函数2.delay_us 函数3.delay_ms函数二、sys 文件夹代码介绍1.IO 口的位操作实现三、usart 文件夹代码介绍1.printf 函数支持在 新建工程模板——库函数版本中&#xff0c;我们用到了一个 SYSTEM 文件夹里面的代码&#xff0c;此…

基于java+springboot+mybatis+vue+mysql的智慧养老平台

项目介绍 随着社会的发展我国的人口老龄化严重&#xff0c;为了让这些在年前是给社会做出过贡献的老人老有所依&#xff0c;老有所养&#xff0c;度过一个安详的晚年&#xff0c;很多地方都实现了智慧养老&#xff0c;为此我们通过springbootvueelementUI 开发了本次基于java的…

1.用Python写了一个进销存管理的软件~需求分析界面设计数据库设计技术路线选择~

一、需求分析 总体来说&#xff0c;就是一个在游泳馆使用的进销存管理软件&#xff0c;记录商品的入库、出库情况&#xff0c;以及统计销售的金额等~ 整个系统有三类用户&#xff0c;系统管理员、公司管理员和公司销售员&#xff0c;系统管理员负责录入公司信息以及分配用户&…

ActiveMQ、RabbitMQ、RocketMQ、Kafka区别

1、4种消息中间件比较 特性ActiveMQRabbitMQRocketMQKafka开发语⾔javaerlangjavascala单机吞吐量万级万级10万级10万级时效性ms级us级ms级ms级以内可⽤性⾼(主从架构)⾼(主从架构)⾮常⾼(分布式架构)⾮常⾼(分布式架构)功能特性 成熟的产品&#xff0c; 在很多公司得到应⽤&a…

FFmpeg - Windows下使用ShiftMediaProject方法编译FFmpeg

文章目录一、创建一个ShiftMediaProject文件夹二、下载ShiftMediaProject源码 &#xff08;以下操作最好都要翻墙&#xff09;三、下载其他头文件四、编译五、参考资料一、创建一个ShiftMediaProject文件夹 我创建在&#xff1a; C:\ShiftMediaProject 二、下载ShiftMediaPro…

【LeetCode题目详解】(一)27.原地移除元素、88.合并两个有序数组

目录 一、力扣第27题&#xff1a;原地移除元素 1.思路一&#xff1a; 2.思路二 3.思路三 二、力扣第88题&#xff1a;合并两个有序数组 1.思路一&#xff1a; 2.思路二&#xff1a; 3.思路三&#xff1a; 总结 一、力扣第27题&#xff1a;原地移除元素 题目链接&#xf…

基于YOLOv3的车辆号牌定位

01 OCR原理分析 本文中采用的车辆号牌识别部分的是采用CNNLSTMCTC组合而成&#xff0c;整个网络部分可以分为三个部分&#xff0c;首先是主干网络CNN用于提取字符的特征信息&#xff0c;其次采用深层双向LSTM网络在卷积特征的基础上提取文字或字符的序列特征&#xff0c;最终引…

基于java+springboot+mybatis+vue+mysql的校园台球厅人员与设备管理系统

项目介绍 校园台球厅人员与设备管理系统采用java技术&#xff0c;基于springboot框架&#xff0c;前端使用vue技术&#xff0c;mysql数据库进行开发&#xff0c;实现了以下功能&#xff1a; 本系统主要包括管理员和用户两个角色组成&#xff0c;主要包括以下功能&#xff1a;…

m基于LMMSE+turbo算法的信道估计均衡器误码率仿真,对比LS,DEF以及LMMSE三种均衡算法误码率

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 本文推导了符号间干扰&#xff08;ISI&#xff09;信道的矢量形状因子图表示。结果图具有树形结构&#xff0c;避免了现有图方法中的短周期问题。基于联合高斯近似&#xff0c;我们在LLR&#xf…

CUDA入门和网络加速学习(二)

0. 简介 最近作者希望系统性的去学习一下CUDA加速的相关知识&#xff0c;正好看到深蓝学院有这一门课程。所以这里作者以此课程来作为主线来进行记录分享&#xff0c;方便能给CUDA网络加速学习的萌新们去提供一定的帮助。 1. 基础矩阵乘法 下图是矩阵乘法的示意图&#xff0…

MySQL表的增删查改(上)

作者&#xff1a;~小明学编程 文章专栏&#xff1a;MySQL 格言&#xff1a;目之所及皆为回忆&#xff0c;心之所想皆为过往 前面给大家分享了关于数据库的一些基本的操作&#xff0c;今天分享的是数据库的核心内容&#xff0c;那就是我们常说的增删查改&#xff0c;也是我们数…