2014-01-21 26 views
17

我在OpenCV的幫助下從ip攝像頭捕獲多個流。當我嘗試從OpenCV窗口(cv::namedWindow(...))顯示這些流時,它可以正常工作(迄今爲止我已嘗試過4個流)。如何在Qt中有效地顯示OpenCV視頻?

當我嘗試在Qt小部件中顯示這些流時出現問題。由於捕獲是在另一個線程中完成的,我必須使用信號插槽機制來更新QWidget(位於主線程中)。

基本上,我從捕獲線程發出新捕獲的幀,並在GUI線程捕獲它。當我打開4個流時,我無法像以前一樣順暢地顯示視頻。

這裏是發射器:

void capture::start_process() { 
    m_enable = true; 
    cv::Mat frame; 

    while(m_enable) { 
     if (!m_video_handle->read(frame)) { 
      break; 
     } 
     cv::cvtColor(frame, frame,CV_BGR2RGB); 

     qDebug() << "FRAME : " << frame.data; 

     emit image_ready(QImage(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888)); 
     cv::waitKey(30); 
    } 
} 

這是我的槽:

void widget::set_image(QImage image) { 
    img = image; 
    qDebug() << "PARAMETER IMAGE: " << image.scanLine(0); 
    qDebug() << "MEMBER IMAGE: " << img.scanLine(0); 
} 

的問題似乎是不斷複製QImages的開銷。雖然QImage使用隱式共享,當我通過qDebug()消息比較圖像的數據指針時,我看到不同的地址。

1-有什麼方法可以將OpenCV窗口直接嵌入到QWidget中?

2-什麼是處理顯示多個視頻最有效的方法?例如,視頻管理系統如何在同一時間內顯示多達32臺攝像機?

3-什麼是一定要走的路?

+0

您是否嘗試忽略QImage並直接使用數據數組作爲紋理來渲染QGLWidget?我想這應該會更快。專業解決方案可能使用專用硬件,但這也只是一種猜測。 – Micka

回答

24

使用QImage::scanLine迫使深拷貝,所以至少,你應該使用constScanLine,或者更好的是,改變槽的簽名:

void widget::set_image(const QImage & image); 

當然,你的問題就變成別的東西:在QImage實例指向一個存在於另一個線程中的框架的數據,並且可以(並且將會)隨時改變。

有一個解決方案:需要使用分配在堆上的新鮮幀,並且需要在QImage內捕獲幀。 QScopedPointer用於防止內存泄漏,直到QImage取得該幀的所有權。

static void matDeleter(void* mat) { delete static_cast<cv::Mat*>(mat); } 

class capture { 
    Q_OBJECT 
    bool m_enable; 
    ... 
public: 
    Q_SIGNAL void image_ready(const QImage &); 
    ... 
}; 

void capture::start_process() { 
    m_enable = true; 
    while(m_enable) { 
    QScopedPointer<cv::Mat> frame(new cv::Mat); 
    if (!m_video_handle->read(*frame)) { 
     break; 
    } 
    cv::cvtColor(*frame, *frame, CV_BGR2RGB); 

    // Here the image instance takes ownership of the frame. 
    const QImage image(frame->data, frame->cols, frame->rows, frame->step, 
         QImage::Format_RGB888, matDeleter, frame.take());  
    emit image_ready(image); 
    cv::waitKey(30); 
    } 
} 

當然,因爲Qt提供本地信息調度默認Qt的事件循環在QThread,它的使用QObject用於捕獲過程一件簡單的事情。以下是一個完整的測試示例。

捕獲,轉換和查看器都在自己的線程中運行。由於cv::Mat是一個帶原子,線程安全訪問的隱式共享類,因此它被使用。

轉換器有一個不處理過時幀的選項 - 如果轉換僅用於顯示目的,該選項很有用。

查看器在gui線程中運行並正確刪除過時的幀。觀衆沒有理由處理陳舊的畫面。

如果您要收集數據以保存到磁盤,則應該以高優先級運行捕獲線程。您還應該檢查openCV apis,看看是否有方法將本機相機數據轉儲到磁盤。

爲了加速轉換,您可以在openCV中使用gpu加速類。

// https://github.com/KubaO/stackoverflown/tree/master/questions/opencv-21246766 
#include <QtWidgets> 
#include <opencv2/opencv.hpp> 

Q_DECLARE_METATYPE(cv::Mat) 

