Skip to content

Commit fecdbfb

Browse files
committed
feat(inset): resample image insets to device pixels with PIL
Pre-resize the source to the inset's exact device-pixel footprint with PIL and render it pixel-for-pixel (interpolation='none'), instead of letting matplotlib resample the full-resolution image. Downscale with LANCZOS to antialias (matplotlib's resampler aliases on large reductions); enlarge with NEAREST to stay crisp (smooth filters look hazy and ring on hard edges).
1 parent ee2ffbd commit fecdbfb

4 files changed

Lines changed: 33 additions & 4 deletions

File tree

plotnine/composition/_inset_image.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
if TYPE_CHECKING:
1313
from matplotlib.figure import Figure
14+
from matplotlib.image import BboxImage
1415
from matplotlib.patches import Rectangle
1516
from matplotlib.transforms import Bbox
1617

@@ -63,6 +64,10 @@ class _InsetImage:
6364
# ratio doesn't match.
6465
_anchor: tuple[float, float]
6566

67+
# BboxImage artist created in draw(); its data is updated by
68+
# _arrange_in_box once the layout engine computes the final bbox.
69+
_image_artist: BboxImage
70+
6671
def __init__(
6772
self,
6873
image: PILImage | np.ndarray,
@@ -107,6 +112,8 @@ def _arrange_in_box(
107112
Fractional figure-coordinates of the box assigned to this
108113
inset by `inset_element.align_to`.
109114
"""
115+
from matplotlib.transforms import TransformedBbox
116+
110117
l, b, r, t = _fit_aspect(
111118
left,
112119
bottom,
@@ -119,6 +126,26 @@ def _arrange_in_box(
119126
self._frac_bbox.bounds = (l, b, r - l, t - b) # pyright: ignore[reportAttributeAccessIssue]
120127
self.patch.set_bounds(left, bottom, right - left, top - bottom)
121128

129+
# The layout engine has finalised the bbox, so its device-pixel
130+
# size is now known. _fit_aspect preserves the source aspect, so
131+
# both axes scale by the same factor.
132+
source_w, source_h = self._image_size
133+
tbox = TransformedBbox(self._frac_bbox, self.figure.transFigure)
134+
tw = max(1, round(tbox.width))
135+
th = max(1, round(tbox.height))
136+
downscaling = tw < source_w and th < source_h
137+
138+
# LANCZOS to shrink: it antialiases the downscale.
139+
# Not LANCZOS to enlarge: it looks hazy and rings badly on
140+
# hard edges, so NEAREST.
141+
if downscaling:
142+
resampling = Image.Resampling.LANCZOS
143+
else:
144+
resampling = Image.Resampling.NEAREST
145+
146+
resized = self._image.resize((tw, th), resampling)
147+
self._image_artist.set_data(np.asarray(resized))
148+
122149
def draw(self):
123150
from matplotlib.image import BboxImage
124151
from matplotlib.transforms import TransformedBbox
@@ -128,11 +155,13 @@ def draw(self):
128155
self.theme._setup(self) # pyright: ignore[reportArgumentType]
129156
self._draw_plot_background()
130157

131-
image_artist = BboxImage(
132-
TransformedBbox(self._frac_bbox, self.figure.transFigure)
158+
tbox = TransformedBbox(self._frac_bbox, self.figure.transFigure)
159+
# The image data is set later by _arrange_in_box, once the layout
160+
# engine has computed the final device-pixel dimensions.
161+
self._image_artist = BboxImage(
162+
tbox, interpolation="none", resample=False
133163
)
134-
image_artist.set_data(np.asarray(self._image))
135-
self.figure.add_artist(image_artist)
164+
self.figure.add_artist(self._image_artist)
136165

137166
self.theme.apply()
138167

0 Bytes
Loading
2.05 KB
Loading
1.81 KB
Loading

0 commit comments

Comments
 (0)