From c167d7a269248c93d26c123a1b200c93f1cffd4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Aug 2023 12:09:20 +1000 Subject: [PATCH] Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii --- Tests/test_box_blur.py | 2 +- Tests/test_image_filter.py | 17 ++++++++++++++--- src/PIL/ImageFilter.py | 26 +++++++++++++++++++------- src/_imaging.c | 12 ++++++------ src/libImaging/BoxBlur.c | 27 +++++++++++++++++++-------- src/libImaging/Imaging.h | 4 ++-- src/libImaging/UnsharpMask.c | 2 +- 7 files changed, 62 insertions(+), 28 deletions(-) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 3bdd5177d3f..745364ddc0f 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -22,7 +22,7 @@ def test_imageops_box_blur(): def box_blur(image, radius=1, n=1): - return image._new(image.im.box_blur(radius, n)) + return image._new(image.im.box_blur((radius, radius), n)) def assert_image(im, data, delta=0): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 25b72298e92..521551212d0 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,8 +24,10 @@ ImageFilter.ModeFilter, ImageFilter.GaussianBlur, ImageFilter.GaussianBlur(5), + ImageFilter.GaussianBlur((2, 5)), ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(5), + ImageFilter.BoxBlur((2, 5)), ImageFilter.UnsharpMask, ImageFilter.UnsharpMask(10), ), @@ -185,12 +187,21 @@ def test_consistency_5x5(mode): assert_image_equal(source.filter(kernel), reference) -def test_invalid_box_blur_filter(): +@pytest.mark.parametrize( + "radius", + ( + -2, + (-2, -2), + (-2, 2), + (2, -2), + ), +) +def test_invalid_box_blur_filter(radius): with pytest.raises(ValueError): - ImageFilter.BoxBlur(-2) + ImageFilter.BoxBlur(radius) im = hopper() box_blur_filter = ImageFilter.BoxBlur(2) - box_blur_filter.radius = -2 + box_blur_filter.radius = radius with pytest.raises(ValueError): im.filter(box_blur_filter) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 33bc7cc2e30..0d2fec9ee78 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter): approximates a Gaussian kernel. For details on accuracy see - :param radius: Standard deviation of the Gaussian kernel. + :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two + numbers for x and y, or a single number for both. """ name = "GaussianBlur" @@ -166,7 +167,10 @@ def __init__(self, radius=2): self.radius = radius def filter(self, image): - return image.gaussian_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + return image.gaussian_blur(xy) class BoxBlur(MultibandFilter): @@ -176,21 +180,29 @@ class BoxBlur(MultibandFilter): which runs in linear time relative to the size of the image for any radius value. - :param radius: Size of the box in one direction. Radius 0 does not blur, - returns an identical image. Radius 1 takes 1 pixel - in each direction, i.e. 9 pixels in total. + :param radius: Size of the box in a direction. Either a sequence of two numbers for + x and y, or a single number for both. + + Radius 0 does not blur, returns an identical image. + Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. """ name = "BoxBlur" def __init__(self, radius): - if radius < 0: + xy = radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy[0] < 0 or xy[1] < 0: msg = "radius must be >= 0" raise ValueError(msg) self.radius = radius def filter(self, image): - return image.box_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + return image.box_blur(xy) class UnsharpMask(MultibandFilter): diff --git a/src/_imaging.c b/src/_imaging.c index e15cb89fcea..95da2772d56 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1075,9 +1075,9 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius = 0; + float xradius, yradius; int passes = 3; - if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) { return NULL; } @@ -1087,7 +1087,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { + if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) { ImagingDelete(imOut); return NULL; } @@ -2131,9 +2131,9 @@ _box_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius; + float xradius, yradius; int n = 1; - if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) { return NULL; } @@ -2143,7 +2143,7 @@ _box_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingBoxBlur(imOut, imIn, radius, n)) { + if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) { ImagingDelete(imOut); return NULL; } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 5afe7cf5043..41e9fbed9cb 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -230,14 +230,14 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { } Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) { int i; Imaging imTransposed; if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } - if (radius < 0) { + if (xradius < 0 || yradius < 0) { return ImagingError_ValueError("radius must be >= 0"); } @@ -266,16 +266,16 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { /* Apply blur in one dimension. Use imOut as a destination at first pass, then use imOut as a source too. */ - ImagingHorizontalBoxBlur(imOut, imIn, radius); + ImagingHorizontalBoxBlur(imOut, imIn, xradius); for (i = 1; i < n; i++) { - ImagingHorizontalBoxBlur(imOut, imOut, radius); + ImagingHorizontalBoxBlur(imOut, imOut, xradius); } /* Transpose result for blur in another direction. */ ImagingTranspose(imTransposed, imOut); /* Reuse imTransposed as a source and destination there. */ for (i = 0; i < n; i++) { - ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); + ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); } /* Restore original orientation. */ ImagingTranspose(imOut, imTransposed); @@ -285,8 +285,8 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { return imOut; } -Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { +static float +_gaussian_blur_radius(float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; @@ -299,5 +299,16 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); a /= 6 * (sigma2 - (l + 1) * (l + 1)); - return ImagingBoxBlur(imOut, imIn, l + a, passes); + return l + a; +} + +Imaging +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + return ImagingBoxBlur( + imOut, + imIn, + _gaussian_blur_radius(xradius, passes), + _gaussian_blur_radius(yradius, passes), + passes + ); } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 01f40ee7b06..afcd2229bde 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -309,7 +309,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn); extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes); +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging @@ -376,7 +376,7 @@ ImagingTransform( extern Imaging ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n); extern Imaging ImagingColorLUT3D_linear( Imaging imOut, diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 643ced49f17..2853ce903fc 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -36,7 +36,7 @@ ImagingUnsharpMask( /* First, do a gaussian blur on the image, putting results in imOut temporarily. All format checks are in gaussian blur. */ - result = ImagingGaussianBlur(imOut, imIn, radius, 3); + result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); if (!result) { return NULL; }