From 36c458a6556dc0aeab5dcdd4563f77d911432cc0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:07:23 +0300 Subject: [PATCH 1/3] Add fast-path for getextrema on 8-bit multichannel images --- src/PIL/Image.py | 2 -- src/_imaging.c | 30 ++++++++++++++++++++++--- src/libImaging/GetBBox.c | 47 ++++++++++++++++++++++++++++++++++++++++ src/libImaging/Imaging.h | 2 ++ 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 57b2287b9e1..01f6170eac5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1575,8 +1575,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, *, strip_namespaces: bool = True) -> dict[str, Any]: diff --git a/src/_imaging.c b/src/_imaging.c index fa42332a815..0973fccd112 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2362,15 +2362,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 1952cd9ccb4..1824cf0ca33 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -280,6 +280,53 @@ ImagingGetExtrema(Imaging im, void *extrema) { return 1; /* ok */ } +int +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. + 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++) { + vmin[b] = MIN(vmin[b], in[b]); + vmax[b] = MAX(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 * getcolors32(Imaging im, int maxcolors, int *size); diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index fd4628c40ad..9b10bedaeb6 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -356,6 +356,8 @@ ImagingGetColors(Imaging im, int maxcolors, int *colors); extern int ImagingGetExtrema(Imaging im, void *extrema); extern int +ImagingGetExtremaMultiband(Imaging im, UINT8 extrema[8]); +extern int ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); extern ImagingHistogram ImagingGetHistogram(Imaging im, Imaging mask, void *extrema); From 1989c11490e12e0829c540b2243d74324f7565f6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:11:22 +0300 Subject: [PATCH 2/3] 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 1824cf0ca33..2ba30c5dc26 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -198,11 +198,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; @@ -216,11 +213,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)); @@ -231,6 +225,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 293b8a5c707a43d5e78c5c5e410047895a565f55 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Jul 2026 00:13:57 +0300 Subject: [PATCH 3/3] 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 2ba30c5dc26..de51ea360eb 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -179,7 +179,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; @@ -188,16 +188,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]; } @@ -210,9 +210,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]; } @@ -222,9 +222,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]) { @@ -247,8 +247,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);