Ответ 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
является неявно разделяемым классом с атомарным, потокобезопасным доступом, он используется как таковой.
Конвертер имеет опцию не обрабатывать устаревшие кадры - полезно, если преобразование выполняется только для отображения.
Зритель работает в потоке графического интерфейса и корректно удаляет устаревшие кадры. У зрителя никогда нет причин иметь дело с устаревшими кадрами.
Если вы собираете данные для сохранения на диск, вы должны запустить поток захвата с высоким приоритетом. Вам также следует проверить OpenCV API-интерфейс, чтобы узнать, есть ли способ выгрузки данных с собственной камеры на диск.
Чтобы ускорить конвертацию, вы можете использовать gpu-ускоренные классы в OpenCV.
Приведенный ниже пример гарантирует, что ни одна из памяти не будет перераспределена, за исключением случаев, когда это необходимо для копии: класс Capture
поддерживает свой собственный буфер кадров, который повторно используется для каждого последующего кадра, как и Converter
, и то же самое делает ImageViewer
.
Сделано две глубоких копии данных изображения (помимо всего, что происходит внутри cv::VideoCatprure::read
):
Копия в
Converter
QImage
.Копия в
ImageViewer
QImage
.
Обе копии необходимы для обеспечения разъединения между потоками и предотвращения перераспределения данных из-за необходимости отсоединить cv::Mat
или QImage
, у которых счетчик ссылок больше 1. В современных архитектурах копии памяти очень быстрые.
Поскольку все буферы изображений находятся в одной и той же ячейке памяти, их производительность оптимальна - они остаются в памяти и кэшируются.
AddressTracker
используется для отслеживания перераспределения памяти в целях отладки.
// 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 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"
На этом завершается полный пример. Примечание. Предыдущая версия этого ответа излишне перераспределяла буферы изображений.