Mur*_*ker 19 c++ qt multithreading opencv
我在OpenCV的帮助下从ip摄像头捕获多个流.当我尝试从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);
}
}
Run Code Online (Sandbox Code Playgroud)
这是我的插槽:
void widget::set_image(QImage image) {
img = image;
qDebug() << "PARAMETER IMAGE: " << image.scanLine(0);
qDebug() << "MEMBER IMAGE: " << img.scanLine(0);
}
Run Code Online (Sandbox Code Playgroud)
问题似乎是连续复制QImages的开销.尽管QImage使用隐式共享,但当我通过qDebug()
消息比较图像的数据指针时,我看到了不同的地址.
1-有没有办法将OpenCV窗口直接嵌入到QWidget中?
2-处理显示多个视频的最有效方法是什么?例如,视频管理系统如何同时显示多达32台摄像机?
3-必须走的路是什么?
Rei*_*ica 27
使用QImage::scanLine
强制深层复制,所以至少应该使用constScanLine
,或者更好地将插槽的签名更改为:
void widget::set_image(const QImage & image);
Run Code Online (Sandbox Code Playgroud)
当然,你的问题就变成了别的东西: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);
}
}
Run Code Online (Sandbox Code Playgroud)
当然,由于Qt 在a中默认提供本机消息调度和 Qt事件循环QThread
,因此QObject
用于捕获过程是一件简单的事情.以下是一个完整的测试示例.
捕获,转换和查看器都在自己的线程中运行.由于cv::Mat
是一个隐式共享类,具有原子的,线程安全的访问,因此它就是这样使用的.
转换器可以选择不处理过时帧 - 如果转换仅用于显示目的,则非常有用.
查看器在gui线程中运行并正确地删除过时的帧.观众没有理由处理陈旧的画面.
如果要收集数据以保存到磁盘,则应以高优先级运行捕获线程.您还应该检查OpenCV apis以查看是否有将本机摄像机数据转储到磁盘的方法.
要加快转换速度,可以在OpenCV中使用gpu-accelerated类.
下面的示例确保除非复制需要,否则不会重新分配任何内存:Capture
该类维护自己的帧缓冲区,该缓冲区可以重复用于每个后续帧,因此也是Converter
如此ImageViewer
.
有两个深度的图像数据副本(除了内部发生的任何内容cv::VideoCatprure::read
):
复制到Converter
's 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 ++;
}
}
};
Run Code Online (Sandbox Code Playgroud)
所述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);
}
};
Run Code Online (Sandbox Code Playgroud)
的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);
}
};
Run Code Online (Sandbox Code Playgroud)
该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; }
};
Run Code Online (Sandbox Code Playgroud)
演示实例化上述类,并在专用线程中运行捕获和转换.
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"
Run Code Online (Sandbox Code Playgroud)
以上是完整的示例.注意:此答案的先前版本不必要地重新分配了图像缓冲区.
归档时间: |
|
查看次数: |
19878 次 |
最近记录: |