Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.gui.utils.image: Added support of QImage.Format_Grayscale8 to convertQImageToArray #3958

Merged
merged 5 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions doc/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ This table summarizes the support matrix of silx:
+------------+--------------+--------------------------------+
| System | Python vers. | Qt and its bindings |
+------------+--------------+--------------------------------+
| `Windows`_ | 3.7-3.10 | PyQt5.6+, PySide6.4+, PyQt6.3+ |
| `Windows`_ | 3.7-3.10 | PyQt5.9+, PySide6.4+, PyQt6.3+ |
+------------+--------------+--------------------------------+
| `MacOS`_ | 3.7-3.10 | PyQt5.6+, PySide6.4+, PyQt6.3+ |
| `MacOS`_ | 3.7-3.10 | PyQt5.9+, PySide6.4+, PyQt6.3+ |
+------------+--------------+--------------------------------+
| `Linux`_ | 3.7-3.10 | PyQt5.3+, PySide6.4+, PyQt6.3+ |
| `Linux`_ | 3.7-3.10 | PyQt5.9+, PySide6.4+, PyQt6.3+ |
+------------+--------------+--------------------------------+

For the description of *silx* dependencies, see the Dependencies_ section.
Expand Down
11 changes: 4 additions & 7 deletions src/silx/gui/_glutils/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def rasterTextQt(
font = qt.QFont(font, size, weight, italic)

# get text size
image = qt.QImage(1, 1, qt.QImage.Format_RGB888)
image = qt.QImage(1, 1, qt.QImage.Format_Grayscale8)
painter = qt.QPainter()
painter.begin(image)
painter.setPen(qt.Qt.white)
Expand All @@ -133,11 +133,11 @@ def rasterTextQt(
# align line size to 32 bits to ease conversion to numpy array
width = 4 * ((width + 3) // 4)
image = qt.QImage(
int(width), int(bounds.height() * devicePixelRatio + 2), qt.QImage.Format_RGB888
int(width),
int(bounds.height() * devicePixelRatio + 2),
qt.QImage.Format_Grayscale8,
)
image.setDevicePixelRatio(devicePixelRatio)

# TODO if Qt5 use Format_Grayscale8 instead
image.fill(0)

# Raster text
Expand All @@ -150,9 +150,6 @@ def rasterTextQt(

array = convertQImageToArray(image)

# RGB to R
array = numpy.ascontiguousarray(array[:, :, 0])

# Remove leading and trailing empty columns/rows but one on each side
filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0]
filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0]
Expand Down
38 changes: 26 additions & 12 deletions src/silx/gui/utils/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,30 +84,41 @@ def convertArrayToQImage(array: numpy.ndarray) -> qt.QImage:
def convertQImageToArray(image: qt.QImage) -> numpy.ndarray:
"""Convert a QImage to a numpy array.

If QImage format is not Format_RGB888, Format_RGBA8888 or Format_ARGB32,
it is first converted to one of this format depending on
the presence of an alpha channel.
If QImage format is not one of:
- Format_Grayscale8
- Format_RGB888
- Format_RGBA8888
- Format_ARGB32,
it is first converted to one of this format.

The created numpy array is using a copy of the QImage data.

:param QImage image: The QImage to convert.
:return: The image array of RGB or RGBA channels of shape
(height, width, channels (3 or 4))
:return: Image array of uint8 of shape:
- (height, width) for grayscale images
- (height, width, channels (3 or 4)) for RGB and RGBA images
"""
rgba8888 = getattr(qt.QImage, 'Format_RGBA8888', None) # Only in Qt5
supportedFormats = (
qt.QImage.Format_Grayscale8,
qt.QImage.Format_ARGB32,
qt.QImage.Format_RGB888,
qt.QImage.Format_RGBA8888,
)

# Convert to supported format if needed
if image.format() not in (qt.QImage.Format_ARGB32,
qt.QImage.Format_RGB888,
rgba8888):
if image.format() not in supportedFormats:
if image.hasAlphaChannel():
image = image.convertToFormat(
rgba8888 if rgba8888 is not None else qt.QImage.Format_ARGB32)
image = image.convertToFormat(qt.QImage.Format_RGBA8888)
else:
image = image.convertToFormat(qt.QImage.Format_RGB888)

format_ = image.format()
channels = 3 if format_ == qt.QImage.Format_RGB888 else 4
if format_ == qt.QImage.Format_Grayscale8:
channels = 1
elif format_ == qt.QImage.Format_RGB888:
channels = 3
else:
channels = 4

ptr = image.bits()
if qt.BINDING == 'PyQt5':
Expand All @@ -133,6 +144,9 @@ def convertQImageToArray(image: qt.QImage) -> numpy.ndarray:
else: # big endian: ARGB -> RGBA
view = view[:, :, (1, 2, 3, 0)]

if channels == 1: # Remove channel dimension
view = view[:, :, 0]

# Format_RGB888 and Format_RGBA8888 do not need reshuffling channels:
# They are byte-ordered and already in the right order

Expand Down
83 changes: 46 additions & 37 deletions src/silx/gui/utils/test/test_image.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -28,50 +28,59 @@
__date__ = "16/01/2017"

import numpy
import pytest

from silx.gui import qt
from silx.utils.testutils import ParametricTestCase
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.image import convertArrayToQImage, convertQImageToArray


class TestQImageConversion(TestCaseQt, ParametricTestCase):
"""Tests conversion of QImage to/from numpy array."""
@pytest.mark.parametrize(
"format_, channels",
[
(qt.QImage.Format_RGB888, 3), # Native support
(qt.QImage.Format_ARGB32, 4), # Native support
]
)
def testConvertArrayToQImage(format_, channels):
"""Test conversion of numpy array to QImage"""
image = numpy.arange(
3*3*channels, dtype=numpy.uint8).reshape(3, 3, channels)
qimage = convertArrayToQImage(image)

def testConvertArrayToQImage(self):
"""Test conversion of numpy array to QImage"""
for format_, channels in [('Format_RGB888', 3),
('Format_ARGB32', 4)]:
with self.subTest(format_):
image = numpy.arange(
3*3*channels, dtype=numpy.uint8).reshape(3, 3, channels)
qimage = convertArrayToQImage(image)
assert (qimage.height(), qimage.width()) == image.shape[:2]
assert qimage.format() == format_

self.assertEqual(qimage.height(), image.shape[0])
self.assertEqual(qimage.width(), image.shape[1])
self.assertEqual(qimage.format(), getattr(qt.QImage, format_))
for row in range(3):
for col in range(3):
# Qrgb has no alpha channel, not compared
# Qt uses x,y while array is row,col...
assert qt.QColor(qimage.pixel(col, row)) == qt.QColor(*image[row, col, :3])

for row in range(3):
for col in range(3):
# Qrgb has no alpha channel, not compared
# Qt uses x,y while array is row,col...
self.assertEqual(qt.QColor(qimage.pixel(col, row)),
qt.QColor(*image[row, col, :3]))

@pytest.mark.parametrize(
"format_, channels",
[
(qt.QImage.Format_RGB888, 3), # Native support
(qt.QImage.Format_ARGB32, 4), # Native support
(qt.QImage.Format_RGB32, 3), # Conversion to RGB
]
)
def testConvertQImageToArray(format_, channels):
"""Test conversion of QImage to numpy array"""
color = numpy.arange(channels) # RGB(A) values
qimage = qt.QImage(3, 3, format_)
qimage.fill(qt.QColor(*color))
image = convertQImageToArray(qimage)

def testConvertQImageToArray(self):
"""Test conversion of QImage to numpy array"""
for format_, channels in [
('Format_RGB888', 3), # Native support
('Format_ARGB32', 4), # Native support
('Format_RGB32', 3)]: # Conversion to RGB
with self.subTest(format_):
color = numpy.arange(channels) # RGB(A) values
qimage = qt.QImage(3, 3, getattr(qt.QImage, format_))
qimage.fill(qt.QColor(*color))
image = convertQImageToArray(qimage)
assert (qimage.height(), qimage.width(), len(color)) == image.shape
assert numpy.all(numpy.equal(image, color))

self.assertEqual(qimage.height(), image.shape[0])
self.assertEqual(qimage.width(), image.shape[1])
self.assertEqual(image.shape[2], len(color))
self.assertTrue(numpy.all(numpy.equal(image, color)))

def testConvertQImageToArrayGrayscale():
"""Test conversion of grayscale QImage to numpy array"""
qimage = qt.QImage(3, 3, qt.QImage.Format_Grayscale8)
qimage.fill(1)
image = convertQImageToArray(qimage)

assert (qimage.height(), qimage.width()) == image.shape
assert numpy.all(numpy.equal(image, 1))