Merge pull request #997 from homm/replace-resize

Replace resize method
This commit is contained in:
wiredfool 2014-11-27 10:26:48 -08:00
commit e16ee15f2c
11 changed files with 151 additions and 216 deletions

View File

@ -24,7 +24,7 @@ Changelog (Pillow)
- Use PyQt4 if it has already been imported, otherwise prefer PyQt5. #1003
[AurelienBallier]
- Speedup stretch implementation up to 2.5 times. #977
- Speedup resample implementation up to 2.5 times. #977
[homm]
- Speed up rotation by using cache aware loops, added transpose to rotations. #994

View File

@ -879,7 +879,7 @@ class Image:
elif self.mode == 'P' and mode == 'RGBA':
t = self.info['transparency']
delete_trns = True
if isinstance(t, bytes):
self.im.putpalettealphas(t)
elif isinstance(t, int):
@ -1523,9 +1523,8 @@ class Image:
(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 in a 2x2
environment), :py:attr:`PIL.Image.BICUBIC` (cubic spline
interpolation in a 4x4 environment), or
:py:attr:`PIL.Image.BILINEAR` (linear interpolation),
:py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation), or
:py:attr:`PIL.Image.ANTIALIAS` (a high-quality downsampling filter).
If omitted, or if the image has mode "1" or "P", it is
set :py:attr:`PIL.Image.NEAREST`.
@ -1547,16 +1546,7 @@ class Image:
if self.mode == 'RGBA':
return self.convert('RGBa').resize(size, resample).convert('RGBA')
if resample == ANTIALIAS:
# requires stretch support (imToolkit & PIL 1.1.3)
try:
im = self.im.stretch(size, resample)
except AttributeError:
raise ValueError("unsupported resampling filter")
else:
im = self.im.resize(size, resample)
return self._new(im)
return self._new(self.im.resize(size, resample))
def rotate(self, angle, resample=NEAREST, expand=0):
"""
@ -1772,12 +1762,7 @@ class Image:
:py:meth:`~PIL.Image.Image.draft` method to configure the file reader
(where applicable), and finally resizes the image.
Note that the bilinear and bicubic filters in the current
version of PIL are not well-suited for thumbnail generation.
You should use :py:attr:`PIL.Image.ANTIALIAS` unless speed is much more
important than quality.
Also note that this function modifies the :py:class:`~PIL.Image.Image`
Note that this function modifies the :py:class:`~PIL.Image.Image`
object in place. If you need to use the full resolution image as well,
apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original
image.
@ -1785,10 +1770,9 @@ class Image:
:param size: Requested size.
:param resample: Optional resampling filter. This can be one
of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`,
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.ANTIALIAS`
(best quality). If omitted, it defaults to
:py:attr:`PIL.Image.ANTIALIAS`. (was :py:attr:`PIL.Image.NEAREST`
prior to version 2.5.0)
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.ANTIALIAS`.
If omitted, it defaults to :py:attr:`PIL.Image.ANTIALIAS`.
(was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0)
:returns: None
"""
@ -1807,14 +1791,7 @@ class Image:
self.draft(None, size)
self.load()
try:
im = self.resize(size, resample)
except ValueError:
if resample != ANTIALIAS:
raise
im = self.resize(size, NEAREST) # fallback
im = self.resize(size, resample)
self.im = im.im
self.mode = im.mode

View File

