mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-30 23:47:27 +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