JavaCV读取mov文件帧数为0
JavaCV读取mov文件帧数为0
抢小孩糖吃 发表于3个月前
JavaCV读取mov文件帧数为0
  • 发表于 3个月前
  • 阅读 15
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 十分钟定制你的第一个小程序>>>   

编者注

之前一直被某大公司面试时说对各种原理不深入,在这里碰到一个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.cppcap_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.hppcap_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_secget_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_inputav_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

共有 人打赏支持
抢小孩糖吃
粉丝 59
博文 174
码字总数 198103
×
抢小孩糖吃
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: