QImage 16 bit grayscale with QQuickPaintedItem - c++

I have unsigned 16 bit image data that I displayed by subclassing QQuickPaintedItem in Qt 5.12.3. I used QImage with Format_RGB32 and scaled the data from [0, 16383] to [0, 255] and set that as the color value for all three R,G,B. Now, I am using Qt 5.15.2 which has a QImage FORMAT_GrayScale16 that I'd like to use but for some reason the image is displayed incorrectly. The code I used to convert my unsigned 16 bit image data to QImage for both formats is shown below. The QQuickPaintedItem is a basic subclassing with drawImage(window(), my_qimage); that I pass the QImage as returned from the code below. Why is the new format not displaying correctly?
Format_RGB32 method
QImage image(image_dim, QImage::Format_RGB32);
unsigned int pixel = 0;
for (uint16_t row = 0; row < nrow; row++) {
uint *scanLine = reinterpret_cast<uint *>(image.scanLine(row));
for (uint16_t col = 0; col < ncols; col++) {
uint16_t value = xray_image.data()[pixel++]; // Get each pixel
unsigned short color_value = uint16_t((float(value) / 16383) * 255.0f); // scale [0, 255]
*scanLine++ = qRgb(int(color_value), int(color_value), int(color_value));
}
}
return image;
Format_Grayscale16 method
QImage image(image_dim, QImage::Format_Grayscale16);
unsigned int pixel = 0;
for (uint16_t row = 0; row < nrow; row++) {
// **EDIT WRONG:** uint *scanLine = reinterpret_cast<uint *>(image.scanLine(row));
uint16_t *scanLine = reinterpret_cast<uint16_t *>(image.scanLine(row));
for (uint16_t col = 0; col < ncols; col++) {
*scanLine++ = xray_image.data()[pixel++]; // Get each pixel
}
}
return image;

This worked for me (below is just fragments):
class Widget : public QWidget
{
private:
QImage m_image;
QImage m_newImage;
QGraphicsScene *m_scene;
QPixmap m_pixmap;
};
...
m_image.load("your/file/path/here");
m_newImage = m_image.convertToFormat(QImage::Format_Grayscale8);
m_pixmap.convertFromImage(m_newImage);
m_scene = new QGraphicsScene(this);
m_scene->addPixmap(m_pixmap);
m_scene->setSceneRect(m_pixmap.rect());
ui->graphicsView->setScene(m_scene);
Based on your OP, you probably want to render it differently. graphicsView is just a QGraphicsView defined in design mode.

Related

When modifying a QImage using scanLine, artifacts show up. Why?

My application has different color modes. Therefore, I need to render my icons in different colors.
My icons are grayscale SVGs. Each color mode defines two colors for icons. One color shall replace black, the other shall replace white.
The code I use to implement this is as follows:
struct Gradient {
QRgb a, b;
/// index in [0, 255]. alpha from mask modifies dest alpha.
QRgb at(int index, int alpha) const {
const int rindex = 255 - index;
return qRgba(
(qRed(a) * rindex + qRed(b) * index) / 255,
(qGreen(a) * rindex + qGreen(b) * index) / 255,
(qBlue(a) * rindex + qBlue(b) * index) / 255,
(((qAlpha(a) * rindex + qAlpha(b) * index) / 255) * alpha) / 255
);
}
};
class Icon {
public:
explicit Icon(const QString &path): path{path} {}
/// must set once before rendering pixmaps.
/// generates a mask image from the source SVG with the given size.
void setSize(QSize size) {
if (mask.size() != size) {
QSvgRenderer renderer(path);
if (renderer.isValid()) {
mask = QImage(size, QImage::Format_ARGB32_Premultiplied);
mask.fill(Qt::transparent);
QPainter svgPainter(&mask);
renderer.render(&svgPainter);
}
}
}
/// renders a pixmap that uses primary where the mask is black,
/// secondary where the mask is white. Mask defines transparency.
QPixmap paint(const QColor &primary, const QColor &secondary) const {
QImage buffer(mask.size(), QImage::Format_ARGB32_Premultiplied);
Gradient gradient{primary.rgb(), secondary.rgb()};
for (int y = 0; y < mask.height(); ++y) {
const QRgb *srcLine = reinterpret_cast<const QRgb*>(mask.constScanLine(y));
QRgb *destLine = reinterpret_cast<QRgb*>(buffer.scanLine(y));
for (int x = 0; x < mask.width(); ++x) {
const QRgb &src = srcLine[x];
// using red as indicator for the grayscale color.
destLine[x] = gradient.at(qRed(src), qAlpha(src));
}
}
QPixmap pixmap;
pixmap.convertFromImage(buffer);
return pixmap;
}
bool isNull() const {
return mask.isNull();
}
private:
QString path;
QImage mask;
};
This code generates strange artifacts in the output:
From this input:
This is rendered with primary and secondary colors both #657590. The dark blue is the backing widget's background color. The original icon just has the cog outline.
Why are the additional rectangular parts created? They are not part of the source image. I tried to use buffer.setPixel instead of scanLine, but that produced the same output.
The problem was the image format QImage::Format_ARGB32_Premultiplied. The way I modified the image, QImage::Format_ARGB32 was appropriate. Using that format solved the problem.

