Skip to content

Commit 5a3c22d

Browse files
Jammy2211claude
authored andcommitted
fix: normalize panel sizes in AggregateImages composite output
Panels extracted from source images with different grid layouts (e.g. a 2x2 rgb.png and a 4x3 subplot_fit.png) previously ended up at different pixel dimensions, so the RGB panel appeared half the size of the fit panels in the composite. Add `_normalize_panel_sizes` that resizes every panel to a common target size with LANCZOS resampling before compositing. The target is the max width/height across all panels by default, or an explicit `panel_size` `(width, height)` tuple passed to `extract_image` / `output_to_folder`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 37bea54 commit 5a3c22d

2 files changed

Lines changed: 78 additions & 5 deletions

File tree

autofit/aggregator/summary/aggregate_images.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
2-
from typing import Optional, List, Union, Callable, Type
2+
from typing import Optional, List, Tuple, Union, Callable, Type
33
from pathlib import Path
44

55
from PIL import Image
@@ -104,6 +104,7 @@ def extract_image(
104104
subplots: List[Union[Enum, List[Image.Image], Callable]],
105105
subplot_width: Optional[int] = sys.maxsize,
106106
transpose: bool = False,
107+
panel_size: Optional[Tuple[int, int]] = None,
107108
) -> Image.Image:
108109
"""
109110
Extract the images at the specified subplots and combine them into
@@ -126,6 +127,12 @@ def extract_image(
126127
transpose
127128
If True the output image is transposed before being returned, else it
128129
is returned as is.
130+
panel_size
131+
If provided, all extracted panels are resized to this `(width, height)`
132+
before compositing. If `None` (the default), panels are resized to the
133+
maximum width and height across all panels, so that panels extracted
134+
from source images with different grid layouts end up the same size
135+
in the output.
129136
130137
Returns
131138
-------
@@ -146,6 +153,8 @@ def extract_image(
146153

147154
matrix = [list(row) for row in zip(*matrix)]
148155

156+
matrix = self._normalize_panel_sizes(matrix, panel_size=panel_size)
157+
149158
return self._matrix_to_image(matrix)
150159

151160
def output_to_folder(
@@ -154,6 +163,7 @@ def output_to_folder(
154163
name: Union[str, List[str]],
155164
subplots: List[Union[List[Image.Image], Callable]],
156165
subplot_width: Optional[int] = sys.maxsize,
166+
panel_size: Optional[Tuple[int, int]] = None,
157167
):
158168
"""
159169
Output one subplot image for each fit in the aggregator.
@@ -176,21 +186,29 @@ def output_to_folder(
176186
name
177187
The attribute of each fit to use as the name of the output file.
178188
OR a list of names, one for each fit.
189+
panel_size
190+
If provided, all extracted panels are resized to this `(width, height)`
191+
before compositing. If `None` (the default), panels are resized to the
192+
maximum width and height across all panels, so that panels extracted
193+
from source images with different grid layouts end up the same size
194+
in the output.
179195
"""
180196
if len(subplots) == 0:
181197
raise ValueError("At least one subplot must be provided.")
182198

183199
folder.mkdir(exist_ok=True, parents=True)
184200

185201
for i, result in enumerate(self._aggregator):
186-
image = self._matrix_to_image(
202+
matrix = self._normalize_panel_sizes(
187203
self._matrix_for_result(
188204
i,
189205
result,
190206
subplots,
191207
subplot_width=subplot_width,
192-
)
208+
),
209+
panel_size=panel_size,
193210
)
211+
image = self._matrix_to_image(matrix)
194212

195213
if isinstance(name, str):
196214
output_name = getattr(result, name)
@@ -292,6 +310,38 @@ class name but using snake_case.
292310

293311
return matrix
294312

313+
@staticmethod
314+
def _normalize_panel_sizes(
315+
matrix: List[List[Image.Image]],
316+
panel_size: Optional[Tuple[int, int]] = None,
317+
) -> List[List[Image.Image]]:
318+
"""
319+
Resize every panel in the matrix to a common `(width, height)` so that
320+
panels extracted from source images with different grid layouts are the
321+
same size when composited.
322+
323+
If `panel_size` is `None`, the target size is the maximum width and
324+
height across all panels in the matrix. LANCZOS resampling is used to
325+
preserve image quality.
326+
"""
327+
if not matrix:
328+
return matrix
329+
330+
if panel_size is None:
331+
max_width = max(image.width for row in matrix for image in row)
332+
max_height = max(image.height for row in matrix for image in row)
333+
target = (max_width, max_height)
334+
else:
335+
target = panel_size
336+
337+
return [
338+
[
339+
image if image.size == target else image.resize(target, Image.LANCZOS)
340+
for image in row
341+
]
342+
for row in matrix
343+
]
344+
295345
@staticmethod
296346
def _matrix_to_image(matrix: List[List[Image.Image]]) -> Image.Image:
297347
"""

test_autofit/aggregator/summary_files/test_aggregate_images.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def test_custom_images(
152152
]
153153
)
154154

155-
assert result.size == (193, 120)
155+
assert result.size == (244, 120)
156156

157157

158158
def test_custom_function(aggregate):
@@ -168,7 +168,30 @@ def make_image(output):
168168
]
169169
)
170170

171-
assert result.size == (193, 120)
171+
assert result.size == (244, 120)
172+
173+
174+
def test_panel_size_explicit_normalizes_all_panels(aggregate, aggregator):
175+
small = Image.new("RGB", (10, 10), "white")
176+
images = [small for _ in aggregator]
177+
178+
result = aggregate.extract_image(
179+
[SubplotFit.Data, images],
180+
panel_size=(50, 50),
181+
)
182+
183+
assert result.size == (100, 100)
184+
185+
186+
def test_panel_size_defaults_to_max(aggregate, aggregator):
187+
small = Image.new("RGB", (10, 10), "white")
188+
images = [small for _ in aggregator]
189+
190+
result = aggregate.extract_image(
191+
[SubplotFit.Data, images],
192+
)
193+
194+
assert result.size == (122, 120)
172195

173196

174197
def test_custom_subplot_fit(aggregate):

0 commit comments

Comments
 (0)