Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,14 @@ def isImageType(t):

# 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
Expand Down Expand Up @@ -1518,16 +1519,18 @@ def resize(self, size, resample=NEAREST):
: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()
Expand Down Expand Up @@ -1560,7 +1563,7 @@ def rotate(self, angle, resample=NEAREST, expand=0):
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
Expand Down
48 changes: 47 additions & 1 deletion Tests/test_image_resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_overflow(self):
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")
Expand Down Expand Up @@ -89,6 +89,15 @@ def serialize_image(self, image):
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)
Expand All @@ -98,6 +107,15 @@ def test_reduce_bilinear(self):
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)
Expand All @@ -119,6 +137,15 @@ def test_reduce_lanczos(self):
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)
Expand All @@ -128,6 +155,17 @@ def test_enlarge_bilinear(self):
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)
Expand Down Expand Up @@ -211,14 +249,18 @@ def run_levels_case(self, i):
@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))

Expand All @@ -243,13 +285,17 @@ def run_dity_case(self, i, clean_pixel):

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,))

Expand Down
9 changes: 6 additions & 3 deletions Tests/test_image_resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ def test_convolution_modes(self):
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))
Expand All @@ -64,7 +66,8 @@ def test_endianness(self):
}
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))
Expand Down
23 changes: 22 additions & 1 deletion docs/handbook/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,31 @@ 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
on all pixels that may contribute to the output value.
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.
Expand All @@ -128,8 +145,12 @@ Filters comparison table
+============+=============+===========+=============+
|``NEAREST`` | | | ⭐⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BOX`` | ⭐ | | ⭐⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BILINEAR``| ⭐ | ⭐ | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``HAMMING`` | ⭐⭐ | | ⭐⭐⭐ |
+------------+-------------+-----------+-------------+
|``BICUBIC`` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
+------------+-------------+-----------+-------------+
|``LANCZOS`` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
Expand Down
4 changes: 3 additions & 1 deletion libImaging/Imaging.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any logical order here? It doesn't appear to be numeric, alphabetical, or order added.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From less expensive and quality to more. Same as in the comparison table.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wiredfool Are you happy with this explanation?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, though I expect someone is going to helpfully clean up the order in a PR in a year or two.


typedef int (*ImagingTransformMap)(double* X, double* Y,
int x, int y, void* data);
Expand Down
55 changes: 39 additions & 16 deletions libImaging/Resample.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,15 @@

#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 sinc_filter(double x)
static inline double box_filter(double x)
{
if (x == 0.0)
if (x >= -0.5 && x < 0.5)
return 1.0;
x = x * M_PI;
return sin(x) / x;
}

static inline double lanczos_filter(double x)
{
/* truncated sinc */
if (-3.0 <= x && x < 3.0)
return sinc_filter(x) * sinc_filter(x/3);
return 0.0;
}

Expand All @@ -36,6 +26,16 @@ static inline double bilinear_filter(double 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 */
Expand All @@ -50,10 +50,27 @@ static inline double bicubic_filter(double x)
#undef a
}

static struct filter LANCZOS = { lanczos_filter, 3.0 };
static inline double sinc_filter(double x)
{
if (x == 0.0)
return 1.0;
x = x * M_PI;
return sin(x) / x;
}

static inline double lanczos_filter(double x)
{
/* truncated sinc */
if (-3.0 <= x && x < 3.0)
return sinc_filter(x) * sinc_filter(x/3);
return 0.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.
Expand Down Expand Up @@ -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"
Expand Down