The callback is invoked from an arbitrary thread. Thus the other method calls must be made thread-safe. A simple way to do it is to emit a signal with the image. See this question for other approaches.
But you're also copying the image data unnecessarily. The callback has full control over image lifetime - the sensor_msgs::ImageConstPtr
is a shared pointer, after all. Thus - pass the ImageConstPtr
all the way to the target thread, then the QImage
becomes a thin wrapper over the Image
class, and doesn't copy its data unless a BGR-TO-RGB format conversion is necessary.
There's no need for cvBridge
at all -- you're not using OpenCV, after all.
Let's start with a minimal reimplementation of ROS that will allow us to try things out on a desktop platform without installing ROS :)
// https://github.com/KubaO/stackoverflown/tree/master/questions/qimage-ros-50262348
#include <QtWidgets>
#include <memory>
#include <string>
#include <vector>
// Minimal reimplementation of ROS
#define ROS_ERROR qFatal
namespace sensor_msgs {
namespace image_encodings {
const std::string MONO8{"mono8"}, BGR8{"bgr8"}, BGRA8{"bgra8"}, RGB8{"rgb8"}, RGBA8{"rgba8"};
} // image_encodings
struct Image {
std::vector<quint8> data;
std::string encoding;
uint32_t height;
uint32_t width;
};
using ImagePtr = std::shared_ptr<Image>;
using ImageConstPtr = std::shared_ptr<const Image>;
} // sensor_msgs
namespace ros {
struct Subscriber {};
struct NodeHandle {
template<class M, class T>
Subscriber subscribe(const std::string &, uint32_t, void(T::*fun)(M), T *obj) {
struct Thread : QThread {
Thread(QObject*p):QThread(p){} ~Thread() override { quit(); wait(); } };
static QPointer<Thread> thread = new Thread(qApp);
thread->start(); // no-op if already started
auto *timer = new QTimer;
timer->start(1000/60);
timer->moveToThread(thread);
QObject::connect(timer, &QTimer::timeout, [obj, fun]{
auto const msec = QTime::currentTime().msecsSinceStartOfDay();
QImage img{256, 256, QImage::Format_ARGB32_Premultiplied};
img.fill(Qt::white);
QPainter p{&img};
constexpr int period = 3000;
p.scale(img.width()/2.0, img.height()/2.0);
p.translate(1.0, 1.0);
p.rotate((msec % period) * 360.0/period);
p.setPen({Qt::darkBlue, 0.1});
p.drawLine(QLineF{{-1., 0.}, {1., 0.}});
p.end();
img = std::move(img).convertToFormat(QImage::Format_RGB888).rgbSwapped();
sensor_msgs::ImageConstPtr ptr{new sensor_msgs::Image{
{img.constBits(), img.constBits() + img.sizeInBytes()},
sensor_msgs::image_encodings::BGR8,
(uint32_t)img.height(), (uint32_t)img.width()}};
(*obj.*fun)(ptr);
});
return {};
}
};
void init(int &, char **, const std::string &) {}
} // ros
The callbacks are invoked from a worker thread, as would happen in ROS.
For demonstration purposes, we can make the main window a QLabel
. We need to pass the ImageConstPtr
to the main thread, where it will be wrapped in a QImage
and set on the label. The signal itself can be the callback. Thus:
// Interface
class MainWindow : public QLabel {
Q_OBJECT
public:
MainWindow(int argc, char** argv, QWidget *parent = {});
protected:
Q_SLOT void setImageMsg(const sensor_msgs::ImageConstPtr&);
Q_SIGNAL void newImageMsg(const sensor_msgs::ImageConstPtr&);
private:
ros::Subscriber sub;
};
Q_DECLARE_METATYPE(sensor_msgs::ImageConstPtr)
First, we need a way to wrap the ImageConstPtr
in a QImage
. The QImage
does not copy the data from the msg
unless a format conversion is necessary. The image must be consumed while the msg
is kept alive. The std::move(image).conversion()
is an idiom for modifying the image in-place. Modern Qt supports this optimization.
// Implementation
static QImage toImageShare(const sensor_msgs::ImageConstPtr &msg) {
using namespace sensor_msgs::image_encodings;
QImage::Format format = {};
if (msg->encoding == RGB8 || msg->encoding == BGR8)
format = QImage::Format_RGB888;
else if (msg->encoding == RGBA8 || msg->encoding == BGRA8)
format = QImage::Format_RGBA8888_Premultiplied;
else if (msg->encoding == MONO8)
format = QImage::Format_Grayscale8;
else
return {};
QImage img(msg->data.data(), msg->width, msg->height, format);
if (msg->encoding == BGR8 || msg->encoding == BGRA8)
img = std::move(img).rgbSwapped();
return img;
}
The implementation of the MainWindow
and the rest of the demo harness is then quite straightforward:
MainWindow::MainWindow(int argc, char** argv, QWidget *parent) : QLabel(parent) {
qRegisterMetaType<sensor_msgs::ImageConstPtr>();
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
connect(this, &MainWindow::newImageMsg, this, &MainWindow::setImageMsg);
#else
connect(this, SIGNAL(newImageMsg(sensor_msgs::ImageConstPtr)), SLOT(setImageMsg(sensor_msgs::ImageConstPtr)));
#endif
ros::init(argc,argv,"MainWindow");
ros::NodeHandle n;
sub = n.subscribe("/usb_cam/image_raw", 1, &MainWindow::newImageMsg, this);
}
void MainWindow::setImageMsg(const sensor_msgs::ImageConstPtr &msg) {
auto img = toImageShare(msg);
auto pix = QPixmap::fromImage(std::move(img));
setPixmap(pix);
resize(pix.size());
}
int main(int argc, char *argv[])
{
QApplication app{argc, argv};
MainWindow w{argc, argv};
w.show();
return app.exec();
}
#include "main.moc"
This concludes the example.