Comment afficher efficacement la vidéo OpenCV dans Qt?

je capture plusieurs flux depuis des caméras ip avec L'aide D'OpenCV. Quand j'essaie d'afficher ces flux depuis une fenêtre OpenCV (cv::namedWindow(...)), il fonctionne sans problème (j'ai essayé jusqu'à 4 flux jusqu'à présent).

le problème se pose lorsque j'essaie de montrer ces flux à l'intérieur d'un widget Qt. Puisque la capture est faite dans un autre thread, je dois utiliser le mécanisme de slot de signal pour mettre à jour le QWidget(qui est dans le thread principal).

en gros, j'émets le nouveau cadre capturé à partir du fil de capture et une fente dans le fil GUI le prend. Quand j'ouvre 4 flux, Je ne peux pas afficher les vidéos en douceur comme avant.

Voici l'émetteur:

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

C'est mon slot :

void widget::set_image(QImage image) {
    img = image;
    qDebug() << "PARAMETER IMAGE: " << image.scanLine(0);
    qDebug() << "MEMBER IMAGE: " << img.scanLine(0);
}

le problème semble être celui de la copie de QImages en continu. Bien que QImage utilise le partage implicite, quand je compare les pointeurs de données des images via qDebug() messages, je vois des adresses différentes.

1 - y a-t-il un moyen d'intégrer la fenêtre OpenCV directement dans QWidget ?

2 - Quelle est la façon la plus efficace de gérer l'affichage de plusieurs vidéos? Comment, par exemple, les systèmes de gestion vidéo afficher jusqu'à 32 caméras en même temps ?

3-Quelle doit être la voie à suivre ?

19
demandé sur Kuba Ober 2014-01-21 04:14:44

1 réponses

en utilisant QImage::scanLine force une copie profonde, donc au minimum, vous devez utiliser constScanLine, ou, mieux encore, changez la signature de la fente en:

void widget::set_image(const QImage & image);

bien sûr, votre problème devient alors autre chose: l' QImage l'instance indique les données d'un cadre qui vit dans un autre thread, et peut (et va) changer à tout moment.

il y a une solution pour cela: il faut utiliser de nouveaux cadres alloués sur le tas, et le cadre doit être capturé dans QImage. QScopedPointer est utilisé pour empêcher les fuites de mémoire jusqu'à ce que le QImage prend possession du cadre.

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

bien sûr, depuis Qt fournit le natif de message d'envoi et une boucle d'événement Qt par défaut dans un QThread, c'est une question simple à utiliser QObject pour le processus de capture. Voici un exemple complet et testé.

la capture, la conversion et le spectateur fonctionnent tous dans leurs propres threads. Depuis cv::Mat est une classe implicitement partagée avec atomic, thread-safe d'accès, il est utilisé en tant que tel.

le convertisseur a une option de ne pas traiter les cadres périmés-utile si la conversion n'est faite qu'à des fins d'affichage.

le spectateur court dans le fil gui et laisse tomber correctement les cadres périmés. Il n'y a jamais de raison pour que le spectateur s'occupe de montures périmées.

si vous deviez collecter des données pour sauvegarder sur le disque, vous devriez exécuter le thread de capture à haute priorité. Vous devriez également inspecter les API OpenCV pour voir s'il y a un moyen de dumping des données de caméra natives sur le disque.

pour accélérer la conversion, vous pouvez utiliser les classes gpu-accelerated dans OpenCV.

l'exemple ci-dessous permet de s'assurer qu'aucune mémoire n'est réattribuée sauf si nécessaire pour une copie: le Capture class maintient son propre tampon de cadre qui est réutilisé pour chaque cadre suivant, ainsi que le Converter, et le ImageViewer.

il y a deux copies profondes de données d'image faites (en plus de ce qui se passe en interne cv::VideoCatprure::read):

  1. La copie de l' ConverterQImage.

  2. copie ImageViewerQImage.

les deux copies sont nécessaires pour assurer le découplage entre les threads et empêcher la réallocation de données due à la nécessité de détacher un cv::Mat ou QImage dont le nombre de référence est supérieur à 1. Sur les architectures modernes, les copies mémoire sont très rapides.

puisque tous les tampons d'image restent dans le même les emplacements de mémoire, leur performance est optimale - ils restent bipés et mis en cache.

AddressTracker est utilisé pour suivre les réallocations de mémoire à des fins de débogage.

// 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 remplit le tampon interne avec le cadre capturé. Il informe d'un changement de cadre. Le cadre est la propriété de l'utilisateur de la classe.

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 la classe convertit le cadre entrant en unQImage propriété de l'utilisateur. Il informe de la image mise à jour. L'image est conservée pour éviter les réallocations de mémoire. processAll la propriété sélectionne si tous les cadres seront convertis, ou seulement le plus récent devrait plus d'un être mis en file d'attente.

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 widget est l'équivalent d'un QLabel stockage d'un pixmap. L'image est la propriété de l'utilisateur de l'observateur. L'image entrante est profondément copiée dans la propriété de l'utilisateur, pour empêcher les réallocations de mémoire.

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

La démonstration instancie les classes décrites ci-dessus et exécute la capture et la conversion dans des threads dédiés.

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"

Cette conclut l'exemple complet. Note: la révision précédente de cette réponse a inutilement redistribué les tampons d'image.

26
répondu Kuba Ober 2018-07-20 21:49:03