Skip to content
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
56 changes: 53 additions & 3 deletions autofit/aggregator/summary/aggregate_images.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from typing import Optional, List, Union, Callable, Type
from typing import Optional, List, Tuple, Union, Callable, Type
from pathlib import Path

from PIL import Image
Expand Down Expand Up @@ -104,6 +104,7 @@ def extract_image(
subplots: List[Union[Enum, List[Image.Image], Callable]],
subplot_width: Optional[int] = sys.maxsize,
transpose: bool = False,
panel_size: Optional[Tuple[int, int]] = None,
) -> Image.Image:
"""
Extract the images at the specified subplots and combine them into
Expand All @@ -126,6 +127,12 @@ def extract_image(
transpose
If True the output image is transposed before being returned, else it
is returned as is.
panel_size
If provided, all extracted panels are resized to this `(width, height)`
before compositing. If `None` (the default), panels are resized to the
maximum width and height across all panels, so that panels extracted
from source images with different grid layouts end up the same size
in the output.

Returns
-------
Expand All @@ -146,6 +153,8 @@ def extract_image(

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

matrix = self._normalize_panel_sizes(matrix, panel_size=panel_size)

return self._matrix_to_image(matrix)

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

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

for i, result in enumerate(self._aggregator):
image = self._matrix_to_image(
matrix = self._normalize_panel_sizes(
self._matrix_for_result(
i,
result,
subplots,
subplot_width=subplot_width,
)
),
panel_size=panel_size,
)
image = self._matrix_to_image(matrix)

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

return matrix

@staticmethod
def _normalize_panel_sizes(
matrix: List[List[Image.Image]],
panel_size: Optional[Tuple[int, int]] = None,
) -> List[List[Image.Image]]:
"""
Resize every panel in the matrix to a common `(width, height)` so that
panels extracted from source images with different grid layouts are the
same size when composited.

If `panel_size` is `None`, the target size is the maximum width and
height across all panels in the matrix. LANCZOS resampling is used to
preserve image quality.
"""
if not matrix:
return matrix

if panel_size is None:
max_width = max(image.width for row in matrix for image in row)
max_height = max(image.height for row in matrix for image in row)
target = (max_width, max_height)
else:
target = panel_size

return [
[
image if image.size == target else image.resize(target, Image.LANCZOS)
for image in row
]
for row in matrix
]

@staticmethod
def _matrix_to_image(matrix: List[List[Image.Image]]) -> Image.Image:
"""
Expand Down
27 changes: 25 additions & 2 deletions test_autofit/aggregator/summary_files/test_aggregate_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_custom_images(
]
)

assert result.size == (193, 120)
assert result.size == (244, 120)


def test_custom_function(aggregate):
Expand All @@ -168,7 +168,30 @@ def make_image(output):
]
)

assert result.size == (193, 120)
assert result.size == (244, 120)


def test_panel_size_explicit_normalizes_all_panels(aggregate, aggregator):
small = Image.new("RGB", (10, 10), "white")
images = [small for _ in aggregator]

result = aggregate.extract_image(
[SubplotFit.Data, images],
panel_size=(50, 50),
)

assert result.size == (100, 100)


def test_panel_size_defaults_to_max(aggregate, aggregator):
small = Image.new("RGB", (10, 10), "white")
images = [small for _ in aggregator]

result = aggregate.extract_image(
[SubplotFit.Data, images],
)

assert result.size == (122, 120)


def test_custom_subplot_fit(aggregate):
Expand Down
Loading