web-dev-qa-db-ja.com

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);
}
_

問題はQImageを継続的にコピーするオーバーヘッドのようです。 QImageは暗黙的な共有を使用しますが、qDebug()メッセージを介して画像のデータポインターを比較すると、異なるアドレスが表示されます。

1- OpenCVウィンドウをQWidgetに直接埋め込む方法はありますか?

2-複数のビデオの表示を処理する最も効率的な方法は何ですか?たとえば、ビデオ管理システムが同時に最大32台のカメラを表示する方法を教えてください。

3-どうすればよいですか。

21
Murat Şeker

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でデフォルトでネイティブメッセージディスパッチand Qtイベントループを提供するため、キャプチャプロセスにQObjectを使用するのは簡単です。以下は、テスト済みの完全な例です。

キャプチャ、変換、ビューアはすべて独自のスレッドで実行されます。 cv::Matは、アトミックでスレッドセーフなアクセスを持つ暗黙的に共有されるクラスであるため、そのように使用されます。

コンバーターには古いフレームを処理しないオプションがあります-表示目的でのみ変換が行われる場合に役立ちます。

ビューアはGUIスレッドで実行され、古いフレームを正しくドロップします。ビューアが古いフレームを処理する理由は決してありません。

データを収集してディスクに保存する場合は、キャプチャスレッドを高い優先度で実行する必要があります。また、OpenCV APIを検査して、ネイティブカメラデータをディスクにダンプする方法があるかどうかを確認する必要があります。

変換を高速化するには、OpenCVでgpuアクセラレーションクラスを使用できます。

次の例では、コピーに必要な場合を除いて、メモリが再割り当てされないようにします。Captureクラスは、後続の各フレームで再利用される独自のフレームバッファーを維持し、ConverterImageViewerも維持します。

作成された画像データのディープコピーが2つあります(cv::VideoCatprure::readで内部的に行われる処理に加えて)。

  1. ConverterQImageへのコピー。

  2. ImageViewerQImageへのコピー。

両方のコピーは、スレッド間のデカップリングを保証し、参照カウントが1より大きい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と同等です。画像はビューアのユーザープロパティです。着信画像はユーザープロパティにディープコピーされ、メモリの再割り当てを防止します。

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"

これで完全な例は終わりです。注:この回答の以前のリビジョンでは、画像バッファーが不必要に再割り当てされました。

28