diff --git a/CHANGES.rst b/CHANGES.rst index ac3970eb4..618fc4815 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/PIL/Image.py b/PIL/Image.py index 3b51000ee..5cf262668 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -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 diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 603f598d8..79816e450 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -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__': diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index c4111d0b6..4306e9b43 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -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 diff --git a/Tests/test_imaging_stretch.py b/Tests/test_imaging_stretch.py deleted file mode 100644 index aaf1a4d1d..000000000 --- a/Tests/test_imaging_stretch.py +++ /dev/null @@ -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 diff --git a/_imaging.c b/_imaging.c index fe99de742..e11605be4 100644 --- a/_imaging.c +++ b/_imaging.c @@ -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}, diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 61406d179..f374984fc 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -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. diff --git a/libImaging/Geometry.c b/libImaging/Geometry.c index b987a5616..f586974c3 100644 --- a/libImaging/Geometry.c +++ b/libImaging/Geometry.c @@ -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) { diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index a45608c4a..ce20b26b3 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -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( diff --git a/libImaging/Antialias.c b/libImaging/Resample.c similarity index 91% rename from libImaging/Antialias.c rename to libImaging/Resample.c index 1799e7fdc..69c32beba 100644 --- a/libImaging/Antialias.c +++ b/libImaging/Resample.c @@ -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 -/* 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; diff --git a/setup.py b/setup.py index 95d54df01..b1f917f26 100644 --- a/setup.py +++ b/setup.py @@ -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",