【问题标题】:How to efficiently display OpenCV video in Qt?如何在 Qt 中高效显示 OpenCV 视频?
【发布时间】:2014-02-10 09:01:54
【问题描述】:

我在 OpenCV 的帮助下从网络摄像机捕获多个流。当我尝试从 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- 必须走的路是什么?

【问题讨论】:

  • 您是否尝试忽略 QImage 并直接使用数据数组作为纹理在 QGLWidget 中进行渲染?我猜应该会更快。专业的解决方案可能会使用专门的硬件,但这也只是一种猜测。

标签: c++ multithreading qt opencv


【解决方案1】:

使用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 api,看看是否有办法将本机相机数据转储到磁盘。

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

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

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

  1. 复制到ConverterQImage

  2. 复制到ImageViewerQImage

由于需要分离引用计数高于 1 的 cv::MatQImage,因此需要两个副本来确保线程之间的解耦并防止数据重新分配。在现代架构上,内存副本非常快。

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

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。图像是查看器的用户属性。传入的图像被深度复制到用户属性中,以防止内存重新分配。

class ImageViewer : public QWidget {
   Q_OBJECT
   Q_PROPERTY(QImage image READ image WRITE setImage USER true)
   bool painted = true;
   QImage m_img;
   AddressTracker m_track;
   void paintEvent(QPaintEvent *) {
      QPainter p(this);
      if (!m_img.isNull()) {
         setAttribute(Qt::WA_OpaquePaintEvent);
         p.drawImage(0, 0, m_img);
         painted = true;
      }
   }
public:
   ImageViewer(QWidget * parent = nullptr) : QWidget(parent) {}
   ~ImageViewer() { qDebug() << __FUNCTION__ << "reallocations" << m_track.reallocs; }
   Q_SLOT void setImage(const QImage &img) {
      if (!painted) qDebug() << "Viewer dropped frame!";
      if (m_img.size() == img.size() && m_img.format() == img.format()
          && m_img.bytesPerLine() == img.bytesPerLine())
         std::copy_n(img.bits(), img.sizeInBytes(), m_img.bits());
      else
         m_img = img.copy();
      painted = false;
      if (m_img.size() != size()) setFixedSize(m_img.size());
      m_track.track(m_img);
      update();
   }
   QImage image() const { return m_img; }
};

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

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::frameReady, &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"

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

【讨论】:

  • 有趣的解决方案。如果您连接了多个摄像头并在同一系统上运行,您将如何修改它?
  • @blackibiza main 方法可以简单地修改以启动多个捕获、转换器和图像查看器 - Capture::start 方法将相机编号作为参数。每个线程对象的方法在性能方面有点限制。线程是相当重量级的生物。使用QtConcurrent::run 可能会更好,因为它接近GCD 的最终性能。这段代码真的应该重构为类似 GCD 的,我可能有一天会这样做。
  • 为我节省了大量时间。但是现在我在打开相机源时遇到 OpenCV 不稳定的问题,所以我实际上正在寻找一种将 Qt 中的视频输入 OpenCV 的方法,而不是 hehe
  • @LennartRolland “OpenCV 不稳定” 你说的“不稳定”是什么意思?如果它崩溃了,那么使用调试器找出原因,修复它,然后提交一个 OpenCV 补丁。不崩溃,不然怎么不稳定?
  • @KubaOber:在一个完美的世界里,我会这样做,但是我有自己的议程和有限的时间。它不会崩溃,它根本无法可靠地打开视频源。我读到这是一个已知问题,最好的解决方法是自己提供视频(这就是我现在使用 Qt 所做的)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-30
  • 2017-05-29
  • 2013-07-04
相关资源
最近更新 更多