From 6a12853f834578b1a94defb209672c85557f5deb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 22 Sep 2026 22:31:15 +0300 Subject: [PATCH 1/3] Make ImagingExpand work correctly for I16 images, apply hoist optimizations --- Tests/test_image_filter.py | 18 +++++++++++ src/libImaging/Filter.c | 65 ++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5f5eb302cd6..0b0539fb4d8 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -209,6 +209,24 @@ def test_rankfilter_overflow() -> None: im.filter(rankfilter) +@pytest.mark.parametrize( + "mode, fill", + ( + ("L", 0x12), + ("I", 0x12345678), + ("I;16", 0x1234), + ("I;16B", 0x1234), + ("F", 1.5), + ("RGB", (0x12, 0x34, 0x56)), + ), +) +@pytest.mark.parametrize("margin", (0, 1, 3)) +def test_expand(mode: str, fill: float | tuple[int, ...], margin: int) -> None: + im = Image.new(mode, (5, 4), fill) + out = im._new(im.im.expand(margin)) # As done internally by RankFilter + assert_image_equal(out, Image.new(mode, (5 + 2 * margin, 4 + 2 * margin), fill)) + + def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index e61fefc693a..2a4982518ad 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -70,10 +70,10 @@ ImagingExpand(Imaging imIn, int margin) { return (Imaging)ImagingError_ValueError("filter size too large"); } - int xsize = imIn->xsize + 2 * margin; - int ysize = imIn->ysize + 2 * margin; + int out_xsize = imIn->xsize + 2 * margin; + int out_ysize = imIn->ysize + 2 * margin; if (imIn->xsize == 0 || imIn->ysize == 0) { - imOut = ImagingNew(imIn->mode, xsize, ysize); + imOut = ImagingNew(imIn->mode, out_xsize, out_ysize); if (!imOut) { return NULL; } @@ -81,45 +81,50 @@ ImagingExpand(Imaging imIn, int margin) { return imOut; } - imOut = ImagingNewDirty(imIn->mode, xsize, ysize); + imOut = ImagingNewDirty(imIn->mode, out_xsize, out_ysize); if (!imOut) { return NULL; } -#define EXPAND_LINE(type, image, yin, yout) \ - { \ - for (x = 0; x < margin; x++) { \ - imOut->image[yout][x] = imIn->image[yin][0]; \ - } \ - for (x = 0; x < imIn->xsize; x++) { \ - imOut->image[yout][x + margin] = imIn->image[yin][x]; \ - } \ - for (x = 0; x < margin; x++) { \ - imOut->image[yout][margin + imIn->xsize + x] = \ - imIn->image[yin][imIn->xsize - 1]; \ - } \ + int xsize = imIn->xsize, ysize = imIn->ysize; + +#define EXPAND_LINE(type, yin, yout) \ + { \ + const type *in = (const type *)imIn->image[yin]; \ + type *out = (type *)imOut->image[yout]; \ + for (x = 0; x < margin; x++) { \ + out[x] = in[0]; \ + } \ + for (x = 0; x < xsize; x++) { \ + out[x + margin] = in[x]; \ + } \ + for (x = 0; x < margin; x++) { \ + out[margin + xsize + x] = in[xsize - 1]; \ + } \ } -#define EXPAND(type, image) \ - { \ - for (y = 0; y < margin; y++) { \ - EXPAND_LINE(type, image, 0, y); \ - } \ - for (y = 0; y < imIn->ysize; y++) { \ - EXPAND_LINE(type, image, y, y + margin); \ - } \ - for (y = 0; y < margin; y++) { \ - EXPAND_LINE(type, image, imIn->ysize - 1, margin + imIn->ysize + y); \ - } \ +#define EXPAND(type) \ + { \ + for (y = 0; y < margin; y++) { \ + EXPAND_LINE(type, 0, y); \ + } \ + for (y = 0; y < ysize; y++) { \ + EXPAND_LINE(type, y, y + margin); \ + } \ + for (y = 0; y < margin; y++) { \ + EXPAND_LINE(type, ysize - 1, margin + ysize + y); \ + } \ } ImagingSectionEnter(&cookie); MUTEX_LOCK(&imIn->mutex); - if (imIn->image8) { - EXPAND(UINT8, image8); + if (imIn->type == IMAGING_TYPE_I16) { + EXPAND(UINT16); + } else if (imIn->image8) { + EXPAND(UINT8); } else { - EXPAND(INT32, image32); + EXPAND(INT32); } MUTEX_UNLOCK(&imIn->mutex); ImagingSectionLeave(&cookie); From 288dc931dff43d9bbf5f284b35d893ec60ab325d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 22 Sep 2026 21:54:59 +0300 Subject: [PATCH 2/3] Clean up and speed up RankFilter This removes the `IMAGING_PIXEL_(type)` macros from `Imaging.h` too; they were only used in `RankFilter.c` and in one place in `_imaging.c`. --- src/_imaging.c | 4 +- src/libImaging/Imaging.h | 4 -- src/libImaging/RankFilter.c | 135 +++++++++++++++++++----------------- 3 files changed, 74 insertions(+), 69 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 8644d10a067..81bd410a12b 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1924,7 +1924,7 @@ _putdata_impl(ImagingObject *self, PyObject *args) { for (i = x = y = 0; i < n; i++) { double value; set_value_to_item(seq, i); - IMAGING_PIXEL_INT32(image, x, y) = (INT32)(value * scale + offset); + image->image32[y][x] = (INT32)(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; } @@ -1934,7 +1934,7 @@ _putdata_impl(ImagingObject *self, PyObject *args) { for (i = x = y = 0; i < n; i++) { double value; set_value_to_item(seq, i); - IMAGING_PIXEL_FLOAT32(image, x, y) = + ((FLOAT32 *)image->image32[y])[x] = (FLOAT32)(value * scale + offset); if (++x >= (int)image->xsize) { x = 0, y++; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 68b7027e5f6..4bc36d1d3fd 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -132,10 +132,6 @@ struct ImagingMemoryInstance { #define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x) * 4]) #define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x) * 4]) -#define IMAGING_PIXEL_UINT8(im, x, y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_INT32(im, x, y) ((im)->image32[(y)][(x)]) -#define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) - struct ImagingAccessInstance { ModeID mode; void (*get_pixel)(Imaging im, int x, int y, void *pixel); diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 5c0a09d877c..7f4ddd86f03 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -17,53 +17,59 @@ /* Fast rank algorithm (due to Wirth), based on public domain code by Nicolas Devillard, available at http://ndevilla.free.fr */ -#define SWAP(type, a, b) \ - { \ - register type t = (a); \ - (a) = (b); \ - (b) = t; \ - } +#define RANK_INNER_BODY(type) \ + int i, j, l, m; \ + type x; \ + l = 0; \ + m = n - 1; \ + while (l < m) { \ + x = a[k]; \ + i = l; \ + j = m; \ + do { \ + while (a[i] < x) { \ + i++; \ + } \ + while (x < a[j]) { \ + j--; \ + } \ + if (i <= j) { \ + type t = a[i]; \ + a[i] = a[j]; \ + a[j] = t; \ + i++; \ + j--; \ + } \ + } while (i <= j); \ + if (j < k) { \ + l = i; \ + } \ + if (k < i) { \ + m = j; \ + } \ + } \ + return a[k] + +static UINT8 +RankUINT8(UINT8 a[], int n, int k) { + RANK_INNER_BODY(UINT8); +} -#define MakeRankFunction(type) \ - static type Rank##type(type a[], int n, int k) { \ - register int i, j, l, m; \ - register type x; \ - l = 0; \ - m = n - 1; \ - while (l < m) { \ - x = a[k]; \ - i = l; \ - j = m; \ - do { \ - while (a[i] < x) { \ - i++; \ - } \ - while (x < a[j]) { \ - j--; \ - } \ - if (i <= j) { \ - SWAP(type, a[i], a[j]); \ - i++; \ - j--; \ - } \ - } while (i <= j); \ - if (j < k) { \ - l = i; \ - } \ - if (k < i) { \ - m = j; \ - } \ - } \ - return a[k]; \ - } +static INT32 +RankINT32(INT32 a[], int n, int k) { + RANK_INNER_BODY(INT32); +} -MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) +static FLOAT32 +RankFLOAT32(FLOAT32 a[], int n, int k) { + RANK_INNER_BODY(FLOAT32); +} - Imaging ImagingRankFilter(Imaging im, int size, int rank) { +Imaging +ImagingRankFilter(Imaging im, int size, int rank) { Imaging imOut = NULL; ImagingSectionCookie cookie; - int x, y; - int i, margin, size2; + int margin, size2; if (!im || im->bands != 1 || im->type == IMAGING_TYPE_I16) { return (Imaging)ImagingError_ModeError(); @@ -90,29 +96,32 @@ MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) if (!imOut) { return NULL; } + int xsize = imOut->xsize, ysize = imOut->ysize; /* malloc check ok, checked above */ -#define RANK_BODY(type) \ - do { \ - type *buf = malloc(size2 * sizeof(type)); \ - if (!buf) { \ - goto nomemory; \ - } \ - ImagingSectionEnter(&cookie); \ - for (y = 0; y < imOut->ysize; y++) { \ - for (x = 0; x < imOut->xsize; x++) { \ - for (i = 0; i < size; i++) { \ - memcpy( \ - buf + i * size, \ - &IMAGING_PIXEL_##type(im, x, y + i), \ - size * sizeof(type) \ - ); \ - } \ - IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ - } \ - } \ - ImagingSectionLeave(&cookie); \ - free(buf); \ + // restrict safe: buf is a private allocation, imOut is a fresh allocation. +#define RANK_BODY(type) \ + do { \ + type *restrict buf = malloc(size2 * sizeof(type)); \ + if (!buf) { \ + goto nomemory; \ + } \ + ImagingSectionEnter(&cookie); \ + for (int y = 0; y < ysize; y++) { \ + type *restrict out = (type *)imOut->image[y]; \ + for (int x = 0; x < xsize; x++) { \ + type *restrict p = buf; \ + for (int i = 0; i < size; i++) { \ + const type *row = (const type *)im->image[y + i] + x; \ + for (int k = 0; k < size; k++) { \ + *p++ = row[k]; \ + } \ + } \ + out[x] = Rank##type(buf, size2, rank); \ + } \ + } \ + ImagingSectionLeave(&cookie); \ + free(buf); \ } while (0) if (im->image8) { From ccbb07be8ac26dd7f12ba4ee9c3ca7cb72bf31ef Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 24 Sep 2026 15:20:39 +0300 Subject: [PATCH 3/3] Special-case min/max filters to not use quickselect --- src/libImaging/RankFilter.c | 43 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 7f4ddd86f03..e42ad2dc5f1 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -65,6 +65,28 @@ RankFLOAT32(FLOAT32 a[], int n, int k) { RANK_INNER_BODY(FLOAT32); } +#define RANK_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define RANK_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#define MINMAX_BODY(type, op) \ + do { \ + ImagingSectionEnter(&cookie); \ + for (int y = 0; y < ysize; y++) { \ + type *out = (type *)imOut->image[y]; \ + memcpy(out, im->image[y], xsize * sizeof(type)); \ + for (int i = 0; i < size; i++) { \ + const type *row = (const type *)im->image[y + i]; \ + for (int k = (i == 0); k < size; k++) { \ + const type *in = row + k; \ + for (int x = 0; x < xsize; x++) { \ + out[x] = op(out[x], in[x]); \ + } \ + } \ + } \ + } \ + ImagingSectionLeave(&cookie); \ + } while (0) + Imaging ImagingRankFilter(Imaging im, int size, int rank) { Imaging imOut = NULL; @@ -100,7 +122,7 @@ ImagingRankFilter(Imaging im, int size, int rank) { /* malloc check ok, checked above */ // restrict safe: buf is a private allocation, imOut is a fresh allocation. -#define RANK_BODY(type) \ +#define RANK_BODY(type, rank_fn) \ do { \ type *restrict buf = malloc(size2 * sizeof(type)); \ if (!buf) { \ @@ -117,19 +139,30 @@ ImagingRankFilter(Imaging im, int size, int rank) { *p++ = row[k]; \ } \ } \ - out[x] = Rank##type(buf, size2, rank); \ + out[x] = rank_fn(buf, size2, rank); \ } \ } \ ImagingSectionLeave(&cookie); \ free(buf); \ } while (0) +#define RANK_DISPATCH(type) \ + do { \ + if (rank == 0) { \ + MINMAX_BODY(type, RANK_MIN); \ + } else if (rank == size2 - 1) { \ + MINMAX_BODY(type, RANK_MAX); \ + } else { \ + RANK_BODY(type, Rank##type); \ + } \ + } while (0) + if (im->image8) { - RANK_BODY(UINT8); + RANK_DISPATCH(UINT8); } else if (im->type == IMAGING_TYPE_INT32) { - RANK_BODY(INT32); + RANK_DISPATCH(INT32); } else if (im->type == IMAGING_TYPE_FLOAT32) { - RANK_BODY(FLOAT32); + RANK_DISPATCH(FLOAT32); } else { /* safety net (we shouldn't end up here) */ ImagingDelete(imOut);