Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii

This commit is contained in:
Andrew Murray 2023-08-12 12:09:20 +10:00
parent 08b538780d
commit c167d7a269
7 changed files with 62 additions and 28 deletions

View File

@ -22,7 +22,7 @@ def test_imageops_box_blur():
def box_blur(image, radius=1, n=1): 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): def assert_image(im, data, delta=0):

View File

@ -24,8 +24,10 @@ from .helper import assert_image_equal, hopper
ImageFilter.ModeFilter, ImageFilter.ModeFilter,
ImageFilter.GaussianBlur, ImageFilter.GaussianBlur,
ImageFilter.GaussianBlur(5), ImageFilter.GaussianBlur(5),
ImageFilter.GaussianBlur((2, 5)),
ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(0),
ImageFilter.BoxBlur(5), ImageFilter.BoxBlur(5),
ImageFilter.BoxBlur((2, 5)),
ImageFilter.UnsharpMask, ImageFilter.UnsharpMask,
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
), ),
@ -185,12 +187,21 @@ def test_consistency_5x5(mode):
assert_image_equal(source.filter(kernel), reference) 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): with pytest.raises(ValueError):
ImageFilter.BoxBlur(-2) ImageFilter.BoxBlur(radius)
im = hopper() im = hopper()
box_blur_filter = ImageFilter.BoxBlur(2) box_blur_filter = ImageFilter.BoxBlur(2)
box_blur_filter.radius = -2 box_blur_filter.radius = radius
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.filter(box_blur_filter) im.filter(box_blur_filter)

View File

@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter):
approximates a Gaussian kernel. For details on accuracy see approximates a Gaussian kernel. For details on accuracy see
<https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf> <https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf>
: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" name = "GaussianBlur"
@ -166,7 +167,10 @@ class GaussianBlur(MultibandFilter):
self.radius = radius self.radius = radius
def filter(self, image): 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): class BoxBlur(MultibandFilter):
@ -176,21 +180,29 @@ class BoxBlur(MultibandFilter):
which runs in linear time relative to the size of the image which runs in linear time relative to the size of the image
for any radius value. for any radius value.
:param radius: Size of the box in one direction. Radius 0 does not blur, :param radius: Size of the box in a direction. Either a sequence of two numbers for
returns an identical image. Radius 1 takes 1 pixel x and y, or a single number for both.
in each direction, i.e. 9 pixels in total.
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" name = "BoxBlur"
def __init__(self, radius): 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" msg = "radius must be >= 0"
raise ValueError(msg) raise ValueError(msg)
self.radius = radius self.radius = radius
def filter(self, image): 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): class UnsharpMask(MultibandFilter):

View File

@ -1075,9 +1075,9 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
Imaging imIn; Imaging imIn;
Imaging imOut; Imaging imOut;
float radius = 0; float xradius, yradius;
int passes = 3; int passes = 3;
if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) {
return NULL; return NULL;
} }
@ -1087,7 +1087,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
return NULL; return NULL;
} }
if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) {
ImagingDelete(imOut); ImagingDelete(imOut);
return NULL; return NULL;
} }
@ -2131,9 +2131,9 @@ _box_blur(ImagingObject *self, PyObject *args) {
Imaging imIn; Imaging imIn;
Imaging imOut; Imaging imOut;
float radius; float xradius, yradius;
int n = 1; int n = 1;
if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) {
return NULL; return NULL;
} }
@ -2143,7 +2143,7 @@ _box_blur(ImagingObject *self, PyObject *args) {
return NULL; return NULL;
} }
if (!ImagingBoxBlur(imOut, imIn, radius, n)) { if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) {
ImagingDelete(imOut); ImagingDelete(imOut);
return NULL; return NULL;
} }

View File

@ -230,14 +230,14 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) {
} }
Imaging Imaging
ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) {
int i; int i;
Imaging imTransposed; Imaging imTransposed;
if (n < 1) { if (n < 1) {
return ImagingError_ValueError("number of passes must be greater than zero"); 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"); 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. /* Apply blur in one dimension.
Use imOut as a destination at first pass, Use imOut as a destination at first pass,
then use imOut as a source too. */ then use imOut as a source too. */
ImagingHorizontalBoxBlur(imOut, imIn, radius); ImagingHorizontalBoxBlur(imOut, imIn, xradius);
for (i = 1; i < n; i++) { for (i = 1; i < n; i++) {
ImagingHorizontalBoxBlur(imOut, imOut, radius); ImagingHorizontalBoxBlur(imOut, imOut, xradius);
} }
/* Transpose result for blur in another direction. */ /* Transpose result for blur in another direction. */
ImagingTranspose(imTransposed, imOut); ImagingTranspose(imTransposed, imOut);
/* Reuse imTransposed as a source and destination there. */ /* Reuse imTransposed as a source and destination there. */
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius);
} }
/* Restore original orientation. */ /* Restore original orientation. */
ImagingTranspose(imOut, imTransposed); ImagingTranspose(imOut, imTransposed);
@ -285,8 +285,8 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) {
return imOut; return imOut;
} }
Imaging static float
ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { _gaussian_blur_radius(float radius, int passes) {
float sigma2, L, l, a; float sigma2, L, l, a;
sigma2 = radius * radius / passes; 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 = (2 * l + 1) * (l * (l + 1) - 3 * sigma2);
a /= 6 * (sigma2 - (l + 1) * (l + 1)); 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
);
} }

View File

@ -309,7 +309,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn);
extern Imaging extern Imaging
ImagingFlipTopBottom(Imaging imOut, Imaging imIn); ImagingFlipTopBottom(Imaging imOut, Imaging imIn);
extern Imaging extern Imaging
ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes); ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes);
extern Imaging extern Imaging
ImagingGetBand(Imaging im, int band); ImagingGetBand(Imaging im, int band);
extern Imaging extern Imaging
@ -376,7 +376,7 @@ ImagingTransform(
extern Imaging extern Imaging
ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold);
extern Imaging extern Imaging
ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n);
extern Imaging extern Imaging
ImagingColorLUT3D_linear( ImagingColorLUT3D_linear(
Imaging imOut, Imaging imOut,

View File

@ -36,7 +36,7 @@ ImagingUnsharpMask(
/* First, do a gaussian blur on the image, putting results in imOut /* First, do a gaussian blur on the image, putting results in imOut
temporarily. All format checks are in gaussian blur. */ temporarily. All format checks are in gaussian blur. */
result = ImagingGaussianBlur(imOut, imIn, radius, 3); result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3);
if (!result) { if (!result) {
return NULL; return NULL;
} }