Как эффективно отображать видео OpenCV в Qt?

Я снимаю несколько потоков с IP-камер с помощью 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- Какой должен быть путь?

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 по умолчанию обеспечивает отправку собственных сообщений и цикл обработки событий Qt в QThreadпрост в использовании QObject для процесса захвата. Ниже приведен полный, проверенный пример.

Захват, преобразование и просмотрщик все работают в своих собственных потоках. поскольку cv::Mat является неявно разделяемым классом с атомарным, потокобезопасным доступом, он используется как таковой.

Конвертер имеет опцию не обрабатывать устаревшие кадры - полезно, если преобразование выполняется только для отображения.

Зритель запускается в потоке графического интерфейса и корректно удаляет устаревшие кадры. У зрителя никогда нет причин иметь дело с устаревшими кадрами.

Если вы собираете данные для сохранения на диск, вы должны запустить поток захвата с высоким приоритетом. Вам также следует проверить API-интерфейс OpenCV, чтобы выяснить, есть ли способ выгрузки данных с собственной камеры на диск.

Чтобы ускорить конвертацию, вы можете использовать gpu-ускоренные классы в OpenCV.

Приведенный ниже пример гарантирует, что ни одна память не будет перераспределена, если это не необходимо для копии: Capture класс поддерживает свой собственный буфер кадров, который повторно используется для каждого последующего кадра, так же как и Converterи то же самое ImageViewer,

Есть две глубокие копии данных изображения (кроме того, что происходит внутри cv::VideoCatprure::read):

  1. Копия к Converter"s QImage,

  2. Копия к ImageViewer"s QImage,

Обе копии необходимы для обеспечения разъединения между потоками и предотвращения перераспределения данных из-за необходимости отсоединения cv::Mat или же QImage с количеством ссылок выше 1. На современных архитектурах копии памяти очень быстрые.

Поскольку все буферы изображений находятся в одной и той же ячейке памяти, их производительность оптимальна - они остаются в памяти и кэшируются.

AddressTracker используется для отслеживания перераспределения памяти в целях отладки.

// https://github.com/KubaO/stackrun/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"

На этом завершается полный пример. Примечание. Предыдущая версия этого ответа излишне перераспределяла буферы изображений.

Другие вопросы по тегам