Skip to content

Commit 9e13b91

Browse files
committed
Port benchmarks from pillow-perf
1 parent 9fe855a commit 9e13b91

1 file changed

Lines changed: 343 additions & 0 deletions

File tree

Tests/benchmarks.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
"""
2+
pytest-benchmark tests for Pillow features.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import pathlib
8+
from io import BytesIO
9+
10+
import pytest
11+
12+
from PIL import Image, ImageFilter
13+
from PIL.Image import Resampling, Transpose
14+
15+
TYPE_CHECKING = False
16+
17+
if TYPE_CHECKING:
18+
from pytest_benchmark.fixture import (
19+
BenchmarkFixture, # type: ignore[import-not-found]
20+
)
21+
22+
pytest.importorskip(
23+
"pytest_benchmark",
24+
reason="benchmarks.py requires pytest-benchmark",
25+
)
26+
27+
# These can be adjusted to add more modes to benchmark
28+
# (however all features benchmarked might not support all PIL modes).
29+
MODES = ["RGB", "RGBA", "L", "LA"]
30+
31+
# The size for generated test images.
32+
# Note that adjusting this will naturally change how long operations take.
33+
# The `bench` fixture takes care of saving this information in the extra info
34+
# for the benchmark run, so that throughput (Mpx/s) can be recomputed in the future.
35+
SIZES = [(1024, 1024)]
36+
37+
# For benchmarks that act on test fixture files, these are the paths loaded.
38+
IMAGES_PATH = pathlib.Path(__file__).parent / "images"
39+
PATHS = [
40+
IMAGES_PATH / "flower2.jpg",
41+
]
42+
43+
# These are derived from the other configuration, above.
44+
RGB_MODES = [mode for mode in MODES if mode.startswith("RGB")]
45+
ALPHA_MODES = [mode for mode in MODES if mode.endswith("A")]
46+
47+
48+
def _format_size(size: tuple[int, int]) -> str:
49+
return f"{size[0]}x{size[1]}"
50+
51+
52+
def _format_path(path: pathlib.Path) -> str:
53+
return path.name
54+
55+
56+
@pytest.fixture
57+
def bench(
58+
request: pytest.FixtureRequest,
59+
benchmark: BenchmarkFixture,
60+
) -> BenchmarkFixture:
61+
"""
62+
pytest-benchmark with extra information.
63+
"""
64+
try:
65+
benchmark.extra_info["mode"] = request.getfixturevalue("mode")
66+
except LookupError:
67+
pass
68+
try:
69+
size = request.getfixturevalue("size")
70+
benchmark.extra_info["size"] = _format_size(size)
71+
benchmark.extra_info["pixels"] = size[0] * size[1]
72+
except LookupError:
73+
pass
74+
return benchmark
75+
76+
77+
def make_pillow_image(
78+
mode: str,
79+
size: tuple[int, int],
80+
pattern_offset: int = 0,
81+
) -> Image.Image:
82+
im = Image.new("RGB", size)
83+
n = im.width * im.height * 3
84+
period = bytes((i + pattern_offset) % 256 for i in range(256))
85+
im.frombytes((period * (n // 256 + 1))[:n])
86+
return im.convert(mode)
87+
88+
89+
@pytest.mark.benchmark(group="composition")
90+
@pytest.mark.parametrize("mode", MODES)
91+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
92+
def test_blend(
93+
bench: BenchmarkFixture,
94+
mode: str,
95+
size: tuple[int, int],
96+
) -> None:
97+
im1 = make_pillow_image(mode, size)
98+
im2 = make_pillow_image(mode, size, pattern_offset=1024)
99+
result = bench(Image.blend, im1, im2, 0.5)
100+
assert result.size == im1.size
101+
102+
103+
@pytest.mark.benchmark(group="scale")
104+
@pytest.mark.parametrize("resampler", Resampling, ids=lambda r: r.name)
105+
@pytest.mark.parametrize("scale", [0.01, 0.125, 0.8, 2.14])
106+
@pytest.mark.parametrize("mode", MODES)
107+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
108+
def test_scale(
109+
bench: BenchmarkFixture,
110+
mode: str,
111+
size: tuple[int, int],
112+
scale: float,
113+
resampler: Resampling,
114+
) -> None:
115+
im = make_pillow_image(mode, size)
116+
dest = (round(scale * im.width), round(scale * im.height))
117+
bench.extra_info["label"] = [f"{dest[0]}x{dest[1]}", resampler.name]
118+
bench(im.resize, dest, resampler)
119+
120+
121+
@pytest.mark.benchmark(group="blur")
122+
@pytest.mark.parametrize("radius", [1, 10, 30])
123+
@pytest.mark.parametrize("mode", MODES)
124+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
125+
def test_box_blur(
126+
bench: BenchmarkFixture,
127+
mode: str,
128+
size: tuple[int, int],
129+
radius: int,
130+
) -> None:
131+
im = make_pillow_image(mode, size)
132+
bench.extra_info["label"] = [f"{radius}px"]
133+
bench(im.filter, ImageFilter.BoxBlur(radius))
134+
135+
136+
@pytest.mark.benchmark(group="composition")
137+
@pytest.mark.parametrize("mode", ALPHA_MODES)
138+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
139+
def test_alpha_composition(
140+
bench: BenchmarkFixture,
141+
mode: str,
142+
size: tuple[int, int],
143+
) -> None:
144+
im = make_pillow_image(mode, size)
145+
second = im.copy()
146+
bench.extra_info["label"] = ["Composition"]
147+
bench(Image.alpha_composite, im, second)
148+
149+
150+
@pytest.mark.benchmark(group="convert")
151+
@pytest.mark.parametrize(
152+
"mode_from, mode_to",
153+
[
154+
("RGB", "L"),
155+
("RGBA", "LA"),
156+
("RGBa", "RGBA"),
157+
("RGBA", "RGBa"),
158+
],
159+
)
160+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
161+
def test_convert(
162+
bench: BenchmarkFixture,
163+
mode_from: str,
164+
mode_to: str,
165+
size: tuple[int, int],
166+
) -> None:
167+
im = make_pillow_image(mode_from, size)
168+
bench.extra_info["label"] = [f"{mode_from} to {mode_to}"]
169+
bench(im.convert, mode_to)
170+
171+
172+
@pytest.mark.benchmark(group="crop")
173+
@pytest.mark.parametrize(
174+
"scale",
175+
[
176+
(0.9, 0.9),
177+
(1.1, 1.1),
178+
(1.1, 0.9),
179+
],
180+
)
181+
@pytest.mark.parametrize("mode", MODES)
182+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
183+
def test_crop(
184+
bench: BenchmarkFixture,
185+
mode: str,
186+
size: tuple[int, int],
187+
scale: tuple[float, float],
188+
) -> None:
189+
im = make_pillow_image(mode, size)
190+
w, h = im.size
191+
width, height = round(scale[0] * w), round(scale[1] * h)
192+
left = (w - width) // 2
193+
top = (h - height) // 2
194+
box = (left, top, left + width, top + height)
195+
bench.extra_info["label"] = [f"{width}x{height}"]
196+
bench(im.crop, box)
197+
198+
199+
@pytest.mark.benchmark(group="filter")
200+
@pytest.mark.parametrize(
201+
"filter",
202+
[
203+
ImageFilter.SMOOTH,
204+
ImageFilter.SHARPEN,
205+
ImageFilter.SMOOTH_MORE,
206+
],
207+
ids=lambda f: f.name,
208+
)
209+
@pytest.mark.parametrize("mode", MODES)
210+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
211+
def test_filter(
212+
bench: BenchmarkFixture,
213+
mode: str,
214+
size: tuple[int, int],
215+
filter: type[ImageFilter.BuiltinFilter],
216+
) -> None:
217+
im = make_pillow_image(mode, size)
218+
bench.extra_info["label"] = [filter.name]
219+
bench(im.filter, filter)
220+
221+
222+
@pytest.mark.benchmark(group="lut")
223+
@pytest.mark.parametrize(
224+
"channels, table_size",
225+
[
226+
(3, 4),
227+
(3, 16),
228+
(3, 36),
229+
(4, 4),
230+
(4, 16),
231+
(4, 36),
232+
],
233+
)
234+
@pytest.mark.parametrize("mode", RGB_MODES)
235+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
236+
def test_lut(
237+
bench: BenchmarkFixture,
238+
mode: str,
239+
size: tuple[int, int],
240+
channels: int,
241+
table_size: int,
242+
) -> None:
243+
im = make_pillow_image(mode, size)
244+
if channels == 3:
245+
lut = ImageFilter.Color3DLUT.generate(
246+
table_size, lambda r, g, b: (r, g, b), channels, "RGB"
247+
)
248+
else:
249+
lut = ImageFilter.Color3DLUT.generate(
250+
table_size, lambda r, g, b: (r, g, b, r), channels, "RGBA"
251+
)
252+
253+
bench.extra_info["label"] = [f"{table_size}³ table to {channels}D"]
254+
bench(im.filter, lut)
255+
256+
257+
@pytest.mark.benchmark(group="rotate_right")
258+
@pytest.mark.parametrize("op", Transpose, ids=lambda t: t.name)
259+
@pytest.mark.parametrize("mode", MODES)
260+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
261+
def test_rotate_right(
262+
bench: BenchmarkFixture,
263+
mode: str,
264+
size: tuple[int, int],
265+
op: Transpose,
266+
) -> None:
267+
im = make_pillow_image(mode, size)
268+
bench.extra_info["label"] = [op.name]
269+
bench(im.transpose, op)
270+
271+
272+
@pytest.mark.benchmark(group="load")
273+
@pytest.mark.parametrize("path", PATHS, ids=_format_path)
274+
def test_load(bench: BenchmarkFixture, path: pathlib.Path) -> None:
275+
def run() -> None:
276+
im = Image.open(path)
277+
im.load()
278+
279+
bench(run)
280+
281+
282+
@pytest.mark.benchmark(group="save")
283+
@pytest.mark.parametrize("path", PATHS, ids=_format_path)
284+
def test_save_jpeg(bench: BenchmarkFixture, path: pathlib.Path) -> None:
285+
im = Image.open(path)
286+
im.load()
287+
bench(lambda: im.save(BytesIO(), format="JPEG", quality=85))
288+
289+
290+
@pytest.mark.benchmark(group="allocate")
291+
@pytest.mark.parametrize("mode", MODES)
292+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
293+
def test_allocate(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
294+
bench.extra_info["label"] = [f"mode {mode}"]
295+
bench(Image.new, mode, size)
296+
297+
298+
@pytest.mark.benchmark(group="allocate")
299+
@pytest.mark.parametrize("mode", MODES)
300+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
301+
def test_unpack(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
302+
im = make_pillow_image(mode, size)
303+
data = im.tobytes()
304+
bench.extra_info["label"] = [f"Unpack from {mode}"]
305+
bench(im.frombytes, data)
306+
307+
308+
@pytest.mark.benchmark(group="allocate")
309+
@pytest.mark.parametrize("mode", MODES)
310+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
311+
def test_pack(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
312+
im = make_pillow_image(mode, size)
313+
bench.extra_info["label"] = [f"Pack to {mode}"]
314+
bench(im.tobytes)
315+
316+
317+
@pytest.mark.benchmark(group="allocate")
318+
@pytest.mark.parametrize("mode", MODES)
319+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
320+
def test_split(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
321+
im = make_pillow_image(mode, size)
322+
bench.extra_info["label"] = [f"split {mode}"]
323+
bench(im.split)
324+
325+
326+
@pytest.mark.benchmark(group="allocate")
327+
@pytest.mark.parametrize("mode", MODES)
328+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
329+
def test_getband(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
330+
im = make_pillow_image(mode, size)
331+
band = len(im.getbands()) - 1
332+
bench.extra_info["label"] = [f"get {mode[band]} of {mode}"]
333+
bench(im.getchannel, band)
334+
335+
336+
@pytest.mark.benchmark(group="allocate")
337+
@pytest.mark.parametrize("mode", MODES)
338+
@pytest.mark.parametrize("size", SIZES, ids=_format_size)
339+
def test_merge(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None:
340+
im = make_pillow_image(mode, size)
341+
bands = im.split()
342+
bench.extra_info["label"] = [f"merge {mode}"]
343+
bench(Image.merge, mode, bands)

0 commit comments

Comments
 (0)