c++ multithreading qt opencv

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



multithreading (1)

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 intento 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 fugas de memoria hasta que QImage tome posesión del marco.

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

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 recopilar 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.

// https://github.com/KubaO/n/tree/master/questions/opencv-21246766 #include <QtWidgets> #include <opencv2/opencv.hpp> Q_DECLARE_METATYPE(cv::Mat) class Capture : public QObject { Q_OBJECT QBasicTimer m_timer; QScopedPointer<cv::VideoCapture> m_videoCapture; public: Capture(QObject * parent = {}) : QObject(parent) {} 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 matReady(const cv::Mat &); private: void timerEvent(QTimerEvent * ev) { if (ev->timerId() != m_timer.timerId()) return; cv::Mat frame; if (!m_videoCapture->read(frame)) { // Blocks until a new frame is ready m_timer.stop(); return; } emit matReady(frame); } }; class Converter : public QObject { Q_OBJECT QBasicTimer m_timer; cv::Mat m_frame; bool m_processAll = true; static void matDeleter(void* mat) { delete static_cast<cv::Mat*>(mat); } 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(cv::Mat frame) { cv::resize(frame, frame, cv::Size(), 0.3, 0.3, cv::INTER_AREA); cv::cvtColor(frame, frame, CV_BGR2RGB); const QImage image(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888, &matDeleter, new cv::Mat(frame)); Q_ASSERT(image.constBits() == frame.data); emit imageReady(image); } void timerEvent(QTimerEvent * ev) { if (ev->timerId() != m_timer.timerId()) return; process(m_frame); m_frame.release(); m_timer.stop(); } public: explicit Converter(QObject * parent = nullptr) : QObject(parent) {} void setProcessAll(bool all) { m_processAll = all; } Q_SIGNAL void imageReady(const QImage &); Q_SLOT void processFrame(const cv::Mat & frame) { if (m_processAll) process(frame); else queue(frame); } }; class ImageViewer : public QWidget { Q_OBJECT QImage m_img; void paintEvent(QPaintEvent *) { QPainter p(this); p.drawImage(0, 0, m_img); m_img = {}; } public: ImageViewer(QWidget * parent = nullptr) : QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); } Q_SLOT void setImage(const QImage & img) { if (!m_img.isNull()) qDebug() << "Viewer dropped frame!"; m_img = img; if (m_img.size() != size()) setFixedSize(m_img.size()); update(); } }; 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::matReady, &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"