mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-01 08:27:30 +03:00 
			
		
		
		
	Merge pull request #4846 from comhar/features/compute_polygon_coordinates
This commit is contained in:
		
						commit
						3dba4ee10a
					
				
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_regular_octagon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/imagedraw_regular_octagon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 491 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_square.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/imagedraw_square.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 327 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_square_rotate_45.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/imagedraw_square_rotate_45.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 587 B | 
|  | @ -1093,3 +1093,95 @@ def test_same_color_outline(): | |||
|                 # Assert | ||||
|                 expected = f"Tests/images/imagedraw_outline_{operation}_{mode}.png" | ||||
|                 assert_image_similar_tofile(im, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "n_sides, rotation, polygon_name", | ||||
|     [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], | ||||
| ) | ||||
| def test_draw_regular_polygon(n_sides, rotation, polygon_name): | ||||
|     im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) | ||||
|     filename_base = f"Tests/images/imagedraw_{polygon_name}" | ||||
|     filename = ( | ||||
|         f"{filename_base}.png" | ||||
|         if rotation == 0 | ||||
|         else f"{filename_base}_rotate_{rotation}.png" | ||||
|     ) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     bounding_circle = ((W // 2, H // 2), 25) | ||||
|     draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") | ||||
|     assert_image_equal(im, Image.open(filename)) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "n_sides, expected_vertices", | ||||
|     [ | ||||
|         (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), | ||||
|         (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), | ||||
|         ( | ||||
|             5, | ||||
|             [ | ||||
|                 (35.31, 70.23), | ||||
|                 (64.69, 70.23), | ||||
|                 (73.78, 42.27), | ||||
|                 (50.0, 25.0), | ||||
|                 (26.22, 42.27), | ||||
|             ], | ||||
|         ), | ||||
|         ( | ||||
|             6, | ||||
|             [ | ||||
|                 (37.5, 71.65), | ||||
|                 (62.5, 71.65), | ||||
|                 (75.0, 50.0), | ||||
|                 (62.5, 28.35), | ||||
|                 (37.5, 28.35), | ||||
|                 (25.0, 50.0), | ||||
|             ], | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_compute_regular_polygon_vertices(n_sides, expected_vertices): | ||||
|     bounding_circle = (W // 2, H // 2, 25) | ||||
|     vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) | ||||
|     assert vertices == expected_vertices | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "n_sides, bounding_circle, rotation, expected_error, error_message", | ||||
|     [ | ||||
|         (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), | ||||
|         (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), | ||||
|         (3, 50, 0, TypeError, "bounding_circle should be a tuple"), | ||||
|         ( | ||||
|             3, | ||||
|             (50, 50, 100, 100), | ||||
|             0, | ||||
|             ValueError, | ||||
|             "bounding_circle should contain 2D coordinates " | ||||
|             "and a radius (e.g. (x, y, r) or ((x, y), r) )", | ||||
|         ), | ||||
|         ( | ||||
|             3, | ||||
|             (50, 50, "25"), | ||||
|             0, | ||||
|             ValueError, | ||||
|             "bounding_circle should only contain numeric data", | ||||
|         ), | ||||
|         ( | ||||
|             3, | ||||
|             ((50, 50, 50), 25), | ||||
|             0, | ||||
|             ValueError, | ||||
|             "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", | ||||
|         ), | ||||
|         (3, (50, 50, 0), 0, ValueError, "bounding_circle radius should be > 0",), | ||||
|         (3, (50, 50, 25), "0", ValueError, "rotation should be an int or float",), | ||||
|     ], | ||||
| ) | ||||
| def test_compute_regular_polygon_vertices_input_error_handling( | ||||
|     n_sides, bounding_circle, rotation, expected_error, error_message | ||||
| ): | ||||
|     with pytest.raises(expected_error) as e: | ||||
|         ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) | ||||
|     assert str(e.value) == error_message | ||||
|  |  | |||
|  | @ -254,6 +254,24 @@ Methods | |||
|     :param outline: Color to use for the outline. | ||||
|     :param fill: Color to use for the fill. | ||||
| 
 | ||||
| 
 | ||||
| .. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) | ||||
| 
 | ||||
|     Draws a regular polygon inscribed in ``bounding_circle``, | ||||
|     with ``n_sides``, and rotation of ``rotation`` degrees. | ||||
| 
 | ||||
|     :param bounding_circle: The bounding circle is a tuple defined | ||||
|         by a point and radius. | ||||
|         (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). | ||||
|         The polygon is inscribed in this circle. | ||||
|     :param n_sides: Number of sides | ||||
|         (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). | ||||
|     :param rotation: Apply an arbitrary rotation to the polygon | ||||
|         (e.g. ``rotation=90``, applies a 90 degree rotation). | ||||
|     :param fill: Color to use for the fill. | ||||
|     :param outline: Color to use for the outline. | ||||
| 
 | ||||
| 
 | ||||
| .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) | ||||
| 
 | ||||
|     Draws a rectangle. | ||||
|  |  | |||
|  | @ -242,6 +242,13 @@ class ImageDraw: | |||
|         if ink is not None and ink != fill: | ||||
|             self.draw.draw_polygon(xy, ink, 0) | ||||
| 
 | ||||
|     def regular_polygon( | ||||
|         self, bounding_circle, n_sides, rotation=0, fill=None, outline=None | ||||
|     ): | ||||
|         """Draw a regular polygon.""" | ||||
|         xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) | ||||
|         self.polygon(xy, fill, outline) | ||||
| 
 | ||||