@ -1,5 +1,91 @@
"""
Tests for resize functionality.
"""
from itertools import permutations
from helper import unittest, PillowTestCase, hopper
from PIL import Image
class TestImagingCoreResize(PillowTestCase):
def resize(self, im, size, f):
# Image class independent version of resize.
im.load()
return im._new(im.im.resize(size, f))
def test_nearest_mode(self):
for mode in ["1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr",
"I;16"]: # exotic mode
im = hopper(mode)
r = self.resize(im, (15, 12), Image.NEAREST)
self.assertEqual(r.mode, mode)
self.assertEqual(r.size, (15, 12) )
self.assertEqual(r.im.bands, im.im.bands)
def test_convolution_modes(self):
self.assertRaises(ValueError, self.resize, hopper("1"),
(15, 12), Image.BILINEAR)
self.assertRaises(ValueError, self.resize, hopper("P"),
(15, 12), Image.BILINEAR)
self.assertRaises(ValueError, self.resize, hopper("I;16"),
(15, 12), Image.BILINEAR)
for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]:
im = hopper(mode)
r = self.resize(im, (15, 12), Image.BILINEAR)
self.assertEqual(r.mode, mode)
self.assertEqual(r.size, (15, 12) )
self.assertEqual(r.im.bands, im.im.bands)
def test_reduce_filters(self):
for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.ANTIALIAS]:
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.ANTIALIAS]:
r = self.resize(hopper("RGB"), (212, 195), f)
self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (212, 195))
def test_endianness(self):
# Make an image with one colored pixel, in one channel.
# When resized, that channel should be the same as a GS image.
# Other channels should be unaffected.
# The R and A channels should not swap, which is indicitive of
# an endianness issues.
samples = {
'blank': Image.new('L', (2, 2), 0),
'filled': Image.new('L', (2, 2), 255),
'dirty': Image.new('L', (2, 2), 0),
}
samples['dirty'].putpixel((1, 1), 128)
for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.ANTIALIAS]:
# samples resized with current filter
references = dict(
(name, self.resize(ch, (4, 4), f))
for name, ch in samples.items()
)
for mode, channels_set in [
('RGB', ('blank', 'filled', 'dirty')),
('RGBA', ('blank', 'blank', 'filled', 'dirty')),
('LA', ('filled', 'dirty')),
]:
for channels in set(permutations(channels_set)):
# compile image from different channels permutations
im = Image.merge(mode, [samples[ch] for ch in channels])
resized = self.resize(im, (4, 4), f)
for i, ch in enumerate(resized.split()):
# check what resized channel in image is the same
# as separately resized channel
self.assert_image_equal(ch, references[channels[i]])
class TestImageResize(PillowTestCase):
@ -9,8 +95,8 @@ class TestImageResize(PillowTestCase):
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, size)
for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (100, 100))
resize(mode, (200, 200))
resize(mode, (112, 103))
resize(mode, (188, 214))
if __name__ == '__main__':

View File

