Skip to content

Commit 6dad599

Browse files
authored
Merge pull request #26 from zivy/pyside
Utilities for converting to and from PySide QImage, QPixmap.
2 parents 64e8575 + a97adb2 commit 6dad599

File tree

5 files changed

+157
-3
lines changed

5 files changed

+157
-3
lines changed

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Install dependencies
3838
run: |
3939
python -m pip install --upgrade pip
40-
python -m pip install -r docs/requirements.txt .[vtk,dask]
40+
python -m pip install -r docs/requirements.txt .[vtk,dask,pyside]
4141
- name: Build Sphinx Documentation
4242
run: |
4343
make -C docs html

.github/workflows/main.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ jobs:
4848
- name: Install dependencies
4949
run: |
5050
python -m pip install --upgrade pip
51-
python -m pip install -e .[vtk,dask] -r test/requirements.txt
51+
python -m pip install -e .[vtk,dask,pyside] -r test/requirements.txt
52+
sudo apt install libegl1
5253
5354
- name: Test with pytest
55+
env:
56+
QT_QPA_PLATFORM: offscreen
5457
run: |
5558
python -m pytest
5659

SimpleITK/utilities/pyside.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# ========================================================================
2+
#
3+
# Copyright NumFOCUS
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0.txt
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# ========================================================================
18+
19+
import SimpleITK as sitk
20+
import numpy as np
21+
from PySide6.QtGui import QPixmap, QImage
22+
23+
24+
def sitk2qimage(image: sitk.Image) -> QImage:
25+
"""Convert a SimpleITK.Image to PySide QImage.
26+
27+
Works with 2D images, grayscale or three channel, where it is assumed that
28+
the three channels represent values in the RGB color space. In SimpleITK there is no notion
29+
of color space, so if the three channels are in the HSV colorspace the display
30+
will look strange. If the SimpleITK pixel type represents a high dynamic range,
31+
the intensities are linearly scaled to [0,255].
32+
33+
:param image: Image to convert.
34+
:return: A QImage.
35+
"""
36+
number_components_per_pixel = image.GetNumberOfComponentsPerPixel()
37+
if number_components_per_pixel not in [1, 3]:
38+
raise ValueError(
39+
f"SimpleITK image has {number_components_per_pixel} channels, expected 1 or 3 channels"
40+
)
41+
if number_components_per_pixel == 3 and image.GetPixelID() != sitk.sitkVectorUInt8:
42+
raise ValueError(
43+
f"SimpleITK three channel image has pixel type ({image.GetPixelIDTypeAsString()}), expected vector 8-bit unsigned integer"
44+
)
45+
46+
if number_components_per_pixel == 1 and image.GetPixelID() != sitk.sitkUInt8:
47+
image = sitk.Cast(
48+
sitk.RescaleIntensity(image, outputMinimum=0, outputMaximum=255),
49+
sitk.sitkUInt8,
50+
)
51+
arr = sitk.GetArrayViewFromImage(image)
52+
return QImage(
53+
arr.data,
54+
image.GetWidth(),
55+
image.GetHeight(),
56+
arr.strides[0], # number of bytes per row
57+
QImage.Format_Grayscale8
58+
if number_components_per_pixel == 1
59+
else QImage.Format_RGB888,
60+
)
61+
62+
63+
def sitk2qpixmap(image: sitk.Image) -> QPixmap:
64+
"""Convert a SimpleITK.Image to PySide QPixmap.
65+
66+
Works with 2D images, grayscale or three channel, where it is assumed that
67+
the three channels represent values in the RGB color space. In SimpleITK there is no notion
68+
of color space, so if the three channels are in the HSV colorspace the display
69+
will look strange. If the SimpleITK pixel type represents a high dynamic range,
70+
the intensities are linearly scaled to [0,255].
71+
72+
:param image: Image to convert.
73+
:return: A QPixmap.
74+
"""
75+
return QPixmap.fromImage(sitk2qimage(image))
76+
77+
78+
def qimage2sitk(image: QImage) -> sitk.Image:
79+
"""Convert a QImage to SimpleITK.Image.
80+
81+
If the QImage contains a grayscale image will return a scalar
82+
SimpleITK image. Otherwise, returns a three channel RGB image.
83+
84+
:param image: QImage to convert.
85+
:return: A SimpleITK image, single channel or three channel RGB.
86+
"""
87+
# Use constBits() to get the raw data without copying (bits() returns a deep copy).
88+
# Then reshape the array to the image shape.
89+
is_vector = True
90+
# Convert image to Format_RGB888 because it keeps the byte order
91+
# regardless of big/little endian (RGBA8888 doesn't).
92+
image = image.convertToFormat(QImage.Format_RGB888)
93+
arr = np.ndarray(
94+
(image.height(), image.width(), 3),
95+
buffer=image.constBits(),
96+
strides=[image.bytesPerLine(), 3, 1],
97+
dtype=np.uint8,
98+
)
99+
if image.isGrayscale():
100+
arr = arr[:, :, 0]
101+
is_vector = False
102+
return sitk.GetImageFromArray(arr, isVector=is_vector)
103+
104+
105+
def qpixmap2sitk(pixmap: QPixmap) -> sitk.Image:
106+
"""Convert a QPixmap to SimpleITK.Image.
107+
108+
If the QPixmap contains a grayscale image will return a scalar
109+
SimpleITK image. Otherwise, returns a four channel RGBA image.
110+
111+
:param qpixmap: QPixmap to convert.
112+
:return: A SimpleITK image, single channel or four channel.
113+
"""
114+
return qimage2sitk(pixmap.toImage())

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dynamic = ["dependencies", "version"]
2222
[project.optional-dependencies]
2323
vtk=['vtk>=9.0']
2424
dask=['dask']
25+
pyside=['PySide6']
2526

