mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-11 04:07:21 +03:00
Merge pull request #2328 from wiredfool/pr_2155
Add center and translate to Image.rotate
This commit is contained in:
commit
c84da36474
59
PIL/Image.py
59
PIL/Image.py
|
@ -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
BIN
Tests/images/hopper_45.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
|
@ -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__':
|
||||
|
|
Loading…
Reference in New Issue
Block a user