@ -44,7 +44,9 @@ class TestImageTransform(PillowTestCase):
w//2, h//2, w//2, 0),
Image.BILINEAR)
scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h))
scaled = im.transform((w, h), Image.AFFINE,
(.5, 0, 0, 0, .5, 0),
Image.BILINEAR)
self.assert_image_equal(transformed, scaled)
@ -61,9 +63,9 @@ class TestImageTransform(PillowTestCase):
w, h, w, 0))], # ul -> ccw around quad
Image.BILINEAR)
# transformed.save('transformed.png')
scaled = im.resize((w//2, h//2), Image.BILINEAR)
scaled = im.transform((w//2, h//2), Image.AFFINE,
(2, 0, 0, 0, 2, 0),
Image.BILINEAR)
checker = Image.new('RGBA', im.size)
checker.paste(scaled, (0, 0))
@ -128,7 +130,8 @@ class TestImageTransform(PillowTestCase):
foo = [
Image.new('RGBA', (1024, 1024), (a, a, a, a))
for a in range(1, 65)]
for a in range(1, 65)
]
# Yeah. Watch some JIT optimize this out.
foo = None

View File

@ -1,86 +0,0 @@
"""
Tests for ImagingCore.stretch functionality.
"""
from itertools import permutations
from helper import unittest, PillowTestCase
from PIL import Image
im = Image.open("Tests/images/hopper.ppm").copy()
class TestImagingStretch(PillowTestCase):
def stretch(self, im, size, f):
return im._new(im.im.stretch(size, f))
def test_modes(self):
self.assertRaises(ValueError, im.convert("1").im.stretch,
(15, 12), Image.ANTIALIAS)
self.assertRaises(ValueError, im.convert("P").im.stretch,
(15, 12), Image.ANTIALIAS)
for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]:
s = im.convert(mode).im
r = s.stretch((15, 12), Image.ANTIALIAS)
self.assertEqual(r.mode, mode)
self.assertEqual(r.size, (15, 12))
self.assertEqual(r.bands, s.bands)
def test_reduce_filters(self):
# There is no Image.NEAREST because im.stretch implementation
# is not NEAREST for reduction. It should be removed
# or renamed to supersampling.
for f in [Image.BILINEAR, Image.BICUBIC, Image.ANTIALIAS]:
r = im.im.stretch((15, 12), f)
self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (15, 12))
def test_enlarge_filters(self):
for f in [Image.BILINEAR, Image.BICUBIC, Image.ANTIALIAS]:
r = im.im.stretch((764, 414), f)
self.assertEqual(r.mode, "RGB")
self.assertEqual(r.size, (764, 414))
def test_endianness(self):
# Make an image with one colored pixel, in one channel.
# When stretched, that channel should be the same as a GS image.
# Other channels should be unaffected.
# The R and A channels should not swap, which is indicitive of
# an endianness issues.
samples = {
'blank': Image.new('L', (2, 2), 0),
'filled': Image.new('L', (2, 2), 255),
'dirty': Image.new('L', (2, 2), 0),
}
samples['dirty'].putpixel((1, 1), 128)
for f in [Image.BILINEAR, Image.BICUBIC, Image.ANTIALIAS]:
# samples resized with current filter
resized = dict(
(name, self.stretch(ch, (4, 4), f))
for name, ch in samples.items()
)
for mode, channels_set in [
('RGB', ('blank', 'filled', 'dirty')),
('RGBA', ('blank', 'blank', 'filled', 'dirty')),
('LA', ('filled', 'dirty')),
]:
for channels in set(permutations(channels_set)):
# compile image from different channels permutations
im = Image.merge(mode, [samples[ch] for ch in channels])
stretched = self.stretch(im, (4, 4), f)
for i, ch in enumerate(stretched.split()):
# check what resized channel in image is the same
# as separately resized channel
self.assert_image_equal(ch, resized[channels[i]])
if __name__ == '__main__':
unittest.main()
# End of file

View File

