Comment afficher efficacement les vidéos OpenCV dans Qt?

Je capture plusieurs stream de caméras IP avec l’aide d’OpenCV. Lorsque j’essaie d’afficher ces stream depuis une fenêtre OpenCV ( cv::namedWindow(...) ), cela fonctionne sans aucun problème (j’ai essayé jusqu’à 4 stream jusqu’à présent).

Le problème se pose lorsque j’essaie de montrer ces stream dans un widget Qt. Comme la capture est effectuée dans un autre thread, je dois utiliser le mécanisme de slot de signal afin de mettre à jour le QWidget (qui est dans le thread principal).

Fondamentalement, j’émets la nouvelle image capturée à partir du fil de capture et un emplacement dans le fil de l’interface graphique l’attrape. Lorsque j’ouvre 4 stream, 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); } } 

Ceci est ma fente:

 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 la surcharge de copier QImages en continu. Bien que QImage utilise le partage implicite, lorsque je compare les pointeurs de données d’images via des messages qDebug() , je vois des adresses différentes.

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

2- Quel est le moyen le plus efficace de gérer l’affichage de plusieurs vidéos? Par exemple, comment les systèmes de gestion vidéo affichent-ils jusqu’à 32 caméras en même temps?

3- Quel doit être le chemin à parcourir?

L’utilisation de QImage::scanLine force une copie en profondeur. Vous devez donc au minimum utiliser constScanLine ou, mieux encore, modifier la signature de l’emplacement en:

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

Bien entendu, votre problème devient alors autre chose: l’instance de QImage pointe sur les données d’une image qui vit dans un autre thread et qui peut (et va) changer à tout moment.

Il existe une solution pour cela: il faut utiliser de nouvelles images allouées sur le tas, et l’image doit être capturée dans QImage . QScopedPointer est utilisé pour empêcher les memory leaks jusqu’à ce que QImage propriétaire de la trame.

 static void matDeleter(void* mat) { delete static_cast(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 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, étant donné que Qt fournit par défaut la dissortingbution de messages native et une boucle d’événements Qt dans un QThread , il est simple d’utiliser QObject pour le processus de capture. Vous trouverez ci-dessous un exemple complet et testé.

La capture, la conversion et le visualiseur s’exécutent tous dans leurs propres threads. Puisque cv::Mat est une classe implicitement partagée avec un access atomique sans risque de threads, il est utilisé comme tel.

Le convertisseur dispose d’une option permettant de ne pas traiter les images obsolètes – utile si la conversion est uniquement effectuée à des fins d’affichage.

La visionneuse s’exécute dans le fil de l’interface graphique et supprime correctement les images périmées. Il n’y a jamais de raison pour que le spectateur traite des images périmées.

Si vous devez collecter des données à enregistrer sur le disque, vous devez exécuter le thread de capture avec une priorité élevée. Vous devez également inspecter les API OpenCV pour voir s’il existe un moyen de transférer les données natives de la caméra sur disque.

Pour accélérer la conversion, vous pouvez utiliser les classes accélérées par gpu dans OpenCV.

L’exemple ci-dessous permet de s’assurer qu’aucune partie de la mémoire n’est réaffectée, à moins que cela ne soit nécessaire pour une copie: la classe Capture conserve son propre tampon d’images réutilisé pour chaque image suivante, le Converter et ImageViewer .

Il existe deux copies cv::VideoCatprure::read de données d’image (en plus de tout ce qui se passe en interne dans cv::VideoCatprure::read ):

  1. La copie dans la QImage .

  2. La copie dans QImage .

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

Étant donné que tous les tampons d’image restnt dans les mêmes emplacements mémoire, leurs performances sont optimales: ils restnt recherchés et mis en cache.

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

 // https://github.com/KubaO/stackoverflown/tree/master/questions/opencv-21246766 #include  #include  #include  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 ++; } } }; 

La classe Capture remplit le tampon d’image interne avec l’image capturée. Il notifie un changement de cadre. Le cadre est la propriété 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 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); } }; 

La classe Converter convertit le cadre entrant en une propriété utilisateur QImage réduite. Il notifie la mise à jour de l’image. L’image est conservée pour éviter les réaffectations de mémoire. La propriété processAll permet de sélectionner si toutes les images doivent être converties ou si seule la plus récente doit être mise en queue.

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

Le widget ImageViewer est l'équivalent d'un QLabel stockant un pixmap. L'image est la propriété de l'utilisateur de la visionneuse. L'image entrante est copiée en profondeur dans la propriété de l'utilisateur, afin d'éviter toute réaffectation 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()) { setAtsortingbute(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(); 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" 

Ceci conclut l’exemple complet. Remarque: la révision précédente de cette réponse a réalloué de manière inutile les tampons d'image.