¿Cómo mostrar eficientemente el video OpenCV en Qt?

Estoy capturando varias transmisiones de cámaras IP con la ayuda de OpenCV. Cuando trato de mostrar estas transmisiones desde una ventana de OpenCV ( cv::namedWindow(...) ), funciona sin ningún problema (hasta ahora he probado hasta 4 secuencias).

El problema surge cuando bash mostrar estas transmisiones dentro de un widget Qt. Dado que la captura se realiza en otro hilo, tengo que usar el mecanismo de ranura de señal para actualizar el QWidget (que está en el hilo principal).

Básicamente, emito el marco recién capturado del hilo de captura y lo atrapa una ranura en el hilo de la GUI. Cuando abro 4 transmisiones, no puedo mostrar los videos sin problemas como antes.

Aquí está el emisor:

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

Esta es mi ranura:

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

El problema parece ser la sobrecarga de copiar QImages continuamente. Aunque QImage utiliza el uso compartido implícito, cuando comparo los punteros de datos de imágenes a través de mensajes qDebug() , veo diferentes direcciones.

1- ¿Hay alguna forma de incrustar la ventana de OpenCV directamente en QWidget?

2- ¿Cuál es la forma más eficiente de manejar la visualización de videos múltiples? Por ejemplo, ¿cómo los sistemas de administración de video muestran hasta 32 cámaras al mismo tiempo?

3- ¿Cuál debe ser el camino a seguir?

El uso de QImage::scanLine fuerza una copia profunda, por lo que, como mínimo, debe usar constScanLine o, mejor aún, cambiar la firma de la ranura a:

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

Por supuesto, su problema se convierte en algo más: la instancia de QImage apunta a los datos de un marco que vive en otro hilo, y puede (y lo hará) cambiar en cualquier momento.

Hay una solución para eso: uno necesita utilizar cuadros nuevos asignados en el montón, y el cuadro debe capturarse dentro de QImage . QScopedPointer se usa para evitar memory leaks hasta que QImage tome posesión del marco.

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

Por supuesto, dado que Qt proporciona el envío de mensajes nativos y un bucle de evento Qt por defecto en un QThread , es una cuestión simple usar QObject para el proceso de captura. A continuación se muestra un ejemplo completo y probado.

La captura, la conversión y el visor se ejecutan en sus propios hilos. Como cv::Mat es una clase implícitamente compartida con acceso atómico y seguro para subprocesos, se usa como tal.

El convertidor tiene la opción de no procesar marcos obsoletos; es útil si la conversión solo se realiza con fines de visualización.

El visor se ejecuta en el hilo de la interfaz gráfica de usuario y deja caer correctamente los marcos obsoletos. Nunca hay una razón para que el espectador se ocupe de los marcos obsoletos.

Si tuviera que recostackr datos para guardar en el disco, debe ejecutar el hilo de captura con alta prioridad. También debe inspeccionar OpenCV apis para ver si hay una forma de volcar los datos nativos de la cámara al disco.

Para acelerar la conversión, puede usar las clases aceleradas por GPU en OpenCV.

El siguiente ejemplo asegura que en ninguna parte de la memoria se reasigne a menos que sea necesario para una copia: la clase Capture mantiene su propio búfer de cuadros que se reutiliza para cada fotogtwig siguiente, también lo hace el Converter , y también lo hace el ImageViewer .

Hay dos copias profundas de datos de imágenes realizadas (además de lo que ocurra internamente en cv::VideoCatprure::read ):

  1. La copia al QImage del QImage .

  2. La copia a QImage ImageViewer .

Ambas copias son necesarias para asegurar el desacoplamiento entre los hilos y evitar la reasignación de datos debido a la necesidad de separar un cv::Mat o QImage que tenga el recuento de referencias superior a 1. En las architectures modernas, las copias de memoria son muy rápidas.

Dado que todos los almacenamientos intermedios de imágenes permanecen en las mismas ubicaciones de memoria, su rendimiento es óptimo: permanecen localizados y almacenados en caché.

El AddressTracker se utiliza para rastrear reasignaciones de memoria para fines de depuración.

 // 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 clase Capture llena el búfer de ttwig interno con el fotogtwig capturado. Notifica un cambio de marco. El marco es propiedad del usuario de la clase.

 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 clase Converter convierte el marco entrante a una propiedad de usuario de QImage reducida. Notifica de la actualización de la imagen. La imagen se conserva para evitar reasignaciones de memoria. La propiedad processAll selecciona si todos los fotogtwigs se convertirán, o solo el más reciente debería ponerse en cola más de uno.

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

El widget ImageViewer es el equivalente de un QLabel almacena un mapa de píxeles. La imagen es propiedad del usuario del espectador. La imagen entrante se copia profundamente en la propiedad del usuario, para evitar reasignaciones de memoria.

 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 demostración ejemplifica las clases descritas anteriormente y ejecuta la captura y conversión en hilos dedicados.

 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" 

Esto concluye el ejemplo completo. Nota: La revisión previa de esta respuesta reasignó innecesariamente los almacenamientos intermedios de imagen.