Merge pull request #1959 from uploadcare/new-filters

Add Box and Hamming filters for resampling
This commit is contained in:
wiredfool 2016-07-05 12:25:57 +01:00 committed by GitHub
commit e8c123c833
6 changed files with 141 additions and 43 deletions

View File

@ -168,13 +168,14 @@ MESH = 4
# resampling filters # resampling filters
NEAREST = NONE = 0 NEAREST = NONE = 0
LANCZOS = ANTIALIAS = 1 BOX = 4
BILINEAR = LINEAR = 2 BILINEAR = LINEAR = 2
HAMMING = 5
BICUBIC = CUBIC = 3 BICUBIC = CUBIC = 3
LANCZOS = ANTIALIAS = 1
# dithers # dithers
NONE = 0 NEAREST = NONE = 0
NEAREST = 0
ORDERED = 1 # Not yet implemented ORDERED = 1 # Not yet implemented
RASTERIZE = 2 # Not yet implemented RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default FLOYDSTEINBERG = 3 # default
@ -1518,16 +1519,18 @@ class Image(object):
:param size: The requested size in pixels, as a 2-tuple: :param size: The requested size in pixels, as a 2-tuple:
(width, height). (width, height).
:param resample: An optional resampling filter. This can be :param resample: An optional resampling filter. This can be
one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`,
:py:attr:`PIL.Image.BILINEAR` (linear interpolation), :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`,
:py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation), or :py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`.
:py:attr:`PIL.Image.LANCZOS` (a high-quality downsampling filter).
If omitted, or if the image has mode "1" or "P", it is If omitted, or if the image has mode "1" or "P", it is
set :py:attr:`PIL.Image.NEAREST`. set :py:attr:`PIL.Image.NEAREST`.
See: :ref:`concept-filters`.
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS): if resample not in (
NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING,
):
raise ValueError("unknown resampling filter") raise ValueError("unknown resampling filter")
self.load() self.load()
@ -1560,7 +1563,7 @@ class Image(object):
environment), or :py:attr:`PIL.Image.BICUBIC` environment), or :py:attr:`PIL.Image.BICUBIC`
(cubic spline interpolation in a 4x4 environment). (cubic spline interpolation in a 4x4 environment).
If omitted, or if the image has mode "1" or "P", it is If omitted, or if the image has mode "1" or "P", it is
set :py:attr:`PIL.Image.NEAREST`. set :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`.
:param expand: Optional expansion flag. If true, expands the output :param expand: Optional expansion flag. If true, expands the output
image to make it large enough to hold the entire rotated image. image to make it large enough to hold the entire rotated image.
If false or omitted, make the output image the same size as the If false or omitted, make the output image the same size as the

View File

@ -10,7 +10,7 @@ class TestImagingResampleVulnerability(PillowTestCase):
ysize = 1000 # unimportant ysize = 1000 # unimportant
try: try:
# any resampling filter will do here # any resampling filter will do here
im.im.resize((xsize, ysize), Image.LINEAR) im.im.resize((xsize, ysize), Image.BILINEAR)
self.fail("Resize should raise MemoryError on invalid xsize") self.fail("Resize should raise MemoryError on invalid xsize")
except MemoryError: except MemoryError:
self.assertTrue(True, "Should raise MemoryError") self.assertTrue(True, "Should raise MemoryError")
@ -89,6 +89,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for y in range(image.size[1]) for y in range(image.size[1])
) )
def test_reduce_box(self):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (8, 8), 0xe1)
case = case.resize((4, 4), Image.BOX)
data = ('e1 e1'
'e1 e1')
for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_bilinear(self): def test_reduce_bilinear(self):
for mode in ['RGBX', 'RGB', 'La', 'L']: for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (8, 8), 0xe1) case = self.make_case(mode, (8, 8), 0xe1)
@ -98,6 +107,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_hamming(self):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (8, 8), 0xe1)
case = case.resize((4, 4), Image.HAMMING)
data = ('e1 da'
'da d3')
for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_bicubic(self): def test_reduce_bicubic(self):
for mode in ['RGBX', 'RGB', 'La', 'L']: for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (12, 12), 0xe1) case = self.make_case(mode, (12, 12), 0xe1)
@ -119,6 +137,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (8, 8))) self.check_case(channel, self.make_sample(data, (8, 8)))
def test_enlarge_box(self):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (2, 2), 0xe1)
case = case.resize((4, 4), Image.BOX)
data = ('e1 e1'
'e1 e1')
for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4)))
def test_enlarge_bilinear(self): def test_enlarge_bilinear(self):
for mode in ['RGBX', 'RGB', 'La', 'L']: for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (2, 2), 0xe1) case = self.make_case(mode, (2, 2), 0xe1)
@ -128,6 +155,17 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_enlarge_hamming(self):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (4, 4), 0xe1)
case = case.resize((8, 8), Image.HAMMING)
data = ('e1 e1 ea d1'
'e1 e1 ea d1'
'ea ea f4 d9'
'd1 d1 d9 c4')
for channel in case.split():
self.check_case(channel, self.make_sample(data, (8, 8)))
def test_enlarge_bicubic(self): def test_enlarge_bicubic(self):
for mode in ['RGBX', 'RGB', 'La', 'L']: for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (4, 4), 0xe1) case = self.make_case(mode, (4, 4), 0xe1)
@ -211,14 +249,18 @@ class CoreResampleAlphaCorrectTest(PillowTestCase):
@unittest.skip("current implementation isn't precise enough") @unittest.skip("current implementation isn't precise enough")
def test_levels_rgba(self): def test_levels_rgba(self):
case = self.make_levels_case('RGBA') case = self.make_levels_case('RGBA')
self.run_levels_case(case.resize((512, 32), Image.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
@unittest.skip("current implementation isn't precise enough") @unittest.skip("current implementation isn't precise enough")
def test_levels_la(self): def test_levels_la(self):
case = self.make_levels_case('LA') case = self.make_levels_case('LA')
self.run_levels_case(case.resize((512, 32), Image.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
@ -243,13 +285,17 @@ class CoreResampleAlphaCorrectTest(PillowTestCase):
def test_dirty_pixels_rgba(self): def test_dirty_pixels_rgba(self):
case = self.make_dity_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) case = self.make_dity_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0))
self.run_dity_case(case.resize((20, 20), Image.BOX), (255, 255, 0))
self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0))
self.run_dity_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0))
self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0))
self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0))
def test_dirty_pixels_la(self): def test_dirty_pixels_la(self):
case = self.make_dity_case('LA', (255, 128), (0, 0)) case = self.make_dity_case('LA', (255, 128), (0, 0))
self.run_dity_case(case.resize((20, 20), Image.BOX), (255,))
self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255,)) self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255,))
self.run_dity_case(case.resize((20, 20), Image.HAMMING), (255,))
self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255,)) self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255,))
self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255,)) self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255,))