Setting pixel color of 8-bit grayscale image using pointer

I have this code:
QImage grayImage = image.convertToFormat(QImage::Format_Grayscale8);
int size = grayImage.width() * grayImage.height();
QRgb *data = new QRgb[size];
memmove(data, grayImage.constBits(), size * sizeof(QRgb));
QRgb *ptr = data;
QRgb *end = ptr + size;
for (; ptr < end; ++ptr) {
int gray = qGray(*ptr);
}
delete[] data;
It is based on this: https://stackoverflow.com/a/40740985/8257882
How can I set the color of a pixel using that pointer?
In addition, using qGray() and loading a "bigger" image seem to crash this.
This works:
int width = image.width();
int height = image.height();
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
image.setPixel(x, y, qRgba(0, 0, 0, 255));
}
}
But it is slow when compared to explicitly manipulating the image data.
Edit
Ok, I have this code now:
for (int y = 0; y < height; ++y) {
uchar *line = grayImage.scanLine(y);
for (int x = 0; x < width; ++x) {
int gray = qGray(line[x]);
*(line + x) = uchar(gray);
qInfo() << gray;
}
}
And it seems to work. However, when I use an image that has only black and white colors and print the gray value, black color gives me 0 and white gives 39. How can I get the gray value in a range of 0-255?
First of all you are copying too much data in this line:
memmove(data, grayImage.constBits(), size * sizeof(QRgb));
The size ob Qrgb is 4 bytes, but according to the documentation, the size of a Format_Grayscale8 pixel is only 8 bits or 1 byte. If you remove sizeof(QRgb) you should be copying the correct amount of bytes, assuming all the lines in the bitmap are consecutive (which, according to the documentation, they are not -- they are aligned to at minimum 32-bits, so you would have to account for that in size). The array data should not be of type Qrgb[size] but ucahr[size]. You can then modify data as you like. Finally, you will probably have to create a new QImage with one of the constructors that accept image bits as uchar and assign the new image to the old image:
auto newImage = QImage( data, image.width(), image.height(), QImage::Format_Grayscale8, ...);
grayImage = std::move( newImage );
But instead of copying image data, you could probably just modify grayImage directly by accessing its data through bits(), or even better, through scanLine(), maybe something like this:
int line, column;
auto pLine = grayImage.scanLine(line);
*(pLine + column) = uchar(grayValue);
EDIT:
According to scanLine documentation, the image is at least 32-bit aligned. So if your 8-bit grayScale image is 3 pixels wide, a new scan line will start every 4 bytes. If you have a 3x3 image, the total size of the memory required to hold the image pixels will be 12. The following code shows the required memory size:
int main() {
auto image = QImage(3, 3, QImage::Format_Grayscale8);
std::cout << image.bytesPerLine() * image.height() << "\n";
return 0;
}
The fill method (setting all gray values to 0xC0) could be implemented like this:
auto image = QImage(3, 3, QImage::Format_Grayscale8);
uchar gray = 0xc0;
for ( int i = 0; i < image.height(); ++i ) {
auto pLine = image.scanLine( i );
for ( int j = 0; j < image.width(); ++j )
*pLine++ = gray;
}

Convert a QImage to grayscale

