mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-06 14:13:15 +03:00
Merge pull request #4273 from uploadcare/reduce-in-resize
Reduce for resize
This commit is contained in:
commit
c3232e5093
|
@ -136,6 +136,93 @@ class TestImagingCoreResize(PillowTestCase):
|
||||||
self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9)
|
self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9)
|
||||||
|
|
||||||
|
|
||||||
|
class TestReducingGapResize(PillowTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gradients_image = Image.open("Tests/images/radial_gradients.png")
|
||||||
|
cls.gradients_image.load()
|
||||||
|
|
||||||
|
def test_reducing_gap_values(self):
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None)
|
||||||
|
im = self.gradients_image.resize((52, 34), Image.BICUBIC)
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99)
|
||||||
|
|
||||||
|
def test_reducing_gap_1(self):
|
||||||
|
for box, epsilon in [
|
||||||
|
(None, 4),
|
||||||
|
((1.1, 2.2, 510.8, 510.9), 4),
|
||||||
|
((3, 10, 410, 256), 10),
|
||||||
|
]:
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box)
|
||||||
|
im = self.gradients_image.resize(
|
||||||
|
(52, 34), Image.BICUBIC, box=box, reducing_gap=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
self.assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
def test_reducing_gap_2(self):
|
||||||
|
for box, epsilon in [
|
||||||
|
(None, 1.5),
|
||||||
|
((1.1, 2.2, 510.8, 510.9), 1.5),
|
||||||
|
((3, 10, 410, 256), 1),
|
||||||
|
]:
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box)
|
||||||
|
im = self.gradients_image.resize(
|
||||||
|
(52, 34), Image.BICUBIC, box=box, reducing_gap=2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
self.assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
def test_reducing_gap_3(self):
|
||||||
|
for box, epsilon in [
|
||||||
|
(None, 1),
|
||||||
|
((1.1, 2.2, 510.8, 510.9), 1),
|
||||||
|
((3, 10, 410, 256), 0.5),
|
||||||
|
]:
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box)
|
||||||
|
im = self.gradients_image.resize(
|
||||||
|
(52, 34), Image.BICUBIC, box=box, reducing_gap=3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
self.assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
def test_reducing_gap_8(self):
|
||||||
|
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box)
|
||||||
|
im = self.gradients_image.resize(
|
||||||
|
(52, 34), Image.BICUBIC, box=box, reducing_gap=8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
def test_box_filter(self):
|
||||||
|
for box, epsilon in [
|
||||||
|
((0, 0, 512, 512), 5.5),
|
||||||
|
((0.9, 1.7, 128, 128), 9.5),
|
||||||
|
]:
|
||||||
|
ref = self.gradients_image.resize((52, 34), Image.BOX, box=box)
|
||||||
|
im = self.gradients_image.resize(
|
||||||
|
(52, 34), Image.BOX, box=box, reducing_gap=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
|
||||||
class TestImageResize(PillowTestCase):
|
class TestImageResize(PillowTestCase):
|
||||||
def test_resize(self):
|
def test_resize(self):
|
||||||
def resize(mode, size):
|
def resize(mode, size):
|
||||||
|
|
|
@ -65,8 +65,36 @@ class TestImageThumbnail(PillowTestCase):
|
||||||
im.paste(Image.new("RGB", (235, 235)), (11, 11))
|
im.paste(Image.new("RGB", (235, 235)), (11, 11))
|
||||||
|
|
||||||
thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0))
|
thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0))
|
||||||
thumb.thumbnail((32, 32), Image.BICUBIC)
|
# small reducing_gap to amplify the effect
|
||||||
|
thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0)
|
||||||
|
|
||||||
ref = im.resize((32, 32), Image.BICUBIC)
|
ref = im.resize((32, 32), Image.BICUBIC)
|
||||||
# This is still JPEG, some error is present. Without the fix it is 11.5
|
# This is still JPEG, some error is present. Without the fix it is 11.5
|
||||||
self.assert_image_similar(thumb, ref, 1.5)
|
self.assert_image_similar(thumb, ref, 1.5)
|
||||||
|
|
||||||
|
def test_reducing_gap_values(self):
|
||||||
|
im = hopper()
|
||||||
|
im.thumbnail((18, 18), Image.BICUBIC)
|
||||||
|
|
||||||
|
ref = hopper()
|
||||||
|
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0)
|
||||||
|
# reducing_gap=2.0 should be the default
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
ref = hopper()
|
||||||
|
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
self.assert_image_similar(ref, im, 3.5)
|
||||||
|
|
||||||
|
def test_reducing_gap_for_DCT_scaling(self):
|
||||||
|
with Image.open("Tests/images/hopper.jpg") as ref:
|
||||||
|
# thumbnail should call draft with reducing_gap scale
|
||||||
|
ref.draft(None, (18 * 3, 18 * 3))
|
||||||
|
ref = ref.resize((18, 18), Image.BICUBIC)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
|
im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0)
|
||||||
|
|
||||||
|
self.assert_image_equal(ref, im)
|
||||||
|
|
|
@ -87,6 +87,33 @@ Custom unidentified image error
|
||||||
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
|
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
|
||||||
identified. For backwards compatibility, this will inherit from ``IOError``.
|
identified. For backwards compatibility, this will inherit from ``IOError``.
|
||||||
|
|
||||||
|
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Speeds up resizing by resizing the image in two steps. The bigger ``reducing_gap``,
|
||||||
|
the closer the result to the fair resampling. The smaller ``reducing_gap``,
|
||||||
|
the faster resizing. With ``reducing_gap`` greater or equal to 3.0,
|
||||||
|
the result is indistinguishable from fair resampling.
|
||||||
|
|
||||||
|
The default value for :py:meth:`~PIL.Image.Image.resize` is ``None``,
|
||||||
|
which means that the optimization is turned off by default.
|
||||||
|
|
||||||
|
The default value for :py:meth:`~PIL.Image.Image.thumbnail` is 2.0,
|
||||||
|
which is very close to fair resampling while still being faster in many cases.
|
||||||
|
In addition, the same gap is applied when :py:meth:`~PIL.Image.Image.thumbnail`
|
||||||
|
calls :py:meth:`~PIL.Image.Image.draft`, which may greatly improve the quality
|
||||||
|
of JPEG thumbnails. As a result, :py:meth:`~PIL.Image.Image.thumbnail`
|
||||||
|
in the new version provides equally high speed and high quality from any
|
||||||
|
source (JPEG or arbitrary images).
|
||||||
|
|
||||||
|
New Image.reduce() method
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
:py:meth:`~PIL.Image.Image.reduce` is a highly efficient operation
|
||||||
|
to reduce an image by integer times. Normally, it shouldn't be used directly.
|
||||||
|
Used internally by :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail`
|
||||||
|
methods to speed up resize when a new argument ``reducing_gap`` is set.
|
||||||
|
|
||||||
Loading WMF images at a given DPI
|
Loading WMF images at a given DPI
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -63,8 +63,9 @@ def _save(im, fp, filename):
|
||||||
fp.write(struct.pack("<H", 32)) # wBitCount(2)
|
fp.write(struct.pack("<H", 32)) # wBitCount(2)
|
||||||
|
|
||||||
image_io = BytesIO()
|
image_io = BytesIO()
|
||||||
|
# TODO: invent a more convenient method for proportional scalings
|
||||||
tmp = im.copy()
|
tmp = im.copy()
|
||||||
tmp.thumbnail(size, Image.LANCZOS)
|
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
|
||||||
tmp.save(image_io, "png")
|
tmp.save(image_io, "png")
|
||||||
image_io.seek(0)
|
image_io.seek(0)
|
||||||
image_bytes = image_io.read()
|
image_bytes = image_io.read()
|
||||||
|
|
|
@ -144,6 +144,9 @@ HAMMING = 5
|
||||||
BICUBIC = CUBIC = 3
|
BICUBIC = CUBIC = 3
|
||||||
LANCZOS = ANTIALIAS = 1
|
LANCZOS = ANTIALIAS = 1
|
||||||
|
|
||||||
|
_filters_support = {BOX: 0.5, BILINEAR: 1.0, HAMMING: 1.0, BICUBIC: 2.0, LANCZOS: 3.0}
|
||||||
|
|
||||||
|
|
||||||
# dithers
|
# dithers
|
||||||
NEAREST = NONE = 0
|
NEAREST = NONE = 0
|
||||||
ORDERED = 1 # Not yet implemented
|
ORDERED = 1 # Not yet implemented
|
||||||
|
@ -1763,7 +1766,24 @@ class Image:
|
||||||
|
|
||||||
return m_im
|
return m_im
|
||||||
|
|
||||||
def resize(self, size, resample=BICUBIC, box=None):
|
def _get_safe_box(self, size, resample, box):
|
||||||
|
"""Expands the box so it includes adjacent pixels
|
||||||
|
that may be used by resampling with the given resampling filter.
|
||||||
|
"""
|
||||||
|
filter_support = _filters_support[resample] - 0.5
|
||||||
|
scale_x = (box[2] - box[0]) / size[0]
|
||||||
|
scale_y = (box[3] - box[1]) / size[1]
|
||||||
|
support_x = filter_support * scale_x
|
||||||
|
support_y = filter_support * scale_y
|
||||||
|
|
||||||
|
return (
|
||||||
|
max(0, int(box[0] - support_x)),
|
||||||
|
max(0, int(box[1] - support_y)),
|
||||||
|
min(self.size[0], math.ceil(box[2] + support_x)),
|
||||||
|
min(self.size[1], math.ceil(box[3] + support_y)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None):
|
||||||
"""
|
"""
|
||||||
Returns a resized copy of this image.
|
Returns a resized copy of this image.
|
||||||
|
|
||||||
|
@ -1781,6 +1801,18 @@ class Image:
|
||||||
the source image region to be scaled.
|
the source image region to be scaled.
|
||||||
The values must be within (0, 0, width, height) rectangle.
|
The values must be within (0, 0, width, height) rectangle.
|
||||||
If omitted or None, the entire source is used.
|
If omitted or None, the entire source is used.
|
||||||
|
:param reducing_gap: Apply optimization by resizing the image
|
||||||
|
in two steps. First, reducing the image by integer times
|
||||||
|
using :py:meth:`~PIL.Image.Image.reduce`.
|
||||||
|
Second, resizing using regular resampling. The last step
|
||||||
|
changes size no less than by ``reducing_gap`` times.
|
||||||
|
``reducing_gap`` may be None (no first step is performed)
|
||||||
|
or should be greater than 1.0. The bigger ``reducing_gap``,
|
||||||
|
the closer the result to the fair resampling.
|
||||||
|
The smaller ``reducing_gap``, the faster resizing.
|
||||||
|
With ``reducing_gap`` greater or equal to 3.0, the result is
|
||||||
|
indistinguishable from fair resampling in most cases.
|
||||||
|
The default value is None (no optimization).
|
||||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1802,6 +1834,9 @@ class Image:
|
||||||
message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
|
message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if reducing_gap is not None and reducing_gap < 1.0:
|
||||||
|
raise ValueError("reducing_gap must be 1.0 or greater")
|
||||||
|
|
||||||
size = tuple(size)
|
size = tuple(size)
|
||||||
|
|
||||||
if box is None:
|
if box is None:
|
||||||
|
@ -1822,6 +1857,19 @@ class Image:
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
if reducing_gap is not None and resample != NEAREST:
|
||||||
|
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
|
||||||
|
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
|
||||||
|
if factor_x > 1 or factor_y > 1:
|
||||||
|
reduce_box = self._get_safe_box(size, resample, box)
|
||||||
|
self = self.reduce((factor_x, factor_y), box=reduce_box)
|
||||||
|
box = (
|
||||||
|
(box[0] - reduce_box[0]) / factor_x,
|
||||||
|
(box[1] - reduce_box[1]) / factor_y,
|
||||||
|
(box[2] - reduce_box[0]) / factor_x,
|
||||||
|
(box[3] - reduce_box[1]) / factor_y,
|
||||||
|
)
|
||||||
|
|
||||||
return self._new(self.im.resize(size, resample, box))
|
return self._new(self.im.resize(size, resample, box))
|
||||||
|
|
||||||
def reduce(self, factor, box=None):
|
def reduce(self, factor, box=None):
|
||||||
|
@ -2147,7 +2195,7 @@ class Image:
|
||||||
"""
|
"""
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def thumbnail(self, size, resample=BICUBIC):
|
def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0):
|
||||||
"""
|
"""
|
||||||
Make this image into a thumbnail. This method modifies the
|
Make this image into a thumbnail. This method modifies the
|
||||||
image to contain a thumbnail version of itself, no larger than
|
image to contain a thumbnail version of itself, no larger than
|
||||||
|
@ -2166,7 +2214,21 @@ class Image:
|
||||||
of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`,
|
of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`,
|
||||||
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`.
|
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`.
|
||||||
If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`.
|
If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`.
|
||||||
(was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0)
|
(was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0).
|
||||||
|
:param reducing_gap: Apply optimization by resizing the image
|
||||||
|
in two steps. First, reducing the image by integer times
|
||||||
|
using :py:meth:`~PIL.Image.Image.reduce` or
|
||||||
|
:py:meth:`~PIL.Image.Image.draft` for JPEG images.
|
||||||
|
Second, resizing using regular resampling. The last step
|
||||||
|
changes size no less than by ``reducing_gap`` times.
|
||||||
|
``reducing_gap`` may be None (no first step is performed)
|
||||||
|
or should be greater than 1.0. The bigger ``reducing_gap``,
|
||||||
|
the closer the result to the fair resampling.
|
||||||
|
The smaller ``reducing_gap``, the faster resizing.
|
||||||
|
With ``reducing_gap`` greater or equal to 3.0, the result is
|
||||||
|
indistinguishable from fair resampling in most cases.
|
||||||
|
The default value is 2.0 (very close to fair resampling
|
||||||
|
while still being faster in many cases).
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2184,12 +2246,13 @@ class Image:
|
||||||
if size == self.size:
|
if size == self.size:
|
||||||
return
|
return
|
||||||
|
|
||||||
res = self.draft(None, size)
|
if reducing_gap is not None:
|
||||||
if res is not None:
|
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
|
||||||
box = res[1]
|
if res is not None:
|
||||||
|
box = res[1]
|
||||||
|
|
||||||
if self.size != size:
|
if self.size != size:
|
||||||
im = self.resize(size, resample, box=box)
|
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
|
||||||
|
|
||||||
self.im = im.im
|
self.im = im.im
|
||||||
self._size = size
|
self._size = size
|
||||||
|
|
Loading…
Reference in New Issue
Block a user