參考第二章,花了一個晚上寫出了一個簡易視頻播放器。
昨晚在看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中創建的窗口,即使在類內被重建,它仍然具有整個應用程序的生命周期。