Merge pull request #7336 from radarhere/blur

This commit is contained in:
Hugo van Kemenade 2023-08-25 03:20:16 -06:00 committed by GitHub
commit c68bf7df8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 42 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

@ -23,9 +23,12 @@ from .helper import assert_image_equal, hopper
ImageFilter.MinFilter, ImageFilter.MinFilter,
ImageFilter.ModeFilter, ImageFilter.ModeFilter,
ImageFilter.GaussianBlur, ImageFilter.GaussianBlur,
ImageFilter.GaussianBlur(0),
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 +188,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,12 @@ 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)
if xy == (0, 0):
return image.copy()
return image.gaussian_blur(xy)
class BoxBlur(MultibandFilter): class BoxBlur(MultibandFilter):
@ -176,21 +182,31 @@ 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)
if xy == (0, 0):
return image.copy()
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");
} }
@ -258,35 +258,45 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) {
return ImagingError_ModeError(); return ImagingError_ModeError();
} }
/* Apply blur in one dimension.
Use imOut as a destination at first pass,
then use imOut as a source too. */
if (xradius != 0) {
ImagingHorizontalBoxBlur(imOut, imIn, xradius);
for (i = 1; i < n; i++) {
ImagingHorizontalBoxBlur(imOut, imOut, xradius);
}
}
if (yradius != 0) {
imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize);
if (!imTransposed) { if (!imTransposed) {
return NULL; return NULL;
} }
/* Apply blur in one dimension.
Use imOut as a destination at first pass,
then use imOut as a source too. */
ImagingHorizontalBoxBlur(imOut, imIn, radius);
for (i = 1; i < n; i++) {
ImagingHorizontalBoxBlur(imOut, imOut, radius);
}
/* Transpose result for blur in another direction. */ /* Transpose result for blur in another direction. */
ImagingTranspose(imTransposed, imOut); ImagingTranspose(imTransposed, xradius == 0 ? imIn : 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);
ImagingDelete(imTransposed); ImagingDelete(imTransposed);
}
if (xradius == 0 && yradius == 0) {
if (!ImagingCopy2(imOut, imIn)) {
return NULL;
}
}
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 +309,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;
} }