Как эффективно отображать видео OpenCV в Qt?
Я снимаю несколько потоков с IP-камер с помощью OpenCV. Когда я пытаюсь отобразить этот поток из окна 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);
}
Проблема выглядит как накладные расходы на копирование QImages непрерывно. Хотя QImage использует неявное совместное использование, когда я сравниваю указатели данных изображений через qDebug()
сообщения, я вижу разные адреса.
1- Есть ли способ встроить окно OpenCV прямо в QWidget?
2- Каков наиболее эффективный способ отображения нескольких видео? Например, как системы управления видео показывают до 32 камер одновременно?
3- Какой должен быть путь?
1 ответ
С помощью 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 по умолчанию обеспечивает отправку собственных сообщений и цикл обработки событий Qt в QThread
прост в использовании QObject
для процесса захвата. Ниже приведен полный, проверенный пример.
Захват, преобразование и просмотрщик все работают в своих собственных потоках. поскольку cv::Mat
является неявно разделяемым классом с атомарным, потокобезопасным доступом, он используется как таковой.
Конвертер имеет опцию не обрабатывать устаревшие кадры - полезно, если преобразование выполняется только для отображения.
Зритель запускается в потоке графического интерфейса и корректно удаляет устаревшие кадры. У зрителя никогда нет причин иметь дело с устаревшими кадрами.
Если вы собираете данные для сохранения на диск, вы должны запустить поток захвата с высоким приоритетом. Вам также следует проверить API-интерфейс OpenCV, чтобы выяснить, есть ли способ выгрузки данных с собственной камеры на диск.
Чтобы ускорить конвертацию, вы можете использовать gpu-ускоренные классы в OpenCV.
Приведенный ниже пример гарантирует, что ни одна память не будет перераспределена, если это не необходимо для копии: Capture
класс поддерживает свой собственный буфер кадров, который повторно используется для каждого последующего кадра, так же как и Converter
и то же самое ImageViewer
,
Есть две глубокие копии данных изображения (кроме того, что происходит внутри cv::VideoCatprure::read
):
Копия к
Converter
"sQImage
,Копия к
ImageViewer
"sQImage
,
Обе копии необходимы для обеспечения разъединения между потоками и предотвращения перераспределения данных из-за необходимости отсоединения cv::Mat
или же QImage
с количеством ссылок выше 1. На современных архитектурах копии памяти очень быстрые.
Поскольку все буферы изображений находятся в одной и той же ячейке памяти, их производительность оптимальна - они остаются в памяти и кэшируются.
AddressTracker
используется для отслеживания перераспределения памяти в целях отладки.
// https://github.com/KubaO/stackrun/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"
На этом завершается полный пример. Примечание. Предыдущая версия этого ответа излишне перераспределяла буферы изображений.