1
votes

I have an 16 bit image of width ("imagewidth") and height ("imageheight"). The data is currently stored in an unsigned short int array of length ("imagewidth"*"imageheight")

I want to create an 16 bit grayscale QImage (using Qt 5.14) from my dataset which is called "data".

This is the code that I am using:

QImage image = Qimage(imagewidth,imageheight,QImage::Format_Grayscale16);

for(int i=0;i<imagewidth;i++)
{
   for(int j=0;j<imageheight;j++)
   {

    uint pixelval = data[i+j*imagewidth];
    QRgb color = qRgb(pixelval, pixelval, pixelval);
    image.setPixel(i,j, color);

   }
}

The code is working and I am getting an image but I only get values in increments of 255... So 0, 255, ...

How I can set the actual pixel value for each pixel from 0 to 65535 ?

2
Is this your real code? Both outer and inner loops are the same: for(int i=0;i<imagewidth;i++).G.M.
No this is not my real code. I changed i to j in the inner loop.jfrans1
Then why does j have the range [0, imagewidth) rather than [0, imageheight)? Please provide some real code -- preferably a minimal reproducible example.G.M.
I am sorry. This is really sloppy. I have updated the question. However, this has nothing to do with my original question. Any thoughts on this ?jfrans1

2 Answers

2
votes

QRgba64 is the right choice for 16 bit (per component) colors.

Another option is to retrieve color with QImage::pixelColor() (and setting with QImage::setPixelColor()) which should be more or less depth agnostic.

Function qRgb() is a bad choice because it deals with 8 bit (per component) colors by intention.

From Qt doc.:

QRgb QColor::qRgb(int r, int g, int b)

Returns the ARGB quadruplet (255, r, g, b).

The value 255 for alpha gives a first hint but inspecting the result type QRgb makes this obvious:

typedef QColor::QRgb

An ARGB quadruplet on the format #AARRGGBB, equivalent to an unsigned int.

A look into source code on woboq.org supports this:

inline Q_DECL_CONSTEXPR QRgb qRgb(int r, int g, int b)// set RGB value
{ return (0xffu << 24) | ((r & 0xffu) << 16) | ((g & 0xffu) << 8) | (b & 0xffu); }

A small sample to illustrate this:

#include <QtWidgets>

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  qDebug() << "qRgb(1 << 12, 1 << 12, 1 << 12):"
    << hex << qRgb(1 << 12, 1 << 12, 1 << 12);
  qDebug() << "QColor(qRgb(1 << 12, 1 << 12, 1 << 12)):"
    << QColor(qRgb(1 << 12, 1 << 12, 1 << 12));
  qDebug() << "QColor().fromRgba64(1 << 12, 1 << 12, 1 << 12):"
    << QColor().fromRgba64(1 << 12, 1 << 12, 1 << 12);
  // done
  return 0;
}

Output:

Qt Version: 5.11.2
qRgb(1 << 12, 1 << 12, 1 << 12): ff000000
QColor(qRgb(1 << 12, 1 << 12, 1 << 12)): QColor(ARGB 1, 0, 0, 0)
QColor().fromRgba64(1 << 12, 1 << 12, 1 << 12): QColor(ARGB 1, 0.062501, 0.062501, 0.062501)

An alternative to using QColor and companions is to write the data values directly into image:

QImage image = Qimage(imagewidth, imageheight, QImage::Format_Grayscale16);
for (int j = 0; j < imageheight; ++j) {
  quint16 *dst = (quint16*)(image.bits() + j * image.bytesPerLine());
  for (int i = 0; i < imagewidth; ++i) {
    dst[i] = data[i + j * imagewidth];
  }
}

This is surely faster and more accurate than converting data value to a color which is converted to a gray level again.

Please note, that I swapped the loops for rows and columns. Processing consecutive bytes in source (data) and destination (dst) will improve cache locality and pay off in speed.


Images with 16 bit depth are rather new in Qt at the time of writing.

New color formats with 16 bit depth per component were added in Qt 5.12:

  • QImage::Format_RGBX64 = 25
    The image is stored using a 64-bit halfword-ordered RGB(x) format (16-16-16-16). This is the same as the Format_RGBX64 except alpha must always be 65535. (added in Qt 5.12)
  • QImage::Format_RGBA64 = 26
    The image is stored using a 64-bit halfword-ordered RGBA format (16-16-16-16). (added in Qt 5.12)
  • QImage::Format_RGBA64_Premultiplied = 27
    The image is stored using a premultiplied 64-bit halfword-ordered RGBA format (16-16-16-16). (added in Qt 5.12)

