From cef372052a68f2c29fd46555fc0e063c2f5b663f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:06:15 +0300 Subject: [PATCH 1/5] Add getextrema benchmark --- Tests/benchmarks.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 0c67d298dcb..7a0c07282ee 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -431,6 +431,15 @@ def test_offset(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> No bench(ImageChops.offset, im, 123, 45) +@pytest.mark.benchmark(group="extrema") +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("size", SIZES, ids=_format_size) +def test_getextrema(bench: BenchmarkFixture, mode: str, size: tuple[int, int]) -> None: + im = make_pillow_image(mode, size) + bench.extra_info["label"] = [f"extrema {mode}"] + bench(im.getextrema) + + @pytest.mark.benchmark(group="histogram") @pytest.mark.parametrize("mode", MODES) @pytest.mark.parametrize("size", SIZES, ids=_format_size) From 28cb4a59346a7e15c568776d5d08e7aa142a6ccd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:07:23 +0300 Subject: [PATCH 2/5] Add fast-path for getextrema on 8-bit multichannel images --- src/PIL/Image.py | 2 -- src/_imaging.c | 30 ++++++++++++++++++++--- src/libImaging/GetBBox.c | 51 ++++++++++++++++++++++++++++++++++++++++ src/libImaging/Imaging.h | 2 ++ 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7a43a824f5..b82a1c4e175 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1573,8 +1573,6 @@ def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: """ self.load() - if self.im.bands > 1: - return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() def getxmp(self) -> dict[str, Any]: diff --git a/src/_imaging.c b/src/_imaging.c index 3cd762ff5c1..28a8e50f170 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2335,15 +2335,39 @@ _getcolors(ImagingObject *self, PyObject *args) { static PyObject * _getextrema(ImagingObject *self, PyObject *args) { + if (self->image->type == IMAGING_TYPE_UINT8 && self->image->bands > 1 && + self->image->bands <= 4) { + // Fast per-band extrema for 8-bit multiband images (RGB, RGBA, etc.) + UINT8 mb[2 * 4]; + int bands = self->image->bands; + int nb = ImagingGetExtremaMultiband(self->image, mb); + if (nb < 0) { + return NULL; + } + PyObject *result = PyTuple_New(bands); + if (!result) { + return NULL; + } + for (int b = 0; b < bands; b++) { + // nb == 0 for an empty image: report None per band. + PyObject *item = + nb ? Py_BuildValue("BB", mb[b * 2], mb[b * 2 + 1]) : Py_NewRef(Py_None); + if (!item) { + Py_DECREF(result); + return NULL; + } + PyTuple_SET_ITEM(result, b, item); + } + return result; + } + union { UINT8 u[2]; INT32 i[2]; FLOAT32 f[2]; UINT16 s[2]; } extrema; - int status; - - status = ImagingGetExtrema(self->image, &extrema); + int status = ImagingGetExtrema(self->image, &extrema); if (status < 0) { return NULL; } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 7a57f6894a0..0429d5444e6 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -248,6 +248,57 @@ ImagingGetExtrema(Imaging im, void *extrema) { return 1; /* ok */ } +int +ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[const 8]) { + // Per-band min/max for interleaved 8-bit images (up to 4 bands). + // Writes 2 * im->bands bytes to extrema as [min0, max0, min1, max1, ...]. + // Returns the band count on success, 0 for an empty image, or -1 on error. + int bands = im->bands, xsize = im->xsize, ysize = im->ysize; + UINT8 vmin[4], vmax[4]; + + if (im->type != IMAGING_TYPE_UINT8 || im->image8 || bands < 2 || bands > 4) { + // `_getextrema` should have checked these, but best be sure, + // in case someone ends up calling this directly. + (void)ImagingError_ModeError(); + return -1; + } + + if (!xsize || !ysize) { + return 0; /* zero size */ + } + + UINT8 *restrict in = (UINT8 *)im->image[0]; + for (int b = 0; b < 4; b++) { + vmin[b] = vmax[b] = in[b]; + } + + for (int y = 0; y < ysize; y++) { + UINT8 *restrict in = (UINT8 *)im->image[y]; + for (int x = 0; x < xsize; x++, in += 4) { + for (int b = 0; b < 4; b++) { + if (in[b] < vmin[b]) { + vmin[b] = in[b]; + } + if (in[b] > vmax[b]) { + vmax[b] = in[b]; + } + } + } + } + + // The second band of a two-band image is stored in the fourth byte + // (mirrors the special case in ImagingGetBand). + if (bands == 2) { + vmin[1] = vmin[3]; + vmax[1] = vmax[3]; + } + for (int b = 0; b < bands; b++) { + extrema[b * 2 + 0] = vmin[b]; + extrema[b * 2 + 1] = vmax[b]; + } + return bands; +} + /* static ImagingColorItem* getcolors8(Imaging im, int maxcolors, int* size);*/ static ImagingColorItem * getcolors32(Imaging im, int maxcolors, int *size); diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 472bda5d0fd..f61af0cc06f 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -354,6 +354,8 @@ ImagingGetColors(Imaging im, int maxcolors, int *colors); extern int ImagingGetExtrema(Imaging im, void *extrema); extern int +ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[const 8]); +extern int ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); extern ImagingHistogram ImagingGetHistogram(Imaging im, Imaging mask, void *extrema); From 6e0dcdd87da3e6525bfcc5173e65a51c30070bfa Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:11:22 +0300 Subject: [PATCH 3/5] Use vectorisable operations in single-band getextrema --- src/libImaging/GetBBox.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 0429d5444e6..52dd5b16446 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -166,11 +166,8 @@ ImagingGetExtrema(Imaging im, void *extrema) { for (y = 0; y < im->ysize; y++) { UINT8 *in = im->image8[y]; for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } + imin = imin < in[x] ? imin : in[x]; + imax = imax > in[x] ? imax : in[x]; } if (imin == 0 && imax == 255) { break; @@ -184,11 +181,8 @@ ImagingGetExtrema(Imaging im, void *extrema) { for (y = 0; y < im->ysize; y++) { INT32 *in = im->image32[y]; for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } + imin = imin < in[x] ? imin : in[x]; + imax = imax > in[x] ? imax : in[x]; } } memcpy(extrema, &imin, sizeof(imin)); @@ -199,6 +193,8 @@ ImagingGetExtrema(Imaging im, void *extrema) { for (y = 0; y < im->ysize; y++) { FLOAT32 *in = (FLOAT32 *)im->image32[y]; for (x = 0; x < im->xsize; x++) { + // Kept as if/else (unlike the integer branches above), + // since float min/max isn't vectorisable due to NaN semantics. if (fmin > in[x]) { fmin = in[x]; } else if (fmax < in[x]) { From 3d10affb6df5e036a80a3d053ab68bb9a673ccca Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:13:57 +0300 Subject: [PATCH 4/5] Apply hoist optimizations to single-band getextrema --- src/libImaging/GetBBox.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 52dd5b16446..2c3711d6aac 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -147,7 +147,7 @@ ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj) { int ImagingGetExtrema(Imaging im, void *extrema) { - int x, y; + int xsize = im->xsize, ysize = im->ysize; INT32 imin, imax; FLOAT32 fmin, fmax; @@ -156,16 +156,16 @@ ImagingGetExtrema(Imaging im, void *extrema) { return -1; /* mismatch */ } - if (!im->xsize || !im->ysize) { + if (!xsize || !ysize) { return 0; /* zero size */ } switch (im->type) { case IMAGING_TYPE_UINT8: imin = imax = im->image8[0][0]; - for (y = 0; y < im->ysize; y++) { + for (int y = 0; y < ysize; y++) { UINT8 *in = im->image8[y]; - for (x = 0; x < im->xsize; x++) { + for (int x = 0; x < xsize; x++) { imin = imin < in[x] ? imin : in[x]; imax = imax > in[x] ? imax : in[x]; } @@ -178,9 +178,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { break; case IMAGING_TYPE_INT32: imin = imax = im->image32[0][0]; - for (y = 0; y < im->ysize; y++) { + for (int y = 0; y < ysize; y++) { INT32 *in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { + for (int x = 0; x < xsize; x++) { imin = imin < in[x] ? imin : in[x]; imax = imax > in[x] ? imax : in[x]; } @@ -190,9 +190,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { break; case IMAGING_TYPE_FLOAT32: fmin = fmax = ((FLOAT32 *)im->image32[0])[0]; - for (y = 0; y < im->ysize; y++) { + for (int y = 0; y < ysize; y++) { FLOAT32 *in = (FLOAT32 *)im->image32[y]; - for (x = 0; x < im->xsize; x++) { + for (int x = 0; x < xsize; x++) { // Kept as if/else (unlike the integer branches above), // since float min/max isn't vectorisable due to NaN semantics. if (fmin > in[x]) { @@ -215,8 +215,8 @@ ImagingGetExtrema(Imaging im, void *extrema) { memcpy(&v, pixel, sizeof(v)); #endif imin = imax = v; - for (y = 0; y < im->ysize; y++) { - for (x = 0; x < im->xsize; x++) { + for (int y = 0; y < ysize; y++) { + for (int x = 0; x < xsize; x++) { pixel = (UINT8 *)im->image[y] + x * sizeof(v); #ifdef WORDS_BIGENDIAN v = pixel[0] + ((UINT16)pixel[1] << 8); From 12b239a647f41d21f39ade0c4e6d6a592b3bc1e4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 13:31:56 +0300 Subject: [PATCH 5/5] Try to make MSVC happy Co-authored-by: Aarni Koskela --- src/libImaging/GetBBox.c | 2 +- src/libImaging/Imaging.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 2c3711d6aac..938e3a6b0ee 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -245,7 +245,7 @@ ImagingGetExtrema(Imaging im, void *extrema) { } int -ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[const 8]) { +ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[8]) { // Per-band min/max for interleaved 8-bit images (up to 4 bands). // Writes 2 * im->bands bytes to extrema as [min0, max0, min1, max1, ...]. // Returns the band count on success, 0 for an empty image, or -1 on error. diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index f61af0cc06f..a2250147d60 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -354,7 +354,7 @@ ImagingGetColors(Imaging im, int maxcolors, int *colors); extern int ImagingGetExtrema(Imaging im, void *extrema); extern int -ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[const 8]); +ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[8]); extern int ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); extern ImagingHistogram