diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 3bdd5177d..745364ddc 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 25b72298e..521551212 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,8 +24,10 @@ from .helper import assert_image_equal, hopper 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 33bc7cc2e..0d2fec9ee 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 @@ class GaussianBlur(MultibandFilter): 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 e15cb89fc..95da2772d 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 5afe7cf50..41e9fbed9 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 01f40ee7b..afcd2229b 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 643ced49f..2853ce903 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; }