View File

@ -39,13 +39,15 @@ class TestImagingCoreResize(PillowTestCase):
self.assertEqual(r.im.bands, im.im.bands) self.assertEqual(r.im.bands, im.im.bands)
def test_reduce_filters(self): def test_reduce_filters(self):
for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: for f in [Image.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING,
Image.BICUBIC, Image.LANCZOS]:
r = self.resize(hopper("RGB"), (15, 12), f) r = self.resize(hopper("RGB"), (15, 12), f)
self.assertEqual(r.mode, "RGB") self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (15, 12)) self.assertEqual(r.size, (15, 12))
def test_enlarge_filters(self): def test_enlarge_filters(self):
for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: for f in [Image.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING,
Image.BICUBIC, Image.LANCZOS]:
r = self.resize(hopper("RGB"), (212, 195), f) r = self.resize(hopper("RGB"), (212, 195), f)
self.assertEqual(r.mode, "RGB") self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (212, 195)) self.assertEqual(r.size, (212, 195))
@ -64,7 +66,8 @@ class TestImagingCoreResize(PillowTestCase):
} }
samples['dirty'].putpixel((1, 1), 128) samples['dirty'].putpixel((1, 1), 128)
for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: for f in [Image.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING,
Image.BICUBIC, Image.LANCZOS]:
# samples resized with current filter # samples resized with current filter
references = dict( references = dict(
(name, self.resize(ch, (4, 4), f)) (name, self.resize(ch, (4, 4), f))

View File

@ -96,7 +96,16 @@ For geometry operations that may map multiple input pixels to a single output
pixel, the Python Imaging Library provides four different resampling *filters*. pixel, the Python Imaging Library provides four different resampling *filters*.
``NEAREST`` ``NEAREST``
Pick the nearest pixel from the input image. Ignore all other input pixels. Pick one nearest pixel from the input image. Ignore all other input pixels.
``BOX``
Each pixel of source image contributes to one pixel of the
destination image with identical weights.
For upscaling is equivalent of ``NEAREST``.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0
``BILINEAR`` ``BILINEAR``
For resize calculate the output pixel value using linear interpolation For resize calculate the output pixel value using linear interpolation
@ -104,6 +113,14 @@ pixel, the Python Imaging Library provides four different resampling *filters*.
For other transformations linear interpolation over a 2x2 environment For other transformations linear interpolation over a 2x2 environment
in the input image is used. in the input image is used.
``HAMMING``
Produces more sharp image than ``BILINEAR``, doesn't have dislocations
on local level like with ``BOX``.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0
``BICUBIC`` ``BICUBIC``
For resize calculate the output pixel value using cubic interpolation For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value. on all pixels that may contribute to the output value.
@ -128,8 +145,12 @@ Filters comparison table
+============+=============+===========+=============+ +============+=============+===========+=============+
|``NEAREST`` | | | ⭐⭐⭐⭐⭐ | |``NEAREST`` | | | ⭐⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+ +------------+-------------+-----------+-------------+
|``BOX`` | ⭐ | | ⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BILINEAR``| ⭐ | ⭐ | ⭐⭐⭐ | |``BILINEAR``| ⭐ | ⭐ | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+ +------------+-------------+-----------+-------------+
|``HAMMING`` | ⭐⭐ | | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BICUBIC`` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | |``BICUBIC`` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
+------------+-------------+-----------+-------------+ +------------+-------------+-----------+-------------+
|``LANCZOS`` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | |``LANCZOS`` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |

View File

@ -229,9 +229,11 @@ extern void ImagingError_Clear(void);
/* standard filters */ /* standard filters */
#define IMAGING_TRANSFORM_NEAREST 0 #define IMAGING_TRANSFORM_NEAREST 0
#define IMAGING_TRANSFORM_LANCZOS 1 #define IMAGING_TRANSFORM_BOX 4
#define IMAGING_TRANSFORM_BILINEAR 2 #define IMAGING_TRANSFORM_BILINEAR 2
#define IMAGING_TRANSFORM_HAMMING 5
#define IMAGING_TRANSFORM_BICUBIC 3 #define IMAGING_TRANSFORM_BICUBIC 3
#define IMAGING_TRANSFORM_LANCZOS 1
typedef int (*ImagingTransformMap)(double* X, double* Y, typedef int (*ImagingTransformMap)(double* X, double* Y,
int x, int y, void* data); int x, int y, void* data);

View File

@ -5,12 +5,51 @@
#define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) #define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F))
struct filter { struct filter {
double (*filter)(double x); double (*filter)(double x);
double support; double support;
}; };
static inline double box_filter(double x)
{
if (x >= -0.5 && x < 0.5)
return 1.0;
return 0.0;
}
static inline double bilinear_filter(double x)
{
if (x < 0.0)
x = -x;
if (x < 1.0)
return 1.0-x;
return 0.0;
}
static inline double hamming_filter(double x)
{
if (x < 0.0)
x = -x;
if (x == 0.0)
return 1.0;
x = x * M_PI;
return sin(x) / x * (0.54f + 0.46f * cos(x));
}
static inline double bicubic_filter(double x)
{
/* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */
#define a -0.5
if (x < 0.0)
x = -x;
if (x < 1.0)
return ((a + 2.0) * x - (a + 3.0)) * x*x + 1;
if (x < 2.0)
return (((x - 5) * x + 8) * x - 4) * a;
return 0.0;
#undef a
}
static inline double sinc_filter(double x) static inline double sinc_filter(double x)
{ {
if (x == 0.0) if (x == 0.0)
@ -27,33 +66,11 @@ static inline double lanczos_filter(double x)
return 0.0; return 0.0;
} }
static inline double bilinear_filter(double x) static struct filter BOX = { box_filter, 0.5 };
{
if (x < 0.0)
x = -x;
if (x < 1.0)
return 1.0-x;
return 0.0;
}
static inline double bicubic_filter(double x)
{
/* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */
#define a -0.5
if (x < 0.0)
x = -x;
if (x < 1.0)
return ((a + 2.0) * x - (a + 3.0)) * x*x + 1;
if (x < 2.0)
return (((x - 5) * x + 8) * x - 4) * a;
return 0.0;
#undef a
}
static struct filter LANCZOS = { lanczos_filter, 3.0 };
static struct filter BILINEAR = { bilinear_filter, 1.0 }; static struct filter BILINEAR = { bilinear_filter, 1.0 };
static struct filter HAMMING = { hamming_filter, 1.0 };
static struct filter BICUBIC = { bicubic_filter, 2.0 }; static struct filter BICUBIC = { bicubic_filter, 2.0 };
static struct filter LANCZOS = { lanczos_filter, 3.0 };
/* 8 bits for result. Filter can have negative areas. /* 8 bits for result. Filter can have negative areas.
@ -524,15 +541,21 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter)
/* check filter */ /* check filter */
switch (filter) { switch (filter) {
case IMAGING_TRANSFORM_LANCZOS: case IMAGING_TRANSFORM_BOX:
filterp = &LANCZOS; filterp = &BOX;
break; break;
case IMAGING_TRANSFORM_BILINEAR: case IMAGING_TRANSFORM_BILINEAR:
filterp = &BILINEAR; filterp = &BILINEAR;
break; break;
case IMAGING_TRANSFORM_HAMMING:
filterp = &HAMMING;
break;
case IMAGING_TRANSFORM_BICUBIC: case IMAGING_TRANSFORM_BICUBIC:
filterp = &BICUBIC; filterp = &BICUBIC;
break; break;
case IMAGING_TRANSFORM_LANCZOS:
filterp = &LANCZOS;
break;
default: default:
return (Imaging) ImagingError_ValueError( return (Imaging) ImagingError_ValueError(
"unsupported resampling filter" "unsupported resampling filter"