编者注
之前一直被某大公司面试时说对各种原理不深入,在这里碰到一个OpenCV的问题,决定研究OpenCV源代码,在研究解决方案。
问题代码
在使用JavaCV的过程中,我在MAC电脑上重新编译了一遍OpenCV是可以正确读取到mov的视频帧数的,但是转换到linux系统上,重新OpenCV无法解决这个问题。linux上无法让opencv3.2编译时链接好最新的ffmpeg库
package com.hava.xxx.xxx.xxx.utils;
import static org.bytedeco.javacpp.opencv_videoio.*;
/**
* Created by zhanpeng on 2017/2/23.
*/
public class JavaCV {
public static double numFrames(String video_path){
CvCapture cvCapture = cvCaptureFromFile(video_path);
double numFrames = cvGetCaptureProperty(cvCapture,CV_CAP_PROP_FRAME_COUNT);
return numFrames;
}
}
通过之前对JavaCV的翻译,确认Java调用C的类库实现的操作。则最核心的问题还是出现在C的库当中,当然经过多方测试,在Linux上编译了OpenCV但是无法和ffmpeg相连接。决定开展对OpenCV的解刨。
JavaCV代码映射OpenCV代码
通过上面的代码,我们可以了解到通过cvCaptureFromFile
函数对视频文件进行读取。通过cvGetCaptureProperty
函数放入之前文件对象和对应参数,能够获取帧数。通过import引入,可以明确知道这些函数在opencv_videoio
的包当中。
在OpenCV源代码中,进入build\opencv\opencv-3.2.0\modules\videoio\src\cap.cpp
文件中,首先找到cvGetCaptureProperty
函数,在opencv3.2源代码105-108行
CV_IMPL double cvGetCaptureProperty( CvCapture* capture, int id )
{
return icvGetCaptureProperty(capture, id);
}
由此了解到操作的实质是通过icvGetCaptureProperty
来实现的。
static inline double icvGetCaptureProperty( const CvCapture* capture, int id )
{
return capture ? capture->getProperty(id) : 0;
}
通过icvGetCaptureProperty
源代码可以看到,通过对capture对象的getProperty
来获取对应参数,如果获取失败则返回0。
顺便复习了下C语言的条件运算符<表达式1>?<表达式2>:<表达式3>
,首先判断表达式1,如果为真则执行表达式2,如果为假则执行表达式3。
OpenCV的实现调用问题
通过分析源代码,可以看到很多种类的实现,各个实现也都具有getProperty
函数,则能够确认的是调用了某个实现的getProperty才能够获取序列帧数。虽然不清楚为什么JavaCV的函数调用为什么和Cpp版本的名称不一致,但是我还是找到了Cpp版本的实现。
CV_IMPL CvCapture * cvCreateFileCapture (const char * filename)
{
return cvCreateFileCaptureWithPreference(filename, CV_CAP_ANY);
}
根据上面的代码,可以看到重点是cvCreateFileCaptureWithPreference
,以下截取比较重要的内容部分
/**
* Videoreader dispatching method: it tries to find the first
* API that can access a given filename.
*/
CV_IMPL CvCapture * cvCreateFileCaptureWithPreference (const char * filename, int apiPreference)
{
CvCapture * result = 0;
switch(apiPreference) {
default:
// user specified an API we do not know
// bail out to let the user know that it is not available
if (apiPreference) break;
#ifdef HAVE_FFMPEG
case CV_CAP_FFMPEG:
TRY_OPEN(result, cvCreateFileCapture_FFMPEG_proxy (filename))
if (apiPreference) break;
#endif
。。。。。。
#if defined(HAVE_QUICKTIME) || defined(HAVE_QTKIT)
case CV_CAP_QT:
TRY_OPEN(result, cvCreateFileCapture_QT (filename))
if (apiPreference) break;
#endif
。。。。。。
return result;
}
以上仅仅只截取了比较重要的部分,首先可以看到,如果具有ffmpeg的包,则opencv会优先调用ffmpeg,但是mac天生自带quicktime,在opencv的实现中也可能会调用quicktime的模块来实现帧数的调取,由于是Java调用Cpp的库,我当前无法对Cpp的库进行Debug,则我也无法弄清楚,在mac具体调用的是什么实现。但是我认为对ffmpeg和quicktime的opencv实现,对获取视频序列帧数是有极大的帮助。
OpenCV真实调用在哪里?
通过上面的结论加上,可以知道去查找cap_ffmpeg.cpp
和cap_qt.cpp
的源代码实现,但是会碰到新的问题。首先以ffmpeg为例子
virtual double getProperty(int propId) const
{
return ffmpegCapture ? icvGetCaptureProperty_FFMPEG_p(ffmpegCapture, propId) : 0;
}
通过getProperty的实现可以知道实现是由icvGetCaptureProperty_FFMPEG_p
来完成的
static CvGetCaptureProperty_Plugin icvGetCaptureProperty_FFMPEG_p = 0;
icvGetCaptureProperty_FFMPEG_p = (CvGetCaptureProperty_Plugin)GetProcAddress(icvFFOpenCV, "cvGetCaptureProperty_FFMPEG");
icvGetCaptureProperty_FFMPEG_p = (CvGetCaptureProperty_Plugin)cvGetCaptureProperty_FFMPEG;
以上代码当中GetProcAddress
函数检索指定的动态链接库(DLL)中的输出库函数地址。也就是说调用动态链接库cvGetCaptureProperty_FFMPEG
,新的问题这个方法在哪里?应该存在于ffmpeg
cap_ffmpeg_impl.hpp
20170801
之前的问题困扰了很久,隔三差五的Google,但是就是没找到结果。神奇的向小伙伴抱怨找不到的时候看到了神奇的两个文件cap_ffmpeg_api.hpp和cap_ffmpeg_impl.hpp。impl难道是实现,为什么实现放到hpp里面,听小伙伴说hpp是头文件,头文件放什么实现@_@
在ffmpeg_api.hpp文件中找到了我之前一直没有找到的函数定义
OPENCV_FFMPEG_API double cvGetCaptureProperty_FFMPEG(struct CvCapture_FFMPEG* cap, int prop);
OPENCV_FFMPEG_API double cvGetCaptureProperty_FFMPEG_2(struct CvCapture_FFMPEG_2* cap, int prop);
在ffmpeg_impl.hpp中
double cvGetCaptureProperty_FFMPEG(CvCapture_FFMPEG* capture, int prop_id)
{
return capture->getProperty(prop_id);
}
和如下实现
double CvCapture_FFMPEG::getProperty( int property_id ) const
{
if( !video_st ) return 0;
switch( property_id )
{
case CV_FFMPEG_CAP_PROP_POS_MSEC:
return 1000.0*(double)frame_number/get_fps();
case CV_FFMPEG_CAP_PROP_POS_FRAMES:
return (double)frame_number;
case CV_FFMPEG_CAP_PROP_POS_AVI_RATIO:
return r2d(ic->streams[video_stream]->time_base);
case CV_FFMPEG_CAP_PROP_FRAME_COUNT:
return (double)get_total_frames();
case CV_FFMPEG_CAP_PROP_FRAME_WIDTH:
return (double)frame.width;
case CV_FFMPEG_CAP_PROP_FRAME_HEIGHT:
return (double)frame.height;
case CV_FFMPEG_CAP_PROP_FPS:
return get_fps();
case CV_FFMPEG_CAP_PROP_FOURCC:
#if LIBAVFORMAT_BUILD > 4628
return (double)video_st->codec->codec_tag;
#else
return (double)video_st->codec.codec_tag;
#endif
case CV_FFMPEG_CAP_PROP_SAR_NUM:
return get_sample_aspect_ratio(ic->streams[video_stream]).num;
case CV_FFMPEG_CAP_PROP_SAR_DEN:
return get_sample_aspect_ratio(ic->streams[video_stream]).den;
default:
break;
}
return 0;
}
终于找到帧数的实现get_total_frames
int64_t CvCapture_FFMPEG::get_total_frames() const
{
int64_t nbf = ic->streams[video_stream]->nb_frames;
if (nbf == 0)
{
nbf = (int64_t)floor(get_duration_sec() * get_fps() + 0.5);
}
return nbf;
}
终于找到了使用ffmpeg实现的地方,分别查看get_duration_sec和get_fps函数,从上面代码我们知道首先尝试从流当中取总帧数,如果返回为0,则获取视频长度和帧速率+0.5,之后向下取整数。
double CvCapture_FFMPEG::get_duration_sec() const
{
double sec = (double)ic->duration / (double)AV_TIME_BASE;
if (sec < eps_zero)
{
sec = (double)ic->streams[video_stream]->duration * r2d(ic->streams[video_stream]->time_base);
}
if (sec < eps_zero)
{
sec = (double)ic->streams[video_stream]->duration * r2d(ic->streams[video_stream]->time_base);
}
return sec;
}
和
double CvCapture_FFMPEG::get_fps() const
{
#if 0 && LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(55, 1, 100) && LIBAVFORMAT_VERSION_MICRO >= 100
double fps = r2d(av_guess_frame_rate(ic, ic->streams[video_stream], NULL));
#else
#if LIBAVCODEC_BUILD >= CALC_FFMPEG_VERSION(54, 1, 0)
double fps = r2d(ic->streams[video_stream]->avg_frame_rate);
#else
double fps = r2d(ic->streams[video_stream]->r_frame_rate);
#endif
#if LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(52, 111, 0)
if (fps < eps_zero)
{
fps = r2d(ic->streams[video_stream]->avg_frame_rate);
}
#endif
if (fps < eps_zero)
{
fps = 1.0 / r2d(ic->streams[video_stream]->codec->time_base);
}
#endif
return fps;
}
根据上面那两个方法的实现,我们可以知道根本还是从ic对象的streams中获取各种参数内容。
ic指针对象的调用
下面这里把ic的关键调用罗列出来
bool CvCapture_FFMPEG::open( const char* _filename )
{
unsigned i;
bool valid = false;
close();
#if USE_AV_INTERRUPT_CALLBACK
/* interrupt callback */
interrupt_metadata.timeout_after_ms = LIBAVFORMAT_INTERRUPT_OPEN_TIMEOUT_MS;
get_monotonic_time(&interrupt_metadata.value);
ic = avformat_alloc_context();
ic->interrupt_callback.callback = _opencv_ffmpeg_interrupt_callback;
ic->interrupt_callback.opaque = &interrupt_metadata;
#endif
#if LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(52, 111, 0)
av_dict_set(&dict, "rtsp_transport", "tcp", 0);
int err = avformat_open_input(&ic, _filename, NULL, &dict);
#else
int err = av_open_input_file(&ic, _filename, NULL, 0, NULL);
#endif
if (err < 0)
{
CV_WARN("Error opening file");
CV_WARN(_filename);
goto exit_func;
}
err =
#if LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(53, 6, 0)
avformat_find_stream_info(ic, NULL);
#else
av_find_stream_info(ic);
。。。。。。
可以看到在不同的在Cpp中又出现了#if,表示如果条件允许,则把范围内的代码编译进去,否则不进行编译。根据if else的代码我们可以看到ffmpeg代码的函数名称与参数的变化。还有一点我们终于明白ic的缩写含义了interrupt callback。
ffmpeg代码分析
在上面的代码最重要的部分是读取文件,我们可以看到根据不同版本,分别是avformat_open_input和av_open_input_file。
使用文件内容搜索工具搜索ffmpeg3.3.2,可以查找目录build/ffmpeg/ffmpeg-3.3.2/libavformat/utils.c
内部具有avformat_open_input
函数。头文件名称为avformat.h
。如下由于篇幅仅仅展示avformat.h关于该函数的描述
/**
* Open an input stream and read the header. The codecs are not opened.
* The stream must be closed with avformat_close_input().
*
* @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context).
* May be a pointer to NULL, in which case an AVFormatContext is allocated by this
* function and written into ps.
* Note that a user-supplied AVFormatContext will be freed on failure.
* @param url URL of the stream to open.
* @param fmt If non-NULL, this parameter forces a specific input format.
* Otherwise the format is autodetected.
* @param options A dictionary filled with AVFormatContext and demuxer-private options.
* On return this parameter will be destroyed and replaced with a dict containing
* options that were not found. May be NULL.
*
* @return 0 on success, a negative AVERROR on failure.
*
* @note If you want to use custom IO, preallocate the format context and set its pb field.
*/
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
通过描述,我们可以知道opencv源代码中的ic的数据结构为AVFormatContext