|     def rectangle(self, xy, fill=None, outline=None, width=1): | ||||
|         """Draw a rectangle.""" | ||||
|         ink, fill = self._getink(outline, fill) | ||||
|  | @ -555,6 +562,123 @@ def floodfill(image, xy, value, border=None, thresh=0): | |||
|         edge = new_edge | ||||
| 
 | ||||
| 
 | ||||
| def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): | ||||
|     """ | ||||
|     Generate a list of vertices for a 2D regular polygon. | ||||
| 
 | ||||
|     :param bounding_circle: The bounding circle is a tuple defined | ||||
|         by a point and radius. The polygon is inscribed in this circle. | ||||
|         (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) | ||||
|     :param n_sides: Number of sides | ||||
|         (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) | ||||
|     :param rotation: Apply an arbitrary rotation to the polygon | ||||
|         (e.g. ``rotation=90``, applies a 90 degree rotation) | ||||
|     :return: List of regular polygon vertices | ||||
|         (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) | ||||
| 
 | ||||
|     How are the vertices computed? | ||||
|     1. Compute the following variables | ||||
|         - theta: Angle between the apothem & the nearest polygon vertex | ||||
|         - side_length: Length of each polygon edge | ||||
|         - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) | ||||
|         - polygon_radius: Polygon radius (last element of bounding_circle) | ||||
|         - angles: Location of each polygon vertex in polar grid | ||||
|             (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) | ||||
| 
 | ||||
|     2. For each angle in angles, get the polygon vertex at that angle | ||||
|         The vertex is computed using the equation below. | ||||
|             X= xcos(φ) + ysin(φ) | ||||
|             Y= −xsin(φ) + ycos(φ) | ||||
| 
 | ||||
|         Note: | ||||
|             φ = angle in degrees | ||||
|             x = 0 | ||||
|             y = polygon_radius | ||||
| 
 | ||||
|         The formula above assumes rotation around the origin. | ||||
|         In our case, we are rotating around the centroid. | ||||
|         To account for this, we use the formula below | ||||
|             X = xcos(φ) + ysin(φ) + centroid_x | ||||
|             Y = −xsin(φ) + ycos(φ) + centroid_y | ||||
|     """ | ||||
|     # 1. Error Handling | ||||
|     # 1.1 Check `n_sides` has an appropriate value | ||||
|     if not isinstance(n_sides, int): | ||||
|         raise TypeError("n_sides should be an int") | ||||
|     if n_sides < 3: | ||||
|         raise ValueError("n_sides should be an int > 2") | ||||
| 
 | ||||
|     # 1.2 Check `bounding_circle` has an appropriate value | ||||
|     if not isinstance(bounding_circle, (list, tuple)): | ||||
|         raise TypeError("bounding_circle should be a tuple") | ||||
| 
 | ||||
|     if len(bounding_circle) == 3: | ||||
|         *centroid, polygon_radius = bounding_circle | ||||
|     elif len(bounding_circle) == 2: | ||||
|         centroid, polygon_radius = bounding_circle | ||||
|     else: | ||||
|         raise ValueError( | ||||
|             "bounding_circle should contain 2D coordinates " | ||||
|             "and a radius (e.g. (x, y, r) or ((x, y), r) )" | ||||
|         ) | ||||
| 
 | ||||
|     if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): | ||||
|         raise ValueError("bounding_circle should only contain numeric data") | ||||
| 
 | ||||
|     if not len(centroid) == 2: | ||||
|         raise ValueError( | ||||
|             "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" | ||||
|         ) | ||||
| 
 | ||||
|     if polygon_radius <= 0: | ||||
|         raise ValueError("bounding_circle radius should be > 0") | ||||
| 
 | ||||
|     # 1.3 Check `rotation` has an appropriate value | ||||
|     if not isinstance(rotation, (int, float)): | ||||
|         raise ValueError("rotation should be an int or float") | ||||
| 
 | ||||
|     # 2. Define Helper Functions | ||||
|     def _apply_rotation(point, degrees, centroid): | ||||
|         return ( | ||||
|             round( | ||||
|                 point[0] * math.cos(math.radians(360 - degrees)) | ||||
|                 - point[1] * math.sin(math.radians(360 - degrees)) | ||||
|                 + centroid[0], | ||||
|                 2, | ||||
|             ), | ||||
|             round( | ||||
|                 point[1] * math.cos(math.radians(360 - degrees)) | ||||
|                 + point[0] * math.sin(math.radians(360 - degrees)) | ||||
|                 + centroid[1], | ||||
|                 2, | ||||
|             ), | ||||
|         ) | ||||
| 
 | ||||
|     def _compute_polygon_vertex(centroid, polygon_radius, angle): | ||||
|         start_point = [polygon_radius, 0] | ||||
|         return _apply_rotation(start_point, angle, centroid) | ||||
| 
 | ||||
|     def _get_angles(n_sides, rotation): | ||||
|         angles = [] | ||||
|         degrees = 360 / n_sides | ||||
|         # Start with the bottom left polygon vertex | ||||
|         current_angle = (270 - 0.5 * degrees) + rotation | ||||
|         for _ in range(0, n_sides): | ||||
|             angles.append(current_angle) | ||||
|             current_angle += degrees | ||||
|             if current_angle > 360: | ||||
|                 current_angle -= 360 | ||||
|         return angles | ||||
| 
 | ||||
|     # 3. Variable Declarations | ||||
|     angles = _get_angles(n_sides, rotation) | ||||
| 
 | ||||
|     # 4. Compute Vertices | ||||
|     return [ | ||||
|         _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| def _color_diff(color1, color2): | ||||
|     """ | ||||
|     Uses 1-norm distance to calculate difference between two values. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user