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) 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..938e3a6b0ee 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,21 +156,18 @@ 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++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } + for (int x = 0; x < xsize; x++) { + imin = imin < in[x] ? imin : in[x]; + imax = imax > in[x] ? imax : in[x]; } if (imin == 0 && imax == 255) { break; @@ -181,14 +178,11 @@ 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++) { - if (imin > in[x]) { - imin = in[x]; - } else if (imax < in[x]) { - imax = in[x]; - } + for (int x = 0; x < xsize; x++) { + imin = imin < in[x] ? imin : in[x]; + imax = imax > in[x] ? imax : in[x]; } } memcpy(extrema, &imin, sizeof(imin)); @@ -196,9 +190,11 @@ 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]) { fmin = in[x]; } else if (fmax < in[x]) { @@ -219,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); @@ -248,6 +244,57 @@ 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++) { + 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..a2250147d60 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[8]); +extern int ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); extern ImagingHistogram ImagingGetHistogram(Imaging im, Imaging mask, void *extrema);