I have a QImage and I need to convert it to grayscale, then later paint over that with colors. I found an allGray() and isGrayScale() function to check if an image is already grayscale, but no toGrayScale() or similarly-named function.
Right now I'm using this code, but it's does not have a very good performance:
for (int ii = 0; ii < image.width(); ii++) {
for (int jj = 0; jj < image.height(); jj++) {
int gray = qGray(image.pixel(ii, jj));
image.setPixel(ii, jj, QColor(gray, gray, gray).rgb());
}
}
What would be the best way, performance-wise, to convert a QImage to grayscale?
Since Qt 5.5, you can call QImage::convertToFormat() to convert a QImage to grayscale as follows:
QImage image = ...;
image.convertToFormat(QImage::Format_Grayscale8);
Rather than using the slow functions QImage::pixel and QImage::setPixel, use
QImage::scanline to access the data. Pixels on a scan (horizontal line ) are consecutive. Assuming you have a 32 bpp image, you can use QRgb to iterate over the scan. Finally always put the x coordinate in the inner loop. Which gives :
for (int ii = 0; ii < image.height(); ii++) {
uchar* scan = image.scanLine(ii);
int depth =4;
for (int jj = 0; jj < image.width(); jj++) {
QRgb* rgbpixel = reinterpret_cast<QRgb*>(scan + jj*depth);
int gray = qGray(*rgbpixel);
*rgbpixel = QColor(gray, gray, gray).rgba();
}
}
A quick test with an 3585 x 2386 image gave
********* Start testing of TestImage *********
Config: Using QTest library 4.7.4, Qt 4.7.4
PASS : TestImage::initTestCase()
RESULT : TestImage::grayscaleOp():
390 msecs per iteration (total: 390, iterations: 1)
PASS : TestImage::grayscaleOp()
RESULT : TestImage::grayscaleFast():
125 msecs per iteration (total: 125, iterations: 1)
PASS : TestImage::grayscaleFast()
PASS : TestImage::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of TestImage *********
Source code:
testimage.h file:
#ifndef TESTIMAGE_H
#define TESTIMAGE_H
#include <QtTest/QtTest>
#include <QObject>
#include <QImage>
class TestImage : public QObject
{
Q_OBJECT
public:
explicit TestImage(QObject *parent = 0);
signals:
private slots:
void grayscaleOp();
void grayscaleFast();
private:
QImage imgop;
QImage imgfast;
};
#endif // TESTIMAGE_H
testimage.cpp file:
#include "testimage.h"
TestImage::TestImage(QObject *parent)
: QObject(parent)
, imgop("path_to_test_image.png")
, imgfast("path_to_test_image.png")
{
}
void TestImage::grayscaleOp()
{
QBENCHMARK
{
QImage& image = imgop;
for (int ii = 0; ii < image.width(); ii++) {
for (int jj = 0; jj < image.height(); jj++) {
int gray = qGray(image.pixel(ii, jj));
image.setPixel(ii, jj, QColor(gray, gray, gray).rgb());
}
}
}
}
void TestImage::grayscaleFast()
{
QBENCHMARK {
QImage& image = imgfast;
for (int ii = 0; ii < image.height(); ii++) {
uchar* scan = image.scanLine(ii);
int depth =4;
for (int jj = 0; jj < image.width(); jj++) {
QRgb* rgbpixel = reinterpret_cast<QRgb*>(scan + jj*depth);
int gray = qGray(*rgbpixel);
*rgbpixel = QColor(gray, gray, gray).rgba();
}
}
}
}
QTEST_MAIN(TestImage)
pro file:
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = QImageTest
TEMPLATE = app
CONFIG += qtestlib
SOURCES += testimage.cpp
HEADERS += testimage.h
Important note:
You already get an important performance boost just by inverting the loops. In this test case it was ~90ms.
You may use other libraries like opencv to make the grayscale conversion and then build the Qimage from an opencv buffer. I expect an even better performance improvement.
I'll post a slightly modified version of #UmNyobe's code. I just increment a pointer for the scan lines instead of calculating each pixel via an index.
// We assume the format to be RGB32!!!
Q_ASSERT(image.format() == QImage::Format_RGB32);
for (int ii = 0; ii < image.height(); ii++) {
QRgb *pixel = reinterpret_cast<QRgb*>(image.scanLine(ii));
QRgb *end = pixel + image.width();
for (; pixel != end; pixel++) {
int gray = qGray(*pixel);
*pixel = QColor(gray, gray, gray).rgb();
}
}
Internal qt class QPixmapColorizeFilter uses function grayscale that solves similar topic.
I derived following function from it, that should solve the problem.
Important part is converting image to 32-bit format, so you can consider each pixel as 32-bit value and you do not need to concern about bit alignment.
You can also use bits function directly and iterate over all pixels instead of iterating over lines and columns. With this trick you avoid multiplication performed in scanLine function.
QImage convertToGrayScale(const QImage &srcImage) {
// Convert to 32bit pixel format
QImage dstImage = srcImage.convertToFormat(srcImage.hasAlphaChannel() ?
QImage::Format_ARGB32 : QImage::Format_RGB32);
unsigned int *data = (unsigned int*)dstImage.bits();
int pixelCount = dstImage.width() * dstImage.height();
// Convert each pixel to grayscale
for(int i = 0; i < pixelCount; ++i) {
int val = qGray(*data);
*data = qRgba(val, val, val, qAlpha(*data));
++data;
}
return dstImage;
}