The grayscale with 16 bit depth was added in Qt 5.13:

  • QImage::Format_Grayscale16 = 28
    The image is stored using an 16-bit grayscale format. (added in Qt 5.13)

(Doc. copied from enum QImage::Format)

To fiddle a bit with this, I made a sample application for conversion of 16 bit-per-component RGB image to 16 bit grayscale.

testQImageGray16.cc:

#include <QtWidgets>

QImage imageToGray16(const QImage &qImg)
{
  QImage qImgGray(qImg.width(), qImg.height(), QImage::Format_Grayscale16);
  for (int y = 0; y < qImg.height(); ++y) {
    for (int x = 0; x < qImg.width(); ++x) {
      qImgGray.setPixelColor(x, y, qImg.pixelColor(x, y));
    }
  }
  return qImgGray;
}

class Canvas: public QWidget {
  private:
    QImage _qImg;
  public:
    std::function<void(QPoint)> sigMouseMove;

  public:
    Canvas(QWidget *pQParent = nullptr):
      QWidget(pQParent)
    {
      setMouseTracking(true);
    }
    Canvas(const QImage &qImg, QWidget *pQParent = nullptr):
      QWidget(pQParent), _qImg(qImg)
    {
      setMouseTracking(true);
    }
    virtual ~Canvas() = default;
    Canvas(const Canvas&) = delete;
    Canvas& operator=(const Canvas&) = delete;

  public:
    virtual QSize sizeHint() const { return _qImg.size(); }
    const QImage& image() const { return _qImg; }
    void setImage(const QImage &qImg) { _qImg = qImg; update(); }
  protected:
    virtual void paintEvent(QPaintEvent *pQEvent) override;
    virtual void mouseMoveEvent(QMouseEvent *pQEvent) override;
};

void Canvas::paintEvent(QPaintEvent *pQEvent)
{
  QWidget::paintEvent(pQEvent);
  QPainter qPainter(this);
  qPainter.drawImage(0, 0, _qImg);
}

void Canvas::mouseMoveEvent(QMouseEvent *pQEvent)
{
  if (sigMouseMove) sigMouseMove(pQEvent->pos());
}

QString getInfo(const QImage &qImg)
{
  QString qStr;
  QDebug(&qStr) << "Image Info:\n" << qImg;
  for (int i = 0, len = qStr.length(); i < qStr.length(); ++i) {
    if (qStr[i] == ',' && i + 1 < len && qStr[i + 1] != ' ') qStr[i] = '\n';
  }
  return qStr;
}

QString getPixelInfo(const QImage &qImg, QPoint pos)
{
  if (!QRect(QPoint(0, 0), qImg.size()).contains(pos)) return QString();
  const int bytes = (qImg.depth() + 7) / 8; assert(bytes > 0);
  const QByteArray data(
    (const char*)(qImg.bits()
      + pos.y() * qImg.bytesPerLine()
      + (pos.x() * qImg.depth() + 7) / 8),
    bytes);
  QString qStr;
  QDebug(&qStr) << pos << ":" << qImg.pixelColor(pos)
    << "raw:" << QString("#") + data.toHex();
  return qStr;
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // load sample data
  const QImage qImgRGB16("pnggrad16rgb.png"/*, QImage::Format_RGBX64*/);
  // setup GUI
  QWidget winMain;
  QGridLayout qGrid;
  int col = 0, row = 0;
  QLabel qLblRGBInfo(getInfo(qImgRGB16));
  qGrid.addWidget(&qLblRGBInfo, row++, col);
  Canvas qCanvasRGB(qImgRGB16);
  qGrid.addWidget(&qCanvasRGB, row++, col);
  QLabel qLblRGB;
  qGrid.addWidget(&qLblRGB, row++, col);
  row = 0; ++col;
  Canvas qCanvasGray(qImgRGB16.convertToFormat(QImage::Format_Grayscale16));
  QLabel qLblGrayInfo;
  qGrid.addWidget(&qLblGrayInfo, row++, col);
  qGrid.addWidget(&qCanvasGray, row++, col);
  QLabel qLblGray;
  qGrid.addWidget(&qLblGray, row++, col);
  QHBoxLayout qHBoxQImageConvert;
  QButtonGroup qBtnGrpQImageConvert;
  QRadioButton qTglQImageConvertBuiltIn("Use QImage::convertToFormat()");
  qBtnGrpQImageConvert.addButton(&qTglQImageConvertBuiltIn);
  qTglQImageConvertBuiltIn.setChecked(true);
  qHBoxQImageConvert.addWidget(&qTglQImageConvertBuiltIn);
  QRadioButton qTglQImageConvertCustom("Use imageToGray16()");
  qBtnGrpQImageConvert.addButton(&qTglQImageConvertCustom);
  qHBoxQImageConvert.addWidget(&qTglQImageConvertCustom);
  qGrid.addLayout(&qHBoxQImageConvert, row++, col);
  winMain.setLayout(&qGrid);
  winMain.show();
  // install signal handlers
  auto updatePixelInfo = [&](QPoint pos)
  {
    qLblRGB.setText (getPixelInfo(qCanvasRGB.image(), pos));
    qLblGray.setText(getPixelInfo(qCanvasGray.image(), pos));
  };
  qCanvasRGB.sigMouseMove = updatePixelInfo;
  qCanvasGray.sigMouseMove = updatePixelInfo;
  auto updateGrayImage = [&](bool customConvert)
  {
    qCanvasGray.setImage(customConvert
      ? qImgRGB16.convertToFormat(QImage::Format_Grayscale16)
      : imageToGray16(qImgRGB16));
    qLblGrayInfo.setText(getInfo(qCanvasGray.image()));
    qLblGray.setText(QString());
  };
  QObject::connect(&qTglQImageConvertBuiltIn, &QRadioButton::toggled,
    [&](bool checked) { if (checked) updateGrayImage(false); });
  QObject::connect(&qTglQImageConvertCustom, &QRadioButton::toggled,
    [&](bool checked) { if (checked) updateGrayImage(true); });
  // runtime loop
  updateGrayImage(false);
  return app.exec();
}

