Skip to content

Commit 23f3520

Browse files
committed
fix(image): match the anti-aliasing kernel to the scaled data type
The scaling functions require the anti-aliasing kernel to have the data type of the data they scale, which was not the case: - when exporting an image area without the LUT but with the interpolation while the Z-axis log scale was enabled: the raw data was exported with the float64 kernel of its logarithm; - after set_data with data of another type: the kernel kept the former type, so that drawing the image failed (aborting the application with PyQt5), as well as exporting it. Add BaseImageItem._get_interpolation_for, which gives the kernel the data type of the data to be scaled, used by all export_roi implementations, and _sync_interpolation_dtype, which keeps the kernel in the data type of the data drawn, now also called by set_data. Refs #89 Assisted-by: Claude Opus 5.5
1 parent 81dd53d commit 23f3520

4 files changed

Lines changed: 171 additions & 7 deletions

File tree

‎plotpy/items/image/base.py‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,8 @@ def set_data(
516516
# display would keep using the previous (now stale) log data.
517517
if self._is_zaxis_log:
518518
self._recompute_log_data()
519+
# The new data may have another data type than the previous one
520+
self._sync_interpolation_dtype()
519521
if not self.param.keep_lut_range:
520522
if self._is_zaxis_log:
521523
# The linear LUT range, restored when the log scale is disabled,
@@ -653,6 +655,33 @@ def get_interpolation(self) -> tuple[int] | tuple[int, np.ndarray]:
653655
"""Get interpolation mode"""
654656
return self.interpolate
655657

658+
def _get_interpolation_for(
659+
self, data: np.ndarray
660+
) -> tuple[int] | tuple[int, np.ndarray]:
661+
"""Return the interpolation mode to be used to scale the given data
662+
663+
The scaling functions require the anti-aliasing kernel to have the data
664+
type of the data they scale, which is not always the data drawn (e.g. the
665+
raw data, exported without the LUT, instead of its logarithm drawn with
666+
the Z-axis logarithmic scale).
667+
668+
Args:
669+
data: Data to be scaled
670+
671+
Returns:
672+
Interpolation mode, with an anti-aliasing kernel of the data type of
673+
the given data
674+
"""
675+
interp = self.interpolate
676+
if interp[0] == INTERP_AA and interp[1].dtype != data.dtype:
677+
interp = (INTERP_AA, interp[1].astype(data.dtype))
678+
return interp
679+
680+
def _sync_interpolation_dtype(self) -> None:
681+
"""Give the anti-aliasing kernel the data type of the data drawn"""
682+
if self.data is not None:
683+
self.interpolate = self._get_interpolation_for(self._get_display_data())
684+
656685
def set_lut_range(self, lut_range: tuple[float, float]) -> None:
657686
"""
658687
Set the current active lut range
@@ -759,9 +788,7 @@ def set_zaxis_log_state(self, state: bool) -> None:
759788
self._is_zaxis_log = False
760789
self._log_data = None
761790
self.set_lut_range(self._lin_lut_range)
762-
if self.data is not None and self.interpolate[0] == INTERP_AA:
763-
dtype = self._get_display_data().dtype
764-
self.interpolate = (INTERP_AA, self.interpolate[1].astype(dtype))
791+
self._sync_interpolation_dtype()
765792
plot = self.plot()
766793
if plot is not None:
767794
plot.update_colormap_axis(self)
@@ -892,7 +919,10 @@ def export_roi(
892919
else:
893920
a, b = 1.0, 0.0
894921
data = self.data
895-
interp = self.interpolate if apply_interpolation else (INTERP_NEAREST,)
922+
if apply_interpolation:
923+
interp = self._get_interpolation_for(data)
924+
else:
925+
interp = (INTERP_NEAREST,)
896926
_scale_rect(data, src_rect, dst_image, dst_rect, (a, b, None), interp)
897927

898928
# ---- QwtPlotItem API -----------------------------------------------------

‎plotpy/items/image/standard.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,10 @@ def export_roi(
404404
else:
405405
a, b = 1.0, 0.0
406406
data = self.data
407-
interp = self.interpolate if apply_interpolation else (INTERP_NEAREST,)
407+
if apply_interpolation:
408+
interp = self._get_interpolation_for(data)
409+
else:
410+
interp = (INTERP_NEAREST,)
408411
rescaled_src = self._rescale_src_rect(src_rect)
409412
_scale_rect(
410413
data,
@@ -697,7 +700,10 @@ def export_roi(
697700
a, b = 1.0, 0.0
698701
lut = (a, b, None, np.zeros((1,), np.uint32))
699702
data = self.data
700-
interp = self.interpolate if apply_interpolation else (INTERP_NEAREST,)
703+
if apply_interpolation:
704+
interp = self._get_interpolation_for(data)
705+
else:
706+
interp = (INTERP_NEAREST,)
701707
xytr = self.x, self.y, src_rect
702708
_scale_xy(data, xytr, dst_image, dst_rect, lut, interp)
703709

‎plotpy/items/image/transform.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def export_roi(
360360
aa = np.ones((force_interp_size, force_interp_size), data.dtype)
361361
interp = (force_interp_mode, aa)
362362
else: # don't force interpolation, keep current image interpolation
363-
interp = self.interpolate
363+
interp = self._get_interpolation_for(data)
364364
else: # don't apply interpolation --> INTERP_NEAREST
365365
interp = (INTERP_NEAREST,)
366366
_scale_tr(data, mat, dst_image, dst_rect, (a, b, None), interp)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""Anti-aliasing interpolation kernel of image items (Issue #89).
7+
8+
The scaling functions require the anti-aliasing kernel to have the data type of
9+
the data they scale: exporting and drawing an image must work whatever the Z-axis
10+
scale and the data type of the image data.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import numpy as np
16+
import pytest
17+
from guidata.qthelpers import qt_app_context
18+
from qtpy import QtCore as QC
19+
from qtpy import QtGui as QG
20+
from qtpy import QtWidgets as QW
21+
from qwt import QwtScaleMap
22+
23+
from plotpy.builder import make
24+
from plotpy.items import assemble_imageitems
25+
26+
KINDS = ("image", "xyimage", "trimage", "maskedimage", "maskedxyimage")
27+
DTYPES = (np.uint8, np.uint16, np.int32, np.float32)
28+
NY, NX = 6, 8
29+
30+
31+
def _data(dtype: type) -> np.ndarray:
32+
"""Return image data of the given data type"""
33+
return np.arange(1, NY * NX + 1).reshape(NY, NX).astype(dtype)
34+
35+
36+
def _make_item(kind: str, data: np.ndarray):
37+
"""Return an image item of the given kind, with the anti-aliasing
38+
interpolation"""
39+
x, y = np.arange(NX, dtype=float), np.arange(NY, dtype=float)
40+
mask = np.zeros(data.shape, dtype=bool)
41+
interpolation = "antialiasing"
42+
builders = {
43+
"image": lambda: make.image(data, interpolation=interpolation),
44+
"xyimage": lambda: make.xyimage(x, y, data, interpolation=interpolation),
45+
"trimage": lambda: make.trimage(data, interpolation=interpolation),
46+
"maskedimage": lambda: make.maskedimage(
47+
data, mask, interpolation=interpolation
48+
),
49+
"maskedxyimage": lambda: make.maskedxyimage(
50+
x, y, data, mask, interpolation=interpolation
51+
),
52+
}
53+
return builders[kind]()
54+
55+
56+
def _export(item, apply_lut: bool) -> np.ndarray:
57+
"""Export the image with its interpolation"""
58+
rect = QC.QRectF(0, 0, NX, NY)
59+
return assemble_imageitems(
60+
[item], rect, NX, NY, apply_lut=apply_lut, apply_interpolation=True
61+
)
62+
63+
64+
def _draw(item) -> None:
65+
"""Draw the image item on an image, raising the exceptions which would be
66+
raised in the plot's paint event"""
67+
width, height = 4 * NX, 4 * NY
68+
xmap, ymap = QwtScaleMap(), QwtScaleMap()
69+
xmap.setPaintInterval(0, width)
70+
xmap.setScaleInterval(0, NX)
71+
ymap.setPaintInterval(0, height)
72+
ymap.setScaleInterval(0, NY)
73+
image = QG.QImage(width, height, QG.QImage.Format_ARGB32)
74+
painter = QG.QPainter(image)
75+
try:
76+
item.draw(painter, xmap, ymap, QC.QRectF(0, 0, width, height))
77+
finally:
78+
painter.end()
79+
80+
81+
def _plot_with_item(item):
82+
"""Return a plot window showing the image item"""
83+
# In unattended mode, leaving a `qt_app_context` schedules the closing of all
84+
# top-level widgets: process it now, so that it does not close this window
85+
QW.QApplication.processEvents()
86+
win = make.dialog(type="image")
87+
win.manager.get_plot().add_item(item)
88+
return win
89+
90+
91+
@pytest.mark.parametrize("dtype", DTYPES)
92+
@pytest.mark.parametrize("kind", KINDS)
93+
def test_export_with_anti_aliasing(kind: str, dtype: type) -> None:
94+
"""Exporting an image area with the anti-aliasing interpolation works with or
95+
without the LUT, with or without the Z-axis log scale"""
96+
with qt_app_context(exec_loop=False):
97+
item = _make_item(kind, _data(dtype))
98+
for log_state in (False, True):
99+
item.set_zaxis_log_state(log_state)
100+
for apply_lut in (False, True):
101+
assert _export(item, apply_lut).shape == (NY, NX)
102+
103+
104+
@pytest.mark.parametrize("kind", KINDS)
105+
def test_set_data_of_another_data_type(kind: str) -> None:
106+
"""After replacing the data by data of another type, the kernel has the data
107+
type of the data drawn, and the image can be drawn and exported"""
108+
with qt_app_context(exec_loop=False):
109+
item = _make_item(kind, _data(np.uint16))
110+
win = _plot_with_item(item)
111+
for log_state in (False, True):
112+
item.set_zaxis_log_state(log_state)
113+
for dtype in (np.float32, np.uint8):
114+
item.set_data(_data(dtype))
115+
# With the log scale, the data drawn is the float64 logarithm
116+
expected_dtype = np.float64 if log_state else dtype
117+
assert item.interpolate[1].dtype == expected_dtype
118+
_draw(item)
119+
for apply_lut in (False, True):
120+
assert _export(item, apply_lut).shape == (NY, NX)
121+
win.close()
122+
123+
124+
if __name__ == "__main__":
125+
for _kind in KINDS:
126+
for _dtype in DTYPES:
127+
test_export_with_anti_aliasing(_kind, _dtype)
128+
test_set_data_of_another_data_type(_kind)

0 commit comments

Comments
 (0)