cv::Mat to QImage strange behavior

I'm using the code suggested in ( how to convert an opencv cv::Mat to qimage ) to display a cv::Mat in my Qt application. However, I'm getting strange results. The black parts are displayed as black, but all other values are inverted.
Conversion code:
QImage ImgConvert::matToQImage(Mat_<double> src)
{
double scale = 255.0;
QImage dest(src.cols, src.rows, QImage::Format_ARGB32);
for (int y = 0; y < src.rows; ++y) {
const double *srcrow = src[y];
QRgb *destrow = (QRgb*)dest.scanLine(y);
for (int x = 0; x < src.cols; ++x) {
unsigned int color = srcrow[x] * scale;
destrow[x] = qRgba(color, color, color, 255);
}
}
return dest;
}
Display code:
void MainWindow::redraw()
{
static QImage image = ImgConvert::matToQImage(im);
static QGraphicsPixmapItem item( QPixmap::fromImage(image));
static QGraphicsScene* scene = new QGraphicsScene;
scene->addItem(&item);
ui->graphicsView->setScene(scene);
ui->graphicsView->repaint();
}
Right now I'm using if(color>0) color = 255-color; to correct for this effect, but I'd much rather understand where it's coming from.
Also, a second mini-question: if I remove the static declarations in redraw(), the image gets removed from memory immediately when the method exits. Is this the best way to fix this, and am I going to have any unintended side effects if I display multiple frames?
I don't know. Setting an array first for me sounds like a cleaner way, see https://stackoverflow.com/a/3387400/1705967 , that could give you ideas.
Although I also use Ypnos's solution with a great success on color images. :)
Ah, and as for the second question, don't worry about the QPixmap. It makes the image data private (clones when necessary) as I have experienced so you won't overwrite it by mistake.
In case anyone is having this problem, I quickly and dirtily fixed it by subtracting the pixel value to 256:
QImage ImgConvert::matToQImage(Mat_<double> src)
{
double scale = 255.0;
QImage dest(src.cols, src.rows, QImage::Format_ARGB32);
for (int y = 0; y < src.rows; ++y) {
const double *srcrow = src[y];
QRgb *destrow = (QRgb*)dest.scanLine(y);
for (int x = 0; x < src.cols; ++x) {
unsigned int color = 256 - (srcrow[x] * scale);
destrow[x] = qRgba(color, color, color, 255);
}
}
return dest;
}
This will slightly corrupt the image, though, modifying by 1 its bright. My purpose was visualizing so the difference was negligible to the eye, however for certain applications in image processing this corruption might be critical. I could not find why was this happening and as I was in a hurry I did not look any further.

Convert Leptonica Pix Object to QPixmap ( or other image object )