and a build-script for CMake CMakeLists.txt:

project(QImageGray16)

cmake_minimum_required(VERSION 3.10.0)

set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(Qt5Widgets CONFIG REQUIRED)

include_directories("${CMAKE_SOURCE_DIR}")

add_executable(testQImageGray16
  testQImageGray16.cc)

target_link_libraries(testQImageGray16
  Qt5::Widgets)

# define QT_NO_KEYWORDS to prevent confusion between of Qt signal-slots and
# other signal-slot APIs
target_compile_definitions(testQImageGray16 PUBLIC QT_NO_KEYWORDS)

I downloaded a sample image pnggrad16rgb.png from www.fnordware.com/superpng/samples.html. (Other sample images can be found on The "official" test-suite for PNG.)

After building and running in VS2017, I got the following snapshot:

Snapshot of testQImageGray16

When mouse is moved over the displayed images, the bottom label shows the current position in image and the respective pixel as QColor and raw hex-values.

Out of curiosity, I implemented the approach of OP with two nested loops (fixing the qRgb() issue):

QImage imageToGray16(const QImage &qImg)
{
  QImage qImgGray(qImg.width(), qImg.height(), QImage::Format_Grayscale16);
  for (int y = 0; y < qImg.height(); ++y) {
    for (int x = 0; x < qImg.width(); ++x) {
      qImgGray.setPixelColor(x, y, qImg.pixelColor(x, y));
    }
  }
  return qImgGray;
}

to compare the result with the QImage::convertToFormat() which I found in the Qt doc.

QImage QImage::convertToFormat(QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) const &

QImage QImage::convertToFormat(QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) &&

Returns a copy of the image in the given format.

The specified image conversion flags control how the image data is handled during the conversion process.

That's interesting:

There is no visible difference. Though, this might not be a surprise considering that the display may reduce color depth to 8 or 10 (beside of the fact that humans are probably not able to differentiate 216 shades of gray nor 2163 RGB values.

However wiping over the images, I realized that with QImage::convertToFormat(), the first and second byte of each pixel were always identical (e.g. b2b2), which is not the case when custom conversion with imageToGray16() is used.

I didn't dig deeper but it could be worth a further investigation which of these methods is actually more accurate.

0
votes

@Scheff, thanks for the input ! I am now using the following code:

Qimage = QImage(imagewidth,imageheight,QImage::Format_Grayscale16);


    for (int j = 0; j < imageheight; ++j)
    {
       quint16 *dst =  reinterpret_cast<quint16*>(Qimage.bits() + j * Qimage.bytesPerLine());

       for (int i = 0; i < imagewidth; ++i)
       {

            unsigned short pixelval =  static_cast<unsigned short>(image[i + j * imagewidth]);

            dst[i] = pixelval;

       }

    }