如何在Qt中有效地显示OpenCV视频?

时间:2014-01-21 00:14:44

标签: c++ multithreading qt opencv

我在OpenCV的帮助下从ip摄像头捕获多个流。当我尝试从OpenCV窗口(cv::namedWindow(...))显示这些流时,它没有任何问题(到目前为止我已尝试过多达4个流)。

当我尝试在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-必须走的路是什么?

1 个答案:

答案 0 :(得分:27)

使用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在QThread中默认提供本机消息调度 Qt事件循环,因此将QObject用于捕获过程是一件简单的事情。以下是一个完整的测试示例。

捕获,转换和查看器都在自己的线程中运行。由于cv::Mat是一个隐式共享类,具有原子线程安全访问权限,因此可以使用它。

转换器可以选择不处理陈旧帧 - 如果转换仅用于显示目的,则非常有用。

查看器在gui线程中运行并正确地删除过时帧。观众没有理由处理陈旧的画面。

如果您要收集数据以保存到磁盘,则应以高优先级运行捕获线程。您还应该检查OpenCV apis以查看是否有将本机摄像机数据转储到磁盘的方法。

为了加快转换速度,您可以在OpenCV中使用gpu-accelerated类。

下面的示例确保除非副本需要,否则不会重新分配任何内存:Capture类维护自己的帧缓冲区,该缓冲区将重复用于每个后续帧,Converter也是如此,ImageViewer

也是如此

图像数据有两个深层副本(除了cv::VideoCatprure::read内部发生的任何内容):

  1. 复制到Converter的{​​{1}}。

  2. 复制到QImage的{​​{1}}。

  3. 需要两个副本来确保线程之间的分离并防止数据重新分配,因为需要分离引用计数大于1的ImageViewerQImage。在现代体系结构中,内存副本非常快。

    由于所有图像缓冲区都位于相同的内存位置,因此它们的性能最佳 - 它们会保持分页和缓存。

    cv::Mat用于跟踪内存重新分配以进行调试。

    QImage

    AddressTracker类用捕获的帧填充内部帧缓冲区。它通知帧更改。该框架是该类的用户属性。

    // https://github.com/KubaO/stackoverflown/tree/master/questions/opencv-21246766
    #include <QtWidgets>
    #include <algorithm>
    #include <opencv2/opencv.hpp>
    
    Q_DECLARE_METATYPE(cv::Mat)
    
    struct AddressTracker {
       const void *address = {};
       int reallocs = 0;
       void track(const cv::Mat &m) { track(m.data); }
       void track(const QImage &img) { track(img.bits()); }
       void track(const void *data) {
          if (data && data != address) {
             address = data;
             reallocs ++;
          }
       }
    };
    

    Capture类将传入的帧转换为按比例缩小的class Capture : public QObject { Q_OBJECT Q_PROPERTY(cv::Mat frame READ frame NOTIFY frameReady USER true) cv::Mat m_frame; QBasicTimer m_timer; QScopedPointer<cv::VideoCapture> m_videoCapture; AddressTracker m_track; public: Capture(QObject *parent = {}) : QObject(parent) {} ~Capture() { qDebug() << __FUNCTION__ << "reallocations" << m_track.reallocs; } 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 frameReady(const cv::Mat &); cv::Mat frame() const { return m_frame; } private: void timerEvent(QTimerEvent * ev) { if (ev->timerId() != m_timer.timerId()) return; if (!m_videoCapture->read(m_frame)) { // Blocks until a new frame is ready m_timer.stop(); return; } m_track.track(m_frame); emit frameReady(m_frame); } }; 用户属性。它通知图像更新。保留图像以防止重新分配内存。 Converter属性选择是否转换所有帧,或者只有最新的帧应该排队等候。

    QImage

    processAll小部件相当于存储像素图的class Converter : public QObject { Q_OBJECT Q_PROPERTY(QImage image READ image NOTIFY imageReady USER true) Q_PROPERTY(bool processAll READ processAll WRITE setProcessAll) QBasicTimer m_timer; cv::Mat m_frame; QImage m_image; bool m_processAll = true; AddressTracker m_track; 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(const cv::Mat &frame) { Q_ASSERT(frame.type() == CV_8UC3); int w = frame.cols / 3.0, h = frame.rows / 3.0; if (m_image.size() != QSize{w,h}) m_image = QImage(w, h, QImage::Format_RGB888); cv::Mat mat(h, w, CV_8UC3, m_image.bits(), m_image.bytesPerLine()); cv::resize(frame, mat, mat.size(), 0, 0, cv::INTER_AREA); cv::cvtColor(mat, mat, CV_BGR2RGB); emit imageReady(m_image); } void timerEvent(QTimerEvent *ev) { if (ev->timerId() != m_timer.timerId()) return; process(m_frame); m_frame.release(); m_track.track(m_frame); m_timer.stop(); } public: explicit Converter(QObject * parent = nullptr) : QObject(parent) {} ~Converter() { qDebug() << __FUNCTION__ << "reallocations" << m_track.reallocs; } bool processAll() const { return m_processAll; } void setProcessAll(bool all) { m_processAll = all; } Q_SIGNAL void imageReady(const QImage &); QImage image() const { return m_image; } Q_SLOT void processFrame(const cv::Mat &frame) { if (m_processAll) process(frame); else queue(frame); } }; 。图像是查看器的用户属性。传入的图像被深度复制到用户属性中,以防止重新分配内存。

    ImageViewer

    演示实例化上述类,并在专用线程中运行捕获和转换。

    QLabel

    这是完整的例子。注意:此答案的先前版本不必要地重新分配了图像缓冲区。