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

Я собираю несколько потоков из ip-камер с помощью OpenCV. Когда я пытаюсь отобразить этот поток из окна OpenCV (cv::namedWindow(...)), он работает без каких-либо проблем (до сих пор я пробовал до 4 потоков).

Проблема возникает, когда я пытаюсь показать эти потоки внутри виджета Qt. Поскольку захват выполняется в другом потоке, я должен использовать механизм слота сигнала для обновления QWidget (который находится в основном потоке).

В принципе, я испускаю вновь захваченный кадр из потока захвата, и слот в потоке графического интерфейса пользователя ловит его. Когда я открываю 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 является неявно разделяемым классом с атомарным, потокобезопасным доступом, он используется как таковой.

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

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

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

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

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

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

  1. Копия в Converter QImage.

  2. Копия в ImageViewer QImage.

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

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

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"

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

Ответ 2

@KubaOber: Если я хочу добавить ссылку RTSP Streaming, где добавить в код. Он принимает только по умолчанию веб-камеру в ноутбуке. Я пытался добавить ссылку RTSP в строку m_videoCapture.reset (новый cv :: VideoCapture ("ссылка RTSP")); но по-прежнему принимает вид камеры по умолчанию. Пожалуйста, предложите