I'm using the Leptonica Library to process some pictures. After that I want to show them in my QT GUI. Leptonica is using their own format Pix for the images, while QT is using their own format QPixmap. At the moment the only way for me is to save the pictures after processing as a file ( like bmp ) and then load them again with a QT function call. Now I want to convert them in my code, so I dont need to take the detour with saving them on the filesystem. Any ideas how to do this?
Best Regards
// edit:
Okay as already suggested I tried to convert the PIX* to a QImage.
The PIX* is defined like this:
http://tpgit.github.com/Leptonica/pix_8h_source.html
struct Pix
{
l_uint32 w; /* width in pixels */
l_uint32 h; /* height in pixels */
l_uint32 d; /* depth in bits */
l_uint32 wpl; /* 32-bit words/line */
l_uint32 refcount; /* reference count (1 if no clones) */
l_int32 xres; /* image res (ppi) in x direction */
/* (use 0 if unknown) */
l_int32 yres; /* image res (ppi) in y direction */
/* (use 0 if unknown) */
l_int32 informat; /* input file format, IFF_* */
char *text; /* text string associated with pix */
struct PixColormap *colormap; /* colormap (may be null) */
l_uint32 *data; /* the image data */
};
while QImage offers me a method like this:
http://developer.qt.nokia.com/doc/qt-4.8/qimage.html#QImage-7
QImage ( const uchar * data,
int width,
int height,
int bytesPerLine,
Format format )
I assume I cant just copy the data from the PIX to the QImage when calling the constructor. I guess I need to fill the QImage Pixel by Pixel, but actually I dont know how? Do I need to loop through all the coordinates? How do I regard the bit depth? Any ideas here?
I use this for conversion QImage to PIX:
PIX* TessTools::qImage2PIX(QImage& qImage) {
PIX * pixs;
l_uint32 *lines;
qImage = qImage.rgbSwapped();
int width = qImage.width();
int height = qImage.height();
int depth = qImage.depth();
int wpl = qImage.bytesPerLine() / 4;
pixs = pixCreate(width, height, depth);
pixSetWpl(pixs, wpl);
pixSetColormap(pixs, NULL);
l_uint32 *datas = pixs->data;
for (int y = 0; y < height; y++) {
lines = datas + y * wpl;
QByteArray a((const char*)qImage.scanLine(y), qImage.bytesPerLine());
for (int j = 0; j < a.size(); j++) {
*((l_uint8 *)lines + j) = a[j];
}
}
return pixEndianByteSwapNew(pixs);
}
And this for conversion PIX to QImage:
QImage TessTools::PIX2QImage(PIX *pixImage) {
int width = pixGetWidth(pixImage);
int height = pixGetHeight(pixImage);
int depth = pixGetDepth(pixImage);
int bytesPerLine = pixGetWpl(pixImage) * 4;
l_uint32 * s_data = pixGetData(pixEndianByteSwapNew(pixImage));
QImage::Format format;
if (depth == 1)
format = QImage::Format_Mono;
else if (depth == 8)
format = QImage::Format_Indexed8;
else
format = QImage::Format_RGB32;
QImage result((uchar*)s_data, width, height, bytesPerLine, format);
// Handle pallete
QVector<QRgb> _bwCT;
_bwCT.append(qRgb(255,255,255));
_bwCT.append(qRgb(0,0,0));
QVector<QRgb> _grayscaleCT(256);
for (int i = 0; i < 256; i++) {
_grayscaleCT.append(qRgb(i, i, i));
}
if (depth == 1) {
result.setColorTable(_bwCT);
} else if (depth == 8) {
result.setColorTable(_grayscaleCT);
} else {
result.setColorTable(_grayscaleCT);
}
if (result.isNull()) {
static QImage none(0,0,QImage::Format_Invalid);
qDebug() << "***Invalid format!!!";
return none;
}
return result.rgbSwapped();
}
This code accepts a const QImage& parameter.
static PIX* makePIXFromQImage(const QImage &image)
{
QByteArray ba;
QBuffer buf(&ba);
buf.open(QIODevice::WriteOnly);
image.save(&buf, "BMP");
return pixReadMemBmp(ba.constData(), ba.size());
}
I do not know the Leptonica Library, but I had a short look at the documentation and found the documentation about the PIX structure. You can create a QImage from the raw data and convert this to a QPixmap with convertFromImage.
Well I could solve the problem this way:
Leptonica offers a function
l_int32 pixWriteMemBmp (l_uint8 **pdata, size_t *psize, PIX *pix)
With this function you can write into the memory instead of a filestream. Still ( in this example ) the Bmp Header and format persists ( there are the same functions for other image formats too ).
The corresponding function from QT is this one:
bool QImage::loadFromData ( const uchar * data, int len, const char * format = 0 )
Since the the Header persits I just need to pass the data ptr and the size to the loadFromData function and QT does the rest.
So all together it would be like this:
PIX *m_pix;
FILE * pFile;
pFile = fopen( "PathToFile", "r" );
m_pix = pixReadStreamBmp(pFile); // If other file format use the according function
fclose(pFile);
// Now we have a Pix object from leptonica
l_uint8* ptr_memory;
size_t len;
pixWriteMemBmp(&ptr_memory, &size, m_pix);
// Now we have the picture somewhere in the memory
QImage testimage;
QPixmap pixmap;
testimage.loadFromData((uchar *)ptr_memory,len);
pixmap.convertFromImage(testimage);
// Now we have the image as a pixmap in Qt
This actually works for me, tho I don't know if there is a way to do this backwards so easy. ( If there is, please let me know )
Best Regards
You can save your pixmap to RAM instead of file (use QByteArray to store the data, and QBuffer as your I/O device).