代码改变世界

学习OpenCV2 C++ API(2)-- 简易视频播放器

2013-03-04 09:42  robturtle  阅读(3864)  评论(0编辑  收藏  举报

参考第二章,花了一个晚上写出了一个简易视频播放器。

昨晚在看cv:createTrackbar的时候,才真正算理解了回调函数的用法,于是模仿它的写法,在播放器类也设计了类似的回调函数接口与外界互动。写着写着,突然觉得,怎么感觉这么像在写 win32 的程序,而且编写的过程中也极容易出现写 win32 程序时出现的错误,如窗口未初始化,未检查窗口组件状态等疏忽导致崩溃等等。

而且自己写时才深刻地感受到,使用回调函数接口的方式操作控制流,使得代码看起来非常地乱。而且因为设计时的一时疏忽,我并没有在类中保存Play()下的回调函数相应资源,为了使播放器能从暂停中恢复,不得不使用了非常丑陋的实现方式。接下来的改进,要不使这相应资源成为类成员,要不让类成员缓存相应资源。

总而言之,我感觉回调函数的互动方式是非常低抽象而且丑陋的。自己尝试了一下,才发觉,之前觉得一文不值的MFC的消息映射方式也有它的长处。昨晚的这一次尝试让我预感,如果对这些接口继续包装完善下去,最终的解决方案可能也将是一个类似消息映射的交换机制。究其原因,恐怕还是说到GUI下的逻辑关系比较复杂。讲到这里,不得不提在收尾阶段的时候,为了添加摄像头支持,又加入了很多丑陋的控制语句。虽然视频文件和摄像头很“优雅”地能够被同一个类打开读取,但二者之间允许操作还是有很多互不相交。

罢了, 反正就是一个花了一个晚上的粗制品,以后慢慢打磨就是了。

目前的解决方案。VideoPlayer 设计了两个回调函数的接口,一个是用于在显示图像前对图像进一步处理的,另一个是控制按键响应的。感觉可以把第二个接口再抽象一点,让它可以很容易地接入GUI控制的前端。回调函数的签名方面,模仿cv::createTrackbar,增加了void*类型的参数用于传入用户数据。函数签名如下:

using img_postprocessor_t = std::function<void(cv::Mat&, void*)>;
using keydown_callback_t = std::function<void(char, VideoPlayer&, void*)>;

编译要求

1. C++11 支持;

2. boost库, opencv2, ffmpeg;

3. 工具代码 “scpp_assert.hpp",可以使用如下代码确保正确编译

// scpp_assert.hpp
#ifndef SCPP_ASSERT_HPP
#define SCPP_ASSERT_HPP
#define <assert.h>

#define SCPP_ASSERT(expr, msg) assert(expr)
#define SCPP_TEST_ASSERT(expr, msg) assert(expr)

#endif /*SCPP_ASSERT_HPP*/

4.工具代码"cppcommon.hpp", 用到的代码如下:

#ifndef CPPCOMMON_HPP
#define CPPCOMMON_HPP
#include <iostream>
namespace cliout {

using std::cout;
using std::endl;
using std::cerr;

#define USAGE(expr, options) \
    if (!(expr)) {
        std::cout << "Usage: " << argv[0]
                  << " " << options << std::endl;
        return 1;
    }

} // namespace

#endif

  

简易播放器

目前并没有测试完所有接口,也没有写测试套件,源代码如下:

// TODO: Multi thread support
// TODO: better inter-class communication, at least better callback signature
#ifndef  VIDEOPLAYER_HPP
#define  VIDEOPLAYER_HPP

#include    "opencv2/opencv.hpp"
#include    "boost/dynamic_bitset.hpp"
#include    "scpp_assert.hpp"
#include  <string>
#include  <iostream>// test

#define capture(...) \
    try {\
        __VA_ARGS__\
    } catch (...) {\
        std::cout << __LINE__ << std::endl;\
    }

class VideoPlayer;

static void onKeyDown(char key, VideoPlayer& vp, void * data);

static void setProgress(int pos, void * pCap)
{
    using vc = cv::VideoCapture;
    vc cap = * (vc*) pCap;

    cap.set(CV_CAP_PROP_POS_FRAMES, pos);
}

class VideoPlayer {
    // Display
    cv::VideoCapture        cap_;
    cv::Mat                 frame_;
    int                     frame_pos_ = 0;
    int                     frame_count_;
      // msec per frame_, set by file / device original settings
    int                     mspf0_;
    int                     mspf_;
      // play_rate = current_fps : original_fps
    double                  play_rate_ = 1.0;

    // Window
    std::string             win_name_;

    // Trackbar
    std::string             tb_name_;

    // flags
    boost::dynamic_bitset<> flags_ =
      boost::dynamic_bitset<>(4);
    enum flag { showTrackbar, paused, fromCameral, quit };

    // Keydown handler
public:
    using keydown_callback_t =
          std::function<void(char, VideoPlayer&, void*)>;
private:
    keydown_callback_t keydownCallback_;
    void *             keydownData_;

    // Implementation
    void initialize()
    {
        cv::destroyWindow(win_name_);

        if (flags_.test(fromCameral)) {
            frame_pos_ = frame_count_ - 1;
            mspf_ = mspf0_ = 30;
        } else {
            frame_count_ = cap_.get(CV_CAP_PROP_FRAME_COUNT);
            SCPP_ASSERT(frame_count_ > 0,
              "frame count " << frame_count_
              << " must greater than 0");

            int fps = cap_.get(CV_CAP_PROP_FPS);
            mspf_ = mspf0_ = 1000 / fps;
        }

        cv::namedWindow(win_name_);

        if (flags_.test(showTrackbar) &&
            !flags_.test(fromCameral)) {
            cv::createTrackbar(tb_name_, win_name_,
              /* current pos = */ &frame_pos_,
              /*     max pos = */ frame_count_,
              /*    postprocess = */ setProgress,
              /*postprocess arg = */ (void*) &cap_);
        }

        if (!frame_.empty())
            cv::imshow(win_name_, frame_);
    }
public:
    VideoPlayer(const std::string & file,
                const std::string & windowName   = "Video Player",
                bool                isPaused     = false,
                bool                hasTrackbar  = true,
                const std::string & trackbarName = "Progress",
                keydown_callback_t  key_callback = onKeyDown,
                void *              key_data     = 0)
    : cap_(file)
    , win_name_(windowName)
    , tb_name_(trackbarName)
    , keydownCallback_(key_callback)
    , keydownData_(key_data)
    {
        SCPP_ASSERT(cap_.isOpened() && cap_.grab(),
          "Bad Video file: " << file);

        flags_.set(fromCameral, false);
        flags_.set(paused, isPaused);
        flags_.set(showTrackbar, hasTrackbar);

        initialize();
    }

    VideoPlayer(int                 device,
                const std::string & windowName   = "Cameral",
                bool                isPaused     = false,
                bool                hasTrackbar  = true,
                const std::string & trackbarName = "Progress",
                keydown_callback_t  key_callback = onKeyDown,
                void *              key_data     = 0)
    : cap_(device)
    , win_name_(windowName)
    , tb_name_(trackbarName)
    , keydownCallback_(key_callback)
    , keydownData_(key_data)
    {
       SCPP_ASSERT(cap_.isOpened() && cap_.grab(),
          "Can not connect to cameral: " << device);

        flags_.set(fromCameral, true);
        flags_.set(paused, isPaused);
        flags_.set(showTrackbar, hasTrackbar);

        initialize();
    }

    ~VideoPlayer()
    {
        cv::destroyWindow(win_name_);
    }

    using img_postprocessor_t = std::function<void(cv::Mat&, void*)>;
    void Play(img_postprocessor_t postprocess = 0,
              void * post_arg = 0)
    {
        while (frame_pos_ < frame_count_) {
            if (flags_.test(quit)) break;
            if (flags_.test(paused)) {
                char c = cv::waitKey(0);
                keydownCallback_(c, *this, keydownData_);
                continue;
            }

            cap_ >> frame_;
            if (postprocess)
                postprocess(frame_, post_arg);

            cv::imshow(win_name_, frame_);

            if (!flags_.test(fromCameral))
                synchronize(++frame_pos_);

            char c = cv::waitKey(mspf_);
            keydownCallback_(c, *this, keydownData_);
        }
    }

    inline void Pause()
    { flags_.flip(paused); }

    inline void Quit()
    { flags_.set(quit, true); }

    inline bool isFromCameral()
    { return flags_.test(fromCameral); }

    void DrawTrackbar()
    {
        if (flags_.test(fromCameral)) return;
        flags_.flip(showTrackbar);
        initialize();
    }

    void setPlayRate(double rate)
    {
        if (play_rate_ != rate) {
            play_rate_ = rate;
            mspf_ = mspf0_ / play_rate_;
        }
    }

    void stepForward(int frames)
    {
        if (frame_pos_ + frames >= frame_count_) {
            frame_pos_ = frame_count_ - 1;
        } else {
            frame_pos_ += frames;
        }

        synchronize(frame_pos_);
    }

    void StepBackward(int frames)
    {
        if (frame_pos_ - frames < 0) {
            frame_pos_ = 0;
        } else {
            frame_pos_ -= frames;
        }

        synchronize(frame_pos_);
    }

private:
    void synchronize(int frame_pos)
    {
        if (flags_.test(showTrackbar)) {
            cv::setTrackbarPos(tb_name_, win_name_,
                               frame_pos);
        } else {
            cap_.set(CV_CAP_PROP_POS_FRAMES,
                               frame_pos);
        }
    }
};

void onKeyDown(char key, VideoPlayer& vp, void * data)
{
    key = std::tolower(key);

    switch (key) {
    case 'q':
        vp.Quit();
        break;
    case ' ': // space
        vp.Pause();
        break;
    case 't':
        vp.DrawTrackbar();
        break;
    default:
        break;
    }
}

#endif  /*VIDEOPLAYER_HPP*/

演示代码

下面的代码对最主要的几个接口进行了演示:

#include    "VideoPlayer.hpp"
#include    "cppcommon.hpp"
#include  <string>
#include  <iostream>// test
#include  <vector>

void blur(cv::Mat& img, void *nouse)
{
    cv::GaussianBlur(img, img, cv::Size(0, 0), 3, 3); 
}

void canny(cv::Mat& img, void *pConf)
{
    std::vector<double> conf;
    if (pConf)
        conf = * (std::vector<double>*) pConf;
    else
        conf = {7, 7, 1.5, 1.5, 0, 30, 3}; 

    cv::cvtColor(img, img, CV_8U, 1); 
    cv::GaussianBlur(img, img, 
                     cv::Size(conf[0],conf[1]),
                     conf[2], conf[3]);
    cv::Canny(img, img, conf[4], conf[5], conf[6]);
}

int main(int argc, char *argv[])
{
    using namespace cliout;
    USAGE(argc==2, "VideoFile");

    // use default window, has class's life cycle
    {   
        VideoPlayer player(argv[1]);
        player.Play(canny);
    }

    // use exsited window, has function's life cycle
    std::string winName("Existed window");
    cv::namedWindow(winName);
    VideoPlayer newpl(argv[1], winName);
    newpl.Play(blur);

    // use cameral and passing user data
    std::vector<double> conf = {3, 3, 2.5, 2.5, 3, 15, 3};
    VideoPlayer cam(0);
    cam.Play(canny, (void*) &conf);
}

效果图:

1: 采用默认参数的canny回调函数, 使用类内构造的窗口

 

2: 使用高斯模糊回调函数, 使用main中构造的窗口

 

3: 从摄像头读取,使用自定义参数调用canny,并且在main中创建的窗口,即使在类内被重建,它仍然具有整个应用程序的生命周期。