@ -1514,9 +1514,26 @@ _resize(ImagingObject* self, PyObject* args)
imIn = self->image;
imOut = ImagingNew(imIn->mode, xsize, ysize);
if (imOut)
(void) ImagingResize(imOut, imIn, filter);
if (imIn->xsize == xsize && imIn->ysize == ysize) {
imOut = ImagingCopy(imIn);
}
else if ( ! filter) {
double a[6];
memset(a, 0, sizeof a);
a[1] = (double) imIn->xsize / xsize;
a[5] = (double) imIn->ysize / ysize;
imOut = ImagingNew(imIn->mode, xsize, ysize);
imOut = ImagingTransformAffine(
imOut, imIn,
0, 0, xsize, ysize,
a, filter, 1);
}
else {
imOut = ImagingResample(imIn, xsize, ysize, filter);
}
return PyImagingNew(imOut);
}
@ -1610,25 +1627,6 @@ im_setmode(ImagingObject* self, PyObject* args)
return Py_None;
}
static PyObject*
_stretch(ImagingObject* self, PyObject* args)
{
Imaging imIn, imOut;
int xsize, ysize;
int filter = IMAGING_TRANSFORM_NEAREST;
if (!PyArg_ParseTuple(args, "(ii)|i", &xsize, &ysize, &filter))
return NULL;
imIn = self->image;
imOut = ImagingStretch(imIn, xsize, ysize, filter);
if ( ! imOut) {
return NULL;
}
return PyImagingNew(imOut);
}
static PyObject*
_transform2(ImagingObject* self, PyObject* args)
@ -3031,8 +3029,10 @@ static struct PyMethodDef methods[] = {
{"rankfilter", (PyCFunction)_rankfilter, 1},
#endif
{"resize", (PyCFunction)_resize, 1},
// There were two methods for image resize before.
// Starting from Pillow 2.7.0 stretch is depreciated.
{"stretch", (PyCFunction)_resize, 1},
{"rotate", (PyCFunction)_rotate, 1},
{"stretch", (PyCFunction)_stretch, 1},
{"transpose", (PyCFunction)_transpose, 1},
{"transform2", (PyCFunction)_transform2, 1},

View File

@ -89,25 +89,21 @@ pixel, the Python Imaging Library provides four different resampling *filters*.
Pick the nearest pixel from the input image. Ignore all other input pixels.
``BILINEAR``
Use linear interpolation over a 2x2 environment in the input image. Note
that in the current version of PIL, this filter uses a fixed input
environment when downsampling.
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.
``BICUBIC``
Use cubic interpolation over a 4x4 environment in the input image. Note
that in the current version of PIL, this filter uses a fixed input
environment when downsampling.
For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value.
For other transformations cubic interpolation over a 4x4 environment
in the input image is used.
``ANTIALIAS``
Calculate the output pixel value using a high-quality resampling filter (a
Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value. In
the current version of PIL, this filter can only be used with the resize
and thumbnail methods.
.. versionadded:: 1.1.3
Note that in the current version of PIL, the ``ANTIALIAS`` filter is the only
filter that behaves properly when downsampling (that is, when converting a
large image to a small one). The ``BILINEAR`` and ``BICUBIC`` filters use a
fixed input environment, and are best used for scale-preserving geometric
transforms and upsamping.

View File

@ -979,30 +979,6 @@ ImagingTransformQuad(Imaging imOut, Imaging imIn,
/* -------------------------------------------------------------------- */
/* Convenience functions */
Imaging
ImagingResize(Imaging imOut, Imaging imIn, int filterid)
{
double a[6];
if (imOut->xsize == imIn->xsize && imOut->ysize == imIn->ysize)
return ImagingCopy2(imOut, imIn);
memset(a, 0, sizeof a);
a[1] = (double) imIn->xsize / imOut->xsize;
a[5] = (double) imIn->ysize / imOut->ysize;
if (!filterid && imIn->type != IMAGING_TYPE_SPECIAL)
return ImagingScaleAffine(
imOut, imIn,
0, 0, imOut->xsize, imOut->ysize,
a, 1);
return ImagingTransformAffine(
imOut, imIn,
0, 0, imOut->xsize, imOut->ysize,
a, filterid, 1);
}
Imaging
ImagingRotate(Imaging imOut, Imaging imIn, double theta, int filterid)
{

View File

@ -286,13 +286,12 @@ extern Imaging ImagingPointTransform(
Imaging imIn, double scale, double offset);
extern Imaging ImagingPutBand(Imaging im, Imaging imIn, int band);
extern Imaging ImagingRankFilter(Imaging im, int size, int rank);
extern Imaging ImagingResize(Imaging imOut, Imaging imIn, int filter);
extern Imaging ImagingRotate(
Imaging imOut, Imaging imIn, double theta, int filter);
extern Imaging ImagingRotate90(Imaging imOut, Imaging imIn);
extern Imaging ImagingRotate180(Imaging imOut, Imaging imIn);
extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn);
extern Imaging ImagingStretch(Imaging imIn, int xsize, int ysize, int filter);
extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter);
extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn);
extern Imaging ImagingTransposeToNew(Imaging imIn);
extern Imaging ImagingTransformPerspective(

View File

@ -2,7 +2,7 @@
* The Python Imaging Library
* $Id$
*
* pilopen antialiasing support
* Pillow image resamling support
*
* history:
* 2002-03-09 fl Created (for PIL 1.1.3)
@ -17,8 +17,6 @@
#include <math.h>
/* resampling filters (from antialias.py) */
struct filter {
float (*filter)(float x);
float support;
@ -42,15 +40,6 @@ static inline float antialias_filter(float x)
static struct filter ANTIALIAS = { antialias_filter, 3.0 };
static inline float nearest_filter(float x)
{
if (-0.5 <= x && x < 0.5)
return 1.0;
return 0.0;
}
static struct filter NEAREST = { nearest_filter, 0.5 };
static inline float bilinear_filter(float x)
{
if (x < 0.0)
@ -106,7 +95,7 @@ static float inline i2f(int v) { return (float) v; }
Imaging
ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
ImagingResampleHorizontal(Imaging imIn, int xsize, int filter)
{
ImagingSectionCookie cookie;
Imaging imOut;
@ -119,9 +108,6 @@ ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
/* check filter */
switch (filter) {
case IMAGING_TRANSFORM_NEAREST:
filterp = &NEAREST;
break;
case IMAGING_TRANSFORM_ANTIALIAS:
filterp = &ANTIALIAS;
break;
@ -152,7 +138,7 @@ ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
/* maximum number of coofs */
kmax = (int) ceil(support) * 2 + 1;
/* coefficient buffer (with rounding safety margin) */
/* coefficient buffer */
kk = malloc(xsize * kmax * sizeof(float));
if ( ! kk)
return (Imaging) ImagingError_MemoryError();
@ -208,7 +194,7 @@ ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
ss += i2f(imIn->image8[yy][x]) * k[x - xmin];
imOut->image8[yy][xx] = clip8(ss);
}
} else
} else {
switch(imIn->type) {
case IMAGING_TYPE_UINT8:
/* n-bit grayscale */
@ -283,13 +269,8 @@ ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
IMAGING_PIXEL_F(imOut, xx, yy) = ss;
}
break;
default:
ImagingSectionLeave(&cookie);
ImagingDelete(imOut);
free(kk);
free(xbounds);
return (Imaging) ImagingError_ModeError();
}
}
}
ImagingSectionLeave(&cookie);
free(kk);
@ -299,7 +280,7 @@ ImagingStretchHorizontal(Imaging imIn, int xsize, int filter)
Imaging
ImagingStretch(Imaging imIn, int xsize, int ysize, int filter)
ImagingResample(Imaging imIn, int xsize, int ysize, int filter)
{
Imaging imTemp1, imTemp2, imTemp3;
Imaging imOut;
@ -307,8 +288,11 @@ ImagingStretch(Imaging imIn, int xsize, int ysize, int filter)
if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0)
return (Imaging) ImagingError_ModeError();
if (imIn->type == IMAGING_TYPE_SPECIAL)
return (Imaging) ImagingError_ModeError();
/* two-pass resize, first pass */
imTemp1 = ImagingStretchHorizontal(imIn, xsize, filter);
imTemp1 = ImagingResampleHorizontal(imIn, xsize, filter);
if ( ! imTemp1)
return NULL;
@ -319,7 +303,7 @@ ImagingStretch(Imaging imIn, int xsize, int ysize, int filter)
return NULL;
/* second pass */
imTemp3 = ImagingStretchHorizontal(imTemp2, ysize, filter);
imTemp3 = ImagingResampleHorizontal(imTemp2, ysize, filter);
ImagingDelete(imTemp2);
if ( ! imTemp3)
return NULL;

View File

@ -26,7 +26,7 @@ _IMAGING = (
"decode", "encode", "map", "display", "outline", "path")
_LIB_IMAGING = (
"Access", "AlphaComposite", "Antialias", "Bands", "BitDecode", "Blend",
"Access", "AlphaComposite", "Resample", "Bands", "BitDecode", "Blend",
"Chops", "Convert", "ConvertYCbCr", "Copy", "Crc32", "Crop", "Dib", "Draw",
"Effects", "EpsEncode", "File", "Fill", "Filter", "FliDecode",
"Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode",