Skip to content

Commit 3c7080c

Browse files
akxradarhere
andauthored
Speed up Image.filter() for I;16* (#9760)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
1 parent e7f9779 commit 3c7080c

5 files changed

Lines changed: 127 additions & 103 deletions

File tree

Tests/test_image_filter.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@
88

99
from .helper import assert_image_equal, hopper
1010

11+
MODES = (
12+
"L",
13+
"LA",
14+
"La",
15+
"I",
16+
"I;16",
17+
"I;16B",
18+
"I;16L",
19+
"I;16N",
20+
"RGB",
21+
"RGBA",
22+
"RGBa",
23+
"RGBX",
24+
"CMYK",
25+
)
26+
1127

1228
@pytest.mark.parametrize(
1329
"filter_to_apply",
@@ -37,9 +53,7 @@
3753
ImageFilter.UnsharpMask(10),
3854
),
3955
)
40-
@pytest.mark.parametrize(
41-
"mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
42-
)
56+
@pytest.mark.parametrize("mode", MODES)
4357
def test_sanity(
4458
filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str
4559
) -> None:
@@ -53,20 +67,24 @@ def test_sanity(
5367
assert out.size == im.size
5468

5569

56-
@pytest.mark.parametrize(
57-
"mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
58-
)
70+
@pytest.mark.parametrize("mode", MODES)
5971
def test_sanity_error(mode: str) -> None:
6072
im = hopper(mode)
6173
with pytest.raises(TypeError):
6274
im.filter("hello") # type: ignore[arg-type]
6375

6476

65-
# crashes on small images
66-
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
67-
def test_crash(size: tuple[int, int]) -> None:
68-
im = Image.new("RGB", size)
69-
im.filter(ImageFilter.SMOOTH)
77+
def test_noop_on_small_images() -> None:
78+
# If image is smaller than the kernel size, return it as-is.
79+
kernel_size: tuple[int, int] = ImageFilter.SMOOTH_MORE.filterargs[0]
80+
kernel_w, kernel_h = kernel_size
81+
for w in range(1, kernel_w):
82+
for h in range(1, kernel_h):
83+
im = hopper("RGB").resize((w, h))
84+
# Precondition for the below equality test:
85+
# filter is larger or equal to image.
86+
assert im.size < kernel_size
87+
assert_image_equal(im.filter(ImageFilter.SMOOTH_MORE), im)
7088

7189

7290
@pytest.mark.parametrize(
@@ -174,38 +192,39 @@ def test_kernel_not_enough_coefficients() -> None:
174192
ImageFilter.Kernel((3, 3), (0, 0))
175193

176194

177-
@pytest.mark.parametrize(
178-
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
179-
)
180-
def test_consistency_3x3(mode: str) -> None:
181-
matrix = (
195+
EMBOSS_MATRIX = {
196+
3: (
182197
-1, -1, 0,
183198
-1, 0, 1,
184199
0, 1, 1,
185-
) # fmt: skip
186-
with Image.open("Tests/images/hopper.bmp") as source:
187-
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
188-
kernel = ImageFilter.Kernel((3, 3), matrix, 0.3)
189-
assert_image_equal(source.filter(kernel), reference)
190-
191-
192-
@pytest.mark.parametrize(
193-
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
194-
)
195-
def test_consistency_5x5(mode: str) -> None:
196-
matrix = (
200+
),
201+
5: (
197202
-1, -1, -1, -1, 0,
198203
-1, -1, -1, 0, 1,
199204
-1, -1, 0, 1, 1,
200205
-1, 0, 1, 1, 1,
201206
0, 1, 1, 1, 1,
202-
) # fmt: skip
207+
),
208+
} # fmt: skip
209+
210+
211+
@pytest.mark.parametrize("size", (3, 5))
212+
def test_consistency(size: int) -> None:
213+
kernel = ImageFilter.Kernel((size, size), EMBOSS_MATRIX[size], 0.3)
203214
with Image.open("Tests/images/hopper.bmp") as source:
204-
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
205-
kernel = ImageFilter.Kernel((5, 5), matrix, 0.3)
215+
with Image.open(f"Tests/images/hopper_emboss_{size}x{size}.bmp") as reference:
206216
assert_image_equal(source.filter(kernel), reference)
207217

208218

219+
@pytest.mark.parametrize("size", (3, 5))
220+
@pytest.mark.parametrize("mode", ("I;16", "I;16L", "I;16B", "I;16N"))
221+
def test_consistency_i16(size: int, mode: str) -> None:
222+
kernel = ImageFilter.Kernel((size, size), EMBOSS_MATRIX[size], 0.3)
223+
reference = hopper("I").filter(kernel)
224+
result = hopper(mode).filter(kernel)
225+
assert_image_equal(result.convert("I"), reference)
226+
227+
209228
@pytest.mark.parametrize("mode", ("I;16", "I;16L", "I;16B", "I;16N"))
210229
def test_consistency_i16_high_byte(mode: str) -> None:
211230
# Exercise filters with a 16bpc image that has content in the high byte, too.

src/PIL/ImageFilter.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,8 @@ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
5353

5454
class Kernel(BuiltinFilter):
5555
"""
56-
Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating
57-
point kernels.
58-
59-
Kernels can only be applied to "L" and "RGB" images.
56+
Create a convolution kernel.
57+
This only supports 3x3 and 5x5 integer and floating point kernels.
6058
6159
:param size: Kernel size, given as (width, height). This must be (3,3) or (5,5).
6260
:param kernel: A sequence containing kernel weights. The kernel will be flipped

src/libImaging/Filter.c

Lines changed: 75 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,15 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
161161
}
162162
out[x] = in0[x];
163163
}
164-
} else {
164+
} else if (im->type == IMAGING_TYPE_I16) {
165165
int bigendian = 0;
166-
if (im->type == IMAGING_TYPE_I16) {
167-
if (
168-
im->mode == IMAGING_MODE_I_16B
166+
if (
167+
im->mode == IMAGING_MODE_I_16B
169168
#ifdef WORDS_BIGENDIAN
170-
|| im->mode == IMAGING_MODE_I_16N
169+
|| im->mode == IMAGING_MODE_I_16N
171170
#endif
172-
) {
173-
bigendian = 1;
174-
}
171+
) {
172+
bigendian = 1;
175173
}
176174
for (y = 1; y < ysize - 1; y++) {
177175
UINT8 *restrict in_1 = (UINT8 *)im->image[y - 1];
@@ -180,32 +178,36 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
180178
UINT8 *restrict out = (UINT8 *)imOut->image[y];
181179

182180
out[0] = in0[0];
183-
if (im->type == IMAGING_TYPE_I16) {
184-
out[1] = in0[1];
185-
}
181+
out[1] = in0[1];
186182
for (x = 1; x < xsize - 1; x++) {
187183
float ss = offset;
188-
if (im->type == IMAGING_TYPE_I16) {
189-
ss += kernel_i16(3, in1, x, &kernel[0], bigendian);
190-
ss += kernel_i16(3, in0, x, &kernel[3], bigendian);
191-
ss += kernel_i16(3, in_1, x, &kernel[6], bigendian);
192-
// NOT rounding here because `offset` already has a +0.5 bias.
193-
int ss_int = clip16(ss);
194-
out[x * 2 + (bigendian ? 1 : 0)] = (UINT8)(ss_int & 0xff);
195-
out[x * 2 + (bigendian ? 0 : 1)] = (UINT8)(ss_int >> 8);
196-
} else {
197-
ss += KERNEL1x3(in1, x, &kernel[0], 1);
198-
ss += KERNEL1x3(in0, x, &kernel[3], 1);
199-
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
200-
out[x] = clip8(ss);
201-
}
184+
ss += kernel_i16(3, in1, x, &kernel[0], bigendian);
185+
ss += kernel_i16(3, in0, x, &kernel[3], bigendian);
186+
ss += kernel_i16(3, in_1, x, &kernel[6], bigendian);
187+
// NOT rounding here because `offset` already has a +0.5 bias.
188+
int ss_int = clip16(ss);
189+
out[x * 2 + (bigendian ? 1 : 0)] = (UINT8)(ss_int & 0xff);
190+
out[x * 2 + (bigendian ? 0 : 1)] = (UINT8)(ss_int >> 8);
202191
}
203-
if (im->type == IMAGING_TYPE_I16) {
204-
out[x * 2] = in0[x * 2];
205-
out[x * 2 + 1] = in0[x * 2 + 1];
206-
} else {
207-
out[x] = in0[x];
192+
out[x * 2] = in0[x * 2];
193+
out[x * 2 + 1] = in0[x * 2 + 1];
194+
}
195+
} else {
196+
for (y = 1; y < ysize - 1; y++) {
197+
UINT8 *restrict in_1 = (UINT8 *)im->image[y - 1];
198+
UINT8 *restrict in0 = (UINT8 *)im->image[y];
199+
UINT8 *restrict in1 = (UINT8 *)im->image[y + 1];
200+
UINT8 *restrict out = (UINT8 *)imOut->image[y];
201+
202+
out[0] = in0[0];
203+
for (x = 1; x < xsize - 1; x++) {
204+
float ss = offset;
205+
ss += KERNEL1x3(in1, x, &kernel[0], 1);
206+
ss += KERNEL1x3(in0, x, &kernel[3], 1);
207+
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
208+
out[x] = clip8(ss);
208209
}
210+
out[x] = in0[x];
209211
}
210212
}
211213
} else {
@@ -322,17 +324,15 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
322324
out[x + 0] = in0[x + 0];
323325
out[x + 1] = in0[x + 1];
324326
}
325-
} else {
327+
} else if (im->type == IMAGING_TYPE_I16) {
326328
int bigendian = 0;
327-
if (im->type == IMAGING_TYPE_I16) {
328-
if (
329-
im->mode == IMAGING_MODE_I_16B
329+
if (
330+
im->mode == IMAGING_MODE_I_16B
330331
#ifdef WORDS_BIGENDIAN
331-
|| im->mode == IMAGING_MODE_I_16N
332+
|| im->mode == IMAGING_MODE_I_16N
332333
#endif
333-
) {
334-
bigendian = 1;
335-
}
334+
) {
335+
bigendian = 1;
336336
}
337337
for (y = 2; y < ysize - 2; y++) {
338338
UINT8 *restrict in_2 = (UINT8 *)im->image[y - 2];
@@ -344,40 +344,47 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
344344

345345
out[0] = in0[0];
346346
out[1] = in0[1];
347-
if (im->type == IMAGING_TYPE_I16) {
348-
out[2] = in0[2];
349-
out[3] = in0[3];
350-
}
347+
out[2] = in0[2];
348+
out[3] = in0[3];
351349
for (x = 2; x < xsize - 2; x++) {
352350
float ss = offset;
353-
if (im->type == IMAGING_TYPE_I16) {
354-
ss += kernel_i16(5, in2, x, &kernel[0], bigendian);
355-
ss += kernel_i16(5, in1, x, &kernel[5], bigendian);
356-
ss += kernel_i16(5, in0, x, &kernel[10], bigendian);
357-
ss += kernel_i16(5, in_1, x, &kernel[15], bigendian);
358-
ss += kernel_i16(5, in_2, x, &kernel[20], bigendian);
359-
// NOT rounding here because `offset` already has a +0.5 bias.
360-
int ss_int = clip16(ss);
361-
out[x * 2 + (bigendian ? 1 : 0)] = (UINT8)(ss_int & 0xff);
362-
out[x * 2 + (bigendian ? 0 : 1)] = (UINT8)(ss_int >> 8);
363-
} else {
364-
ss += KERNEL1x5(in2, x, &kernel[0], 1);
365-
ss += KERNEL1x5(in1, x, &kernel[5], 1);
366-
ss += KERNEL1x5(in0, x, &kernel[10], 1);
367-
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
368-
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
369-
out[x] = clip8(ss);
370-
}
351+
ss += kernel_i16(5, in2, x, &kernel[0], bigendian);
352+
ss += kernel_i16(5, in1, x, &kernel[5], bigendian);
353+
ss += kernel_i16(5, in0, x, &kernel[10], bigendian);
354+
ss += kernel_i16(5, in_1, x, &kernel[15], bigendian);
355+
ss += kernel_i16(5, in_2, x, &kernel[20], bigendian);
356+
// NOT rounding here because `offset` already has a +0.5 bias.
357+
int ss_int = clip16(ss);
358+
out[x * 2 + (bigendian ? 1 : 0)] = (UINT8)(ss_int & 0xff);
359+
out[x * 2 + (bigendian ? 0 : 1)] = (UINT8)(ss_int >> 8);
371360
}
372-
if (im->type == IMAGING_TYPE_I16) {
373-
out[x * 2 + 0] = in0[x * 2 + 0];
374-
out[x * 2 + 1] = in0[x * 2 + 1];
375-
out[x * 2 + 2] = in0[x * 2 + 2];
376-
out[x * 2 + 3] = in0[x * 2 + 3];
377-
} else {
378-
out[x + 0] = in0[x + 0];
379-
out[x + 1] = in0[x + 1];
361+
out[x * 2 + 0] = in0[x * 2 + 0];
362+
out[x * 2 + 1] = in0[x * 2 + 1];
363+
out[x * 2 + 2] = in0[x * 2 + 2];
364+
out[x * 2 + 3] = in0[x * 2 + 3];
365+
}
366+
} else {
367+
for (y = 2; y < ysize - 2; y++) {
368+
UINT8 *restrict in_2 = (UINT8 *)im->image[y - 2];
369+
UINT8 *restrict in_1 = (UINT8 *)im->image[y - 1];
370+
UINT8 *restrict in0 = (UINT8 *)im->image[y];
371+
UINT8 *restrict in1 = (UINT8 *)im->image[y + 1];
372+
UINT8 *restrict in2 = (UINT8 *)im->image[y + 2];
373+
UINT8 *restrict out = (UINT8 *)imOut->image[y];
374+
375+
out[0] = in0[0];
376+
out[1] = in0[1];
377+
for (x = 2; x < xsize - 2; x++) {
378+
float ss = offset;
379+
ss += KERNEL1x5(in2, x, &kernel[0], 1);
380+
ss += KERNEL1x5(in1, x, &kernel[5], 1);
381+
ss += KERNEL1x5(in0, x, &kernel[10], 1);
382+
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
383+
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
384+
out[x] = clip8(ss);
380385
}
386+
out[x + 0] = in0[x + 0];
387+
out[x + 1] = in0[x + 1];
381388
}
382389
}
383390
} else {

0 commit comments

Comments
 (0)