diff --git a/PIL/ImageFilter.py b/PIL/ImageFilter.py index c89225484..100fea8bd 100644 --- a/PIL/ImageFilter.py +++ b/PIL/ImageFilter.py @@ -160,6 +160,26 @@ class GaussianBlur(MultibandFilter): return image.gaussian_blur(self.radius) +class BoxBlur(MultibandFilter): + """Blurs the image by setting each pixel to the average value of the pixels + in a square box extending radius pixels in each direction. + Supports float radius of arbitrary size. Uses an optimized implementation + 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. + """ + name = "BoxBlur" + + def __init__(self, radius): + self.radius = radius + + def filter(self, image): + return image.box_blur(self.radius) + + class UnsharpMask(MultibandFilter): """Unsharp mask filter. diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 3681109c1..447d48aae 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -21,6 +21,7 @@ from . import Image from ._util import isStringType import operator import functools +import warnings # @@ -437,6 +438,13 @@ def solarize(image, threshold=128): def gaussian_blur(im, radius=None): """ PIL_usm.gblur(im, [radius])""" + warnings.warn( + 'PIL.ImageOps.gaussian_blur is deprecated. ' + 'Use PIL.ImageFilter.GaussianBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + if radius is None: radius = 5.0 @@ -444,12 +452,30 @@ def gaussian_blur(im, radius=None): return im.im.gaussian_blur(radius) -gblur = gaussian_blur + +def gblur(im, radius=None): + """ PIL_usm.gblur(im, [radius])""" + + warnings.warn( + 'PIL.ImageOps.gblur is deprecated. ' + 'Use PIL.ImageFilter.GaussianBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + + return gaussian_blur(im, radius) def unsharp_mask(im, radius=None, percent=None, threshold=None): """ PIL_usm.usm(im, [radius, percent, threshold])""" + warnings.warn( + 'PIL.ImageOps.unsharp_mask is deprecated. ' + 'Use PIL.ImageFilter.UnsharpMask instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + if radius is None: radius = 5.0 if percent is None: @@ -461,7 +487,18 @@ def unsharp_mask(im, radius=None, percent=None, threshold=None): return im.im.unsharp_mask(radius, percent, threshold) -usm = unsharp_mask + +def usm(im, radius=None, percent=None, threshold=None): + """ PIL_usm.usm(im, [radius, percent, threshold])""" + + warnings.warn( + 'PIL.ImageOps.usm is deprecated. ' + 'Use PIL.ImageFilter.UnsharpMask instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + + return unsharp_mask(im, radius, percent, threshold) def box_blur(image, radius): @@ -478,6 +515,13 @@ def box_blur(image, radius): in each direction, i.e. 9 pixels in total. :return: An image. """ + warnings.warn( + 'PIL.ImageOps.box_blur is deprecated. ' + 'Use PIL.ImageFilter.BoxBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + image.load() return image._new(image.im.box_blur(radius)) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index d99847740..622b842d0 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -13,20 +13,6 @@ sample.putdata(sum([ ], [])) -class ImageMock(object): - def __init__(self): - self.im = self - - def load(self): - pass - - def _new(self, im): - return im - - def box_blur(self, radius, n): - return radius, n - - class TestBoxBlurApi(PillowTestCase): def test_imageops_box_blur(self): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 8a38b2979..3636a73f7 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,7 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image -from PIL import ImageFilter +from PIL import Image, ImageFilter class TestImageFilter(PillowTestCase): @@ -9,10 +8,11 @@ class TestImageFilter(PillowTestCase): def test_sanity(self): def filter(filter): - im = hopper("L") - out = im.filter(filter) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) + for mode in ["L", "RGB", "CMYK"]: + im = hopper(mode) + out = im.filter(filter) + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, im.size) filter(ImageFilter.BLUR) filter(ImageFilter.CONTOUR) @@ -28,9 +28,9 @@ class TestImageFilter(PillowTestCase): filter(ImageFilter.MedianFilter) filter(ImageFilter.MinFilter) filter(ImageFilter.ModeFilter) - filter(ImageFilter.Kernel((3, 3), list(range(9)))) filter(ImageFilter.GaussianBlur) filter(ImageFilter.GaussianBlur(5)) + filter(ImageFilter.BoxBlur(5)) filter(ImageFilter.UnsharpMask) filter(ImageFilter.UnsharpMask(10)) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index eed8ff754..cd7dcae5f 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -12,15 +12,25 @@ class TestImageOpsUsm(PillowTestCase): def test_ops_api(self): - i = ImageOps.gaussian_blur(im, 2.0) + i = self.assert_warning(DeprecationWarning, + ImageOps.gaussian_blur, im, 2.0) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - # i.save("blur.bmp") - i = ImageOps.unsharp_mask(im, 2.0, 125, 8) + i = self.assert_warning(DeprecationWarning, + ImageOps.gblur, im, 2.0) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, + ImageOps.unsharp_mask, im, 2.0, 125, 8) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, + ImageOps.usm, im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - # i.save("usm.bmp") def test_filter_api(self): @@ -36,38 +46,38 @@ class TestImageOpsUsm(PillowTestCase): def test_usm_formats(self): - usm = ImageOps.unsharp_mask - self.assertRaises(ValueError, usm, im.convert("1")) - usm(im.convert("L")) - self.assertRaises(ValueError, usm, im.convert("I")) - self.assertRaises(ValueError, usm, im.convert("F")) - usm(im.convert("RGB")) - usm(im.convert("RGBA")) - usm(im.convert("CMYK")) - self.assertRaises(ValueError, usm, im.convert("YCbCr")) + usm = ImageFilter.UnsharpMask + self.assertRaises(ValueError, im.convert("1").filter, usm) + im.convert("L").filter(usm) + self.assertRaises(ValueError, im.convert("I").filter, usm) + self.assertRaises(ValueError, im.convert("F").filter, usm) + im.convert("RGB").filter(usm) + im.convert("RGBA").filter(usm) + im.convert("CMYK").filter(usm) + self.assertRaises(ValueError, im.convert("YCbCr").filter, usm) def test_blur_formats(self): - blur = ImageOps.gaussian_blur - self.assertRaises(ValueError, blur, im.convert("1")) + blur = ImageFilter.GaussianBlur + self.assertRaises(ValueError, im.convert("1").filter, blur) blur(im.convert("L")) - self.assertRaises(ValueError, blur, im.convert("I")) - self.assertRaises(ValueError, blur, im.convert("F")) - blur(im.convert("RGB")) - blur(im.convert("RGBA")) - blur(im.convert("CMYK")) - self.assertRaises(ValueError, blur, im.convert("YCbCr")) + self.assertRaises(ValueError, im.convert("I").filter, blur) + self.assertRaises(ValueError, im.convert("F").filter, blur) + im.convert("RGB").filter(blur) + im.convert("RGBA").filter(blur) + im.convert("CMYK").filter(blur) + self.assertRaises(ValueError, im.convert("YCbCr").filter, blur) def test_usm_accuracy(self): src = snakes.convert('RGB') - i = src._new(ImageOps.unsharp_mask(src, 5, 1024, 0)) + i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) # Image should not be changed because it have only 0 and 255 levels. self.assertEqual(i.tobytes(), src.tobytes()) def test_blur_accuracy(self): - i = snakes._new(ImageOps.gaussian_blur(snakes, .4)) + i = snakes.filter(ImageFilter.GaussianBlur(.4)) # These pixels surrounded with pixels with 255 intensity. # They must be very close to 255. for x, y, c in [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index e89fafbcf..bc1868667 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -38,6 +38,7 @@ image enhancement filters: * **SHARPEN** .. autoclass:: PIL.ImageFilter.GaussianBlur +.. autoclass:: PIL.ImageFilter.BoxBlur .. autoclass:: PIL.ImageFilter.UnsharpMask .. autoclass:: PIL.ImageFilter.Kernel .. autoclass:: PIL.ImageFilter.RankFilter diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index 4708eeb29..6e3a40176 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -4,13 +4,19 @@ Get One Channel From Image ========================== -New method :py:meth:`PIL.Image.Image.getchannel` added. +New method :py:meth:`PIL.Image.Image.getchannel` is added. It returns single channel by index or name. For example, ``image.getchannel("A")`` will return alpha channel as separate image. ``getchannel`` should work up to 6 times faster than ``image.split()[0]`` in previous Pillow versions. +Box Blur +======== + +New filter :py:class:`PIL.ImageFilter.BoxBlur` is added. + + Partial Resampling ==================