2627
[tool.setuptools]
2728
packages = ["SimpleITK.utilities"]
@@ -31,4 +32,4 @@ packages = ["SimpleITK.utilities"]
3132
dependencies = {file = ["requirements.txt"]}
3233

3334
[tool.setuptools_scm]
34-
write_to = "SimpleITK/utilities/_version.py"
35+
write_to = "SimpleITK/utilities/_version.py"

test/test_pyside.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import SimpleITK as sitk
2+
from PySide6 import QtWidgets
3+
from SimpleITK.utilities.pyside import sitk2qpixmap, qpixmap2sitk
4+
5+
6+
def test_pyside():
7+
# QPixmap cannot be created without a QGuiApplication
8+
app = QtWidgets.QApplication()
9+
10+
# Test grayscale
11+
scalar_image = sitk.Image([2, 3], sitk.sitkUInt8)
12+
scalar_image[0, 0] = 1
13+
scalar_image[0, 1] = 2
14+
scalar_image[0, 2] = 3
15+
scalar_image[1, 0] = 253
16+
scalar_image[1, 1] = 254
17+
scalar_image[1, 2] = 255
18+
19+
qpixmap = sitk2qpixmap(scalar_image)
20+
sitk_image = qpixmap2sitk(qpixmap)
21+
# Compare on pixel values, metadata information ignored.
22+
assert sitk.Hash(sitk_image) == sitk.Hash(scalar_image)
23+
24+
# Test color
25+
color_image = sitk.Image([2, 3], sitk.sitkVectorUInt8, 3)
26+
color_image[0, 0] = [0, 1, 2]
27+
color_image[0, 1] = [4, 8, 16]
28+
color_image[0, 2] = [32, 64, 128]
29+
color_image[1, 0] = [0, 10, 20]
30+
color_image[1, 1] = [30, 40, 50]
31+
color_image[1, 2] = [60, 70, 80]
32+
33+
qpixmap = sitk2qpixmap(color_image)
34+
sitk_image = qpixmap2sitk(qpixmap)
35+
# Compare on pixel values, metadata information ignored.
36+
assert sitk.Hash(sitk_image) == sitk.Hash(color_image)

0 commit comments

Comments
 (0)