mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-12 10:16:17 +03:00
Merge pull request #1959 from uploadcare/new-filters
Add Box and Hamming filters for resampling
This commit is contained in:
commit
e8c123c833
21
PIL/Image.py
21
PIL/Image.py
|
@ -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
|
||||
|
|
|
@ -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,))
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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`` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user