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
NEAREST = NONE = 0
LANCZOS = ANTIALIAS = 1
BOX = 4
BILINEAR = LINEAR = 2
HAMMING = 5
BICUBIC = CUBIC = 3
LANCZOS = ANTIALIAS = 1
# dithers
NONE = 0
NEAREST = 0
NEAREST = NONE = 0
ORDERED = 1 # Not yet implemented
RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default
@ -1518,16 +1519,18 @@ class Image(object):
:param size: The requested size in pixels, as a 2-tuple:
(width, height).
:param resample: An optional resampling filter. This can be
one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour),
:py:attr:`PIL.Image.BILINEAR` (linear interpolation),
:py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation), or
:py:attr:`PIL.Image.LANCZOS` (a high-quality downsampling filter).
one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`,
:py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`,
:py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`.
If omitted, or if the image has mode "1" or "P", it is
set :py:attr:`PIL.Image.NEAREST`.
See: :ref:`concept-filters`.
: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")
self.load()
@ -1560,7 +1563,7 @@ class Image(object):
environment), or :py:attr:`PIL.Image.BICUBIC`
(cubic spline interpolation in a 4x4 environment).
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
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

View File

@ -10,7 +10,7 @@ class TestImagingResampleVulnerability(PillowTestCase):
ysize = 1000 # unimportant
try:
# 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")
except MemoryError:
self.assertTrue(True, "Should raise MemoryError")
@ -89,6 +89,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
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):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (8, 8), 0xe1)
@ -98,6 +107,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split():
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):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (12, 12), 0xe1)
@ -119,6 +137,15 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split():
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):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (2, 2), 0xe1)
@ -128,6 +155,17 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
for channel in case.split():
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):
for mode in ['RGBX', 'RGB', 'La', 'L']:
case = self.make_case(mode, (4, 4), 0xe1)
@ -211,14 +249,18 @@ class CoreResampleAlphaCorrectTest(PillowTestCase):
@unittest.skip("current implementation isn't precise enough")
def test_levels_rgba(self):
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.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
@unittest.skip("current implementation isn't precise enough")
def test_levels_la(self):
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.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
@ -243,13 +285,17 @@ class CoreResampleAlphaCorrectTest(PillowTestCase):
def test_dirty_pixels_rgba(self):
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.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.LANCZOS), (255, 255, 0))
def test_dirty_pixels_la(self):
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.HAMMING), (255,))
self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (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)
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)
self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (15, 12))
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)
self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (212, 195))
@ -64,7 +66,8 @@ class TestImagingCoreResize(PillowTestCase):
}
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
references = dict(
(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*.
``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``
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
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``
For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value.
@ -128,8 +145,12 @@ Filters comparison table
+============+=============+===========+=============+
|``NEAREST`` | | | ⭐⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BOX`` | ⭐ | | ⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BILINEAR``| ⭐ | ⭐ | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``HAMMING`` | ⭐⭐ | | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BICUBIC`` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
+------------+-------------+-----------+-------------+
|``LANCZOS`` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |

View File

@ -229,9 +229,11 @@ extern void ImagingError_Clear(void);
/* standard filters */
#define IMAGING_TRANSFORM_NEAREST 0
#define IMAGING_TRANSFORM_LANCZOS 1
#define IMAGING_TRANSFORM_BOX 4
#define IMAGING_TRANSFORM_BILINEAR 2
#define IMAGING_TRANSFORM_HAMMING 5
#define IMAGING_TRANSFORM_BICUBIC 3
#define IMAGING_TRANSFORM_LANCZOS 1
typedef int (*ImagingTransformMap)(double* X, double* Y,
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))
struct filter {
double (*filter)(double x);
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)
{
if (x == 0.0)
@ -27,33 +66,11 @@ static inline double lanczos_filter(double x)
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 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 BOX = { box_filter, 0.5 };
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 LANCZOS = { lanczos_filter, 3.0 };
/* 8 bits for result. Filter can have negative areas.
@ -524,15 +541,21 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter)
/* check filter */
switch (filter) {
case IMAGING_TRANSFORM_LANCZOS:
filterp = &LANCZOS;
case IMAGING_TRANSFORM_BOX:
filterp = &BOX;
break;
case IMAGING_TRANSFORM_BILINEAR:
filterp = &BILINEAR;
break;
case IMAGING_TRANSFORM_HAMMING:
filterp = &HAMMING;
break;
case IMAGING_TRANSFORM_BICUBIC:
filterp = &BICUBIC;
break;
case IMAGING_TRANSFORM_LANCZOS:
filterp = &LANCZOS;
break;
default:
return (Imaging) ImagingError_ValueError(
"unsupported resampling filter"