学习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中创建的窗口,即使在类内被重建,它仍然具有整个应用程序的生命周期。