Merge pull request #2328 from wiredfool/pr_2155

Add center and translate to Image.rotate
This commit is contained in:
wiredfool 2017-01-01 19:06:53 +00:00 committed by GitHub
commit c84da36474
3 changed files with 140 additions and 39 deletions

View File

@ -1555,7 +1555,7 @@ class Image(object):
return self._new(self.im.resize(size, resample))
def rotate(self, angle, resample=NEAREST, expand=0):
def rotate(self, angle, resample=NEAREST, expand=0, center=None, translate=None):
"""
Returns a rotated copy of this image. This method returns a
copy of this image, rotated the given number of degrees counter
@ -1572,13 +1572,19 @@ class Image(object):
: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
input image.
input image. Note that the expand flag assumes rotation around
the center and no translation.
:param center: Optional center of rotation (a 2-tuple). Origin is
the upper left corner. Default is the center of the image.
:param translate: An optional post-rotate translation (a 2-tuple).
:returns: An :py:class:`~PIL.Image.Image` object.
"""
angle = angle % 360.0
# Fast paths regardless of filter
# Fast paths regardless of filter, as long as we're not
# translating or changing the center.
if not (center or translate):
if angle == 0:
return self.copy()
if angle == 180:
@ -1588,32 +1594,59 @@ class Image(object):
if angle == 270 and expand:
return self.transpose(ROTATE_270)
# Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we
# want to interpolate the (discrete) destination pixel from
# the local area around the (floating) source pixel.
# The matrix we actually want (note that it operates from the right):
# (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx)
# (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy)
# (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1)
# The reverse matrix is thus:
# (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx)
# (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty)
# (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1)
# In any case, the final translation may be updated at the end to
# compensate for the expand flag.
w, h = self.size
if translate is None:
translate = [0, 0]
if center is None:
center = [w / 2.0, h / 2.0]
angle = - math.radians(angle)
matrix = [
round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0,
round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0
]
def transform(x, y, matrix=matrix):
def transform(x, y, matrix):
(a, b, c, d, e, f) = matrix
return a*x + b*y + c, d*x + e*y + f
matrix[2], matrix[5] = transform(-center[0] - translate[0], -center[1] - translate[1], matrix)
matrix[2] += center[0]
matrix[5] += center[1]
w, h = self.size
if expand:
# calculate output size
xx = []
yy = []
for x, y in ((0, 0), (w, 0), (w, h), (0, h)):
x, y = transform(x, y)
x, y = transform(x, y, matrix)
xx.append(x)
yy.append(y)
w = int(math.ceil(max(xx)) - math.floor(min(xx)))
h = int(math.ceil(max(yy)) - math.floor(min(yy)))
nw = int(math.ceil(max(xx)) - math.floor(min(xx)))
nh = int(math.ceil(max(yy)) - math.floor(min(yy)))
# adjust center
x, y = transform(w / 2.0, h / 2.0)
matrix[2] = self.size[0] / 2.0 - x
matrix[5] = self.size[1] / 2.0 - y
# We multiply a translation matrix from the right. Because of its
# special form, this is the same as taking the image of the translation vector
# as new translation vector.
matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix)
w, h = nw, nh
return self.transform((w, h), AFFINE, matrix, resample)

BIN
Tests/images/hopper_45.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -4,12 +4,11 @@ from PIL import Image
class TestImageRotate(PillowTestCase):
def test_rotate(self):
def rotate(im, mode, angle):
out = im.rotate(angle)
def rotate(self, im, mode, angle, center=None, translate=None):
out = im.rotate(angle, center=center, translate=translate)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size) # default rotate clips output
out = im.rotate(angle, expand=1)
out = im.rotate(angle, center=center, translate=translate, expand=1)
self.assertEqual(out.mode, mode)
if angle % 180 == 0:
self.assertEqual(out.size, im.size)
@ -18,18 +17,87 @@ class TestImageRotate(PillowTestCase):
else:
self.assertNotEqual(out.size, im.size)
def test_mode(self):
for mode in ("1", "P", "L", "RGB", "I", "F"):
im = hopper(mode)
rotate(im, mode, 45)
self.rotate(im, mode, 45)
def test_angle(self):
for angle in (0, 90, 180, 270):
im = Image.open('Tests/images/test-card.png')
rotate(im, im.mode, angle)
self.rotate(im, im.mode, angle)
def test_zero(self):
for angle in (0, 45, 90, 180, 270):
im = Image.new('RGB',(0,0))
rotate(im, im.mode, angle)
self.rotate(im, im.mode, angle)
def test_resample(self):
# Target image creation, inspected by eye.
# >>> im = Image.open('Tests/images/hopper.ppm')
# >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True)
# >>> im.save('Tests/images/hopper_45.png')
target = Image.open('Tests/images/hopper_45.png')
for (resample, epsilon) in ((Image.NEAREST, 10),
(Image.BILINEAR, 5),
(Image.BICUBIC, 0)):
im = hopper()
im = im.rotate(45, resample=resample, expand=True)
self.assert_image_similar(im, target, epsilon)
def test_center_0(self):
im = hopper()
target = Image.open('Tests/images/hopper_45.png')
target_origin = target.size[1]/2
target = target.crop((0, target_origin, 128, target_origin + 128))
im = im.rotate(45, center=(0,0), resample=Image.BICUBIC)
self.assert_image_similar(im, target, 15)
def test_center_14(self):
im = hopper()
target = Image.open('Tests/images/hopper_45.png')
target_origin = target.size[1] / 2 - 14
target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
im = im.rotate(45, center=(14,14), resample=Image.BICUBIC)
self.assert_image_similar(im, target, 10)
def test_translate(self):
im = hopper()
target = Image.open('Tests/images/hopper_45.png')
target_origin = (target.size[1] / 2 - 64) - 5
target = target.crop((target_origin, target_origin,
target_origin + 128, target_origin + 128))
im = im.rotate(45, translate=(5,5), resample=Image.BICUBIC)
self.assert_image_similar(im, target, 1)
def test_fastpath_center(self):
# if the center is -1,-1 and we rotate by 90<=x<=270 the
# resulting image should be black
for angle in (90, 180, 270):
im = hopper().rotate(angle, center=(-1,-1))
self.assert_image_equal(im, Image.new('RGB', im.size, 'black'))
def test_fastpath_translate(self):
# if we post-translate by -128
# resulting image should be black
for angle in (0, 90, 180, 270):
im = hopper().rotate(angle, translate=(-128,-128))
self.assert_image_equal(im, Image.new('RGB', im.size, 'black'))
def test_center(self):
im = hopper()
self.rotate(im, im.mode, 45, center=(0, 0))
self.rotate(im, im.mode, 45, translate=(im.size[0]/2, 0))
self.rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0]/2, 0))
if __name__ == '__main__':