class Capture : public QObject { 
    Q_OBJECT 
    QBasicTimer m_timer; 
    QScopedPointer<cv::VideoCapture> m_videoCapture; 
public: 
    Capture(QObject * parent = {}) : QObject(parent) {} 
    Q_SIGNAL void started(); 
    Q_SLOT void start(int cam = {}) { 
     if (!m_videoCapture) 
      m_videoCapture.reset(new cv::VideoCapture(cam)); 
     if (m_videoCapture->isOpened()) { 
      m_timer.start(0, this); 
      emit started(); 
     } 
    } 
    Q_SLOT void stop() { m_timer.stop(); } 
    Q_SIGNAL void matReady(const cv::Mat &); 
private: 
    void timerEvent(QTimerEvent * ev) { 
     if (ev->timerId() != m_timer.timerId()) return; 
     cv::Mat frame; 
     if (!m_videoCapture->read(frame)) { // Blocks until a new frame is ready 
      m_timer.stop(); 
      return; 
     } 
     emit matReady(frame); 
    } 
}; 

class Converter : public QObject { 
    Q_OBJECT 
    QBasicTimer m_timer; 
    cv::Mat m_frame; 
    bool m_processAll = true; 
    static void matDeleter(void* mat) { delete static_cast<cv::Mat*>(mat); } 
    void queue(const cv::Mat & frame) { 
     if (!m_frame.empty()) qDebug() << "Converter dropped frame!"; 
     m_frame = frame; 
     if (! m_timer.isActive()) m_timer.start(0, this); 
    } 
    void process(cv::Mat frame) { 
     cv::resize(frame, frame, cv::Size(), 0.3, 0.3, cv::INTER_AREA); 
     cv::cvtColor(frame, frame, CV_BGR2RGB); 
     const QImage image(frame.data, frame.cols, frame.rows, frame.step, 
          QImage::Format_RGB888, &matDeleter, new cv::Mat(frame)); 
     Q_ASSERT(image.constBits() == frame.data); 
     emit imageReady(image); 
    } 
    void timerEvent(QTimerEvent * ev) { 
     if (ev->timerId() != m_timer.timerId()) return; 
     process(m_frame); 
     m_frame.release(); 
     m_timer.stop(); 
    } 
public: 
    explicit Converter(QObject * parent = nullptr) : QObject(parent) {} 
    void setProcessAll(bool all) { m_processAll = all; } 
    Q_SIGNAL void imageReady(const QImage &); 
    Q_SLOT void processFrame(const cv::Mat & frame) { 
     if (m_processAll) process(frame); else queue(frame); 
    } 
}; 

class ImageViewer : public QWidget { 
    Q_OBJECT 
    QImage m_img; 
    void paintEvent(QPaintEvent *) { 
     QPainter p(this); 
     p.drawImage(0, 0, m_img); 
     m_img = {}; 
    } 
public: 
    ImageViewer(QWidget * parent = nullptr) : QWidget(parent) { 
     setAttribute(Qt::WA_OpaquePaintEvent); 
    } 
    Q_SLOT void setImage(const QImage & img) { 
     if (!m_img.isNull()) qDebug() << "Viewer dropped frame!"; 
     m_img = img; 
     if (m_img.size() != size()) setFixedSize(m_img.size()); 
     update(); 
    } 
}; 

class Thread final : public QThread { public: ~Thread() { quit(); wait(); } }; 

int main(int argc, char *argv[]) 
{ 
    qRegisterMetaType<cv::Mat>(); 
    QApplication app(argc, argv); 
    ImageViewer view; 
    Capture capture; 
    Converter converter; 
    Thread captureThread, converterThread; 
    // Everything runs at the same priority as the gui, so it won't supply useless frames. 
    converter.setProcessAll(false); 
    captureThread.start(); 
    converterThread.start(); 
    capture.moveToThread(&captureThread); 
    converter.moveToThread(&converterThread); 
    QObject::connect(&capture, &Capture::matReady, &converter, &Converter::processFrame); 
    QObject::connect(&converter, &Converter::imageReady, &view, &ImageViewer::setImage); 
    view.show(); 
    QObject::connect(&capture, &Capture::started, [](){ qDebug() << "capture started"; }); 
    QMetaObject::invokeMethod(&capture, "start"); 
    return app.exec(); 
} 

#include "main.moc" 
+0

有趣的解決方案。如果連接了多臺攝像機並在同一系統上運行,您將如何修改它? – madduci

+1

@blackibiza'main'方法可以進行簡單修改以啓動多個捕獲,轉換器和圖像查看器 - 'Capture :: start'方法將攝像機編號作爲參數。每線程對象方法在性能方面有點限制。線程是相當重量級的生物。這個解決方案使用'QtConcurrent :: run'可能會更好,因爲這近似於[GCD]的最終性能(https://libdispatch.macosforge.org)。這段代碼真的應該重構爲GCD,我可能會這樣做。 –

+0

爲我節省了一天的時間。但是現在我在打開攝像機資源時苦於OpenCV不穩定,所以實際上我正在尋找一種將視頻從Qt提供給OpenCV的方法,而不是用OpenCV來解決這個問題 –

相關問題