Repository navigation
Added regular_polygon draw method - #4846
Conversation
|
Hmm. What is your use case for this? Our drawing methods of arc, chord, ellipse, etc, accept a bounding box as an argument, and work within that. So I'm wondering if it would be helpful here to have a |
|
It seems to me that either this or @radarhere's suggestion would be a lot more useful if they allowed an arbitrary rotation. It looks like this PR always returns a polygon with a horizontal edge at the bottom. |
I needed to generate regular polygons for a machine learning project. Although drawing regular polygons isn't as common as drawing squares or circles, @radarhere @nulano, I like both of your suggestions. I'll include these changes in the PR. |
|
Here's the updated method # Draw a red hexagon, rotated by 90 degrees, inscribed by the following bounding box = [(50,50), (150, 150)]
img = Image.new('RGBA', size=(200, 200), color=(255, 0, 0, 0))
draw = ImageDraw.Draw(img)
bbox = [(50,50), (150, 150)
n_sides=6
draw.regular_polygon(bbox, n_sides, rotation=90, fill='red') |
|
Note that other functions accept both a Have you considered what happens if the bbox is not a square? I can't see any tests concerning that. I think the current version draws a polygon bounded by the height of the bbox, possibly spilling over horizontally. I'm wondering if it would be useful to first compute a polygon bounded by the height and then stretch the width to match the bbox (similar to shapes in MS Paint). It would no longer be a regular polygon (not sure how this would affect the function name), but perhaps it would be useful to draw axis-stretched shapes. I'm not sure how common this would be, but in such a rare situation, I imagine it could be quite useful. |
| :param fill: Color to use for the fill. | ||
|
|
||
|
|
||
| .. py:method:: ImageDraw.regular_polygon(*, b_box, nb_sides, rotation=0, fill=None, outline=None) |
There was a problem hiding this comment.
Nits:
Let's call it bbox throughout, that's what the rest of the codebase uses.
Also, n_things is used quite a lot, and there's very little nb_things:
| .. py:method:: ImageDraw.regular_polygon(*, b_box, nb_sides, rotation=0, fill=None, outline=None) | |
| .. py:method:: ImageDraw.regular_polygon(*, bbox, n_sides, rotation=0, fill=None, outline=None) |
There was a problem hiding this comment.
I would go with xy here, as while most of the code base uses bbox, the ImageDraw functions use xy: https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.rectangle
| .. py:method:: ImageDraw.regular_polygon(*, b_box, nb_sides, rotation=0, fill=None, outline=None) | |
| .. py:method:: ImageDraw.regular_polygon(*, xy, n_sides, rotation=0, fill=None, outline=None) |
Summary of changes
- Allow positional args in `regular_polygon` method
- Allow multiple bounding box formats
- (e.g. bbox = [(x0, y0), (x1, y1)] or [x0, y0, x1, y1])
- Check if bounding box is square
- Update var names
- b_box => bbox
- nb_sides => n_sides
|
@nulano , thanks for your review.
Both formats are now accepted.
I've added a check to ensure the bounding box is square. |
|
@radarhere , @nulano let me know if there's anything I can do to help with the review. |
|
Minor thing, maybe make the filenames a bit shorter? For example: -Tests/images/imagedraw_regular_polygon__octagon_with_0_degree_rotation.png
+Tests/images/imagedraw_regular_octagon.png
-Tests/images/imagedraw_regular_polygon__square_with_0_degree_rotation.png
+Tests/images/imagedraw_square.png
-Tests/images/imagedraw_regular_polygon__square_with_45_degree_rotation.png
+Tests/images/imagedraw_square_rotate_45.png |
hugovk
left a comment
There was a problem hiding this comment.
Some consistency, nits and simplification!
|
Hmm. The current version draws a polygon inscribed into a circle inscribed into the bounding box. While this is fine for e.g. hexagons, it is not great for squares or triangles: I implemented my stretching idea, but that is instead not great for hexagons, where it would require users to compute the height for a regular hexagon: Maybe it would be best to pass the center point in For reference, I used the following code: def test(*args, **kwargs):
im = Image.new("RGB", (100,100), "yellow")
d = ImageDraw.Draw(im)
d.regular_polygon(*args, **kwargs)
im.show()
test((1,1,98,98), 3, 0, "black")
test((1,1,98,98), 4, 0, "black")
test((1,1,98,98), 6, 0, "black") |
Summary of changes: - `ImageDraw.regular_polygon` now accepts a bounding circle which inscribes the polygon. A bounding circle is defined by a center point (x0, y0) and a radius. A bounding box is no longer accepted. - All keyword args have been replaced with positional args. Misc - Test image file renaming, minor variable name changes
|
@nulano, thanks for your review.
Agreed. A bounding circle is a good compromise + it significantly simplifies the code. I've included this change in the latest version. |
|
@hugovk, thanks the review. I've implemented your suggestions. Small detail, but what are your thoughts on |
|
Thanks for the updates!
Well, |
| :param b_circle: A bounding circle which inscribes the polygon | ||
| (e.g. b_circle=[50, 50, 25]). | ||
| :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). |
There was a problem hiding this comment.
| :param b_circle: A bounding circle which inscribes the polygon | |
| (e.g. b_circle=[50, 50, 25]). | |
| :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 b_circle: A bounding circle which inscribes the polygon | |
| (e.g. ``b_circle=[50, 50, 25]``). | |
| :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). |
nulano
left a comment
There was a problem hiding this comment.
I think bounding_circle is slightly better than b_circle, but both are much better than bcircle. Either way, it should accept a point specified as a tuple.
| if not len(b_circle) == 3: | ||
| raise ValueError( | ||
| "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])" | ||
| ) | ||
|
|
||
| if not all(isinstance(i, (int, float)) for i in b_circle): | ||
| raise ValueError("b_circle should only contain numeric data") |
There was a problem hiding this comment.
| if not len(b_circle) == 3: | |
| raise ValueError( | |
| "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])" | |
| ) | |
| if not all(isinstance(i, (int, float)) for i in b_circle): | |
| raise ValueError("b_circle should only contain numeric data") | |
| if len(b_circle) == 3: | |
| *centroid, polygon_radius = b_circle | |
| elif len(b_circle) == 2: | |
| centroid, polygon_radius = b_circle | |
| else: | |
| raise ValueError( | |
| "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])" | |
| ) | |
| if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): | |
| raise ValueError("b_circle should only contain numeric data") |
|
|
||
| # 3. Variable Declarations | ||
| vertices = [] | ||
| *centroid, polygon_radius = b_circle |
There was a problem hiding this comment.
| *centroid, polygon_radius = b_circle |
| Generate a list of vertices for a 2D regular polygon. | ||
|
|
||
| :param b_circle: A bounding circle which inscribes the polygon | ||
| (e.g. b_circle = [x0, y0, r]) |
There was a problem hiding this comment.
| (e.g. b_circle = [x0, y0, r]) | |
| (e.g. ``b_circle = [x0, y0, r]`` or ``b_circle = [(x0, y0), r]``) |
|
Actually, on tuples vs. lists: :param bounding_circle: A bounding circle which inscribes the polygon
(e.g. ``bounding_circle=[x0, y0, r]`` or ``bounding_circle=[(x0, y0), r]``)Generally, a tuple is better for a set number of things, and lists for a mutable number of things. (See also #3738 (comment).) Here, we're taking three and only three values, so should we use a 3-tuple? We generally do the same elsewhere. For example, :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
So there's a mutable list of immutable 2-tuple Whether we actively disallow a list is another question, but I think at least show tuples in the docs. (I think What do you think? |
|
For :param xy: Two points to define the bounding box. Sequence of either
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
is just outside the drawn rectangle.Other ImageDraw functions are similar. This PR adds an ImageDraw function, so I think it should have the same semantics, i.e. accept a sequence of a point and a radius (or a flat sequence of the coords and a radius. What do you think about the following suggestion (i.e. swapping the two examples)? :param bounding_circle: A point and a radius to define the bounding circle.
Sequence of either ``[(x, y), r]`` or ``[x, y, r]``. The polygon is drawn
inscribed in this circle. |
So can be:
These lists cannot be meaningfully extended:
|
|
Rectangle can only accept two points: Lines 3217 to 3221 in 8deaebd But I think I see what you mean, a circle is never a subsequence of another (longer) list. So perhaps go with the following and silently accept a list as well? :param bounding_circle: A point and a radius to define the bounding circle.
Tuple of either ``(x, y, r)`` or ``((x, y), r)``. The polygon is drawn
inscribed in this circle. |
I like this approach. A tuple is a better fit for the |
|
Thanks! Though I think |
| :param b_circle: A bounding circle which inscribes the polygon | ||
| (e.g. b_circle=[50, 50, 25]). | ||
| :param bounding_circle: A bounding circle is defined by a point and radius. | ||
| (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). |
There was a problem hiding this comment.
| (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). | |
| (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). | |
| The polygon is inscribed in this circle. |
Docs for ImageDraw.regular_polygon should probably also mention this, not just the _compute_regular_polygon_vertices function (which is not shown in the docs and can only be accessed with the __docs__ attribute).
Summary of changes
- Rename `b_circle` and `bounding_circle`
-`bounding_circle` now accepts both formats below:
- (x0, y0, r)
- ((x0, y0), r)
hugovk
left a comment
There was a problem hiding this comment.
Still not keen on ((x, y), r)), but I'll defer to others on that.
Otherwise 👍
|
To me a circle is I can't find other examples online that take a circle parameter, but there doesn't seem to be a strong preference for either:
So it is not clear to me that one is better than the other. If only for point in points:
draw.regular_polygon((*point, 5), ...)or pollute the locals with for x, y in points:
draw.regular_polygon((x, y, 5), ...)Therefore I think it would be nice to support for point in points:
draw.regular_polygon((point, 5), ...)since it doesn't seem to add too much complexity to the function as far as I can tell. |
|
Okay, there's been no comments in a while, so if there's no further suggestions by tomorrow will merge! |
|
@comhar Thank you for your patience and for all your updates, and thank you @nulano for the reviews! Merging now! Please could you also add release notes in https://github.com/python-pillow/Pillow/blob/master/docs/releasenotes/8.0.0.rst? This can be another PR :) |






Changes proposed in this pull request:
regular_polygonmethod toImageDrawUsage
Draw a red hexagon, rotated by 90 degrees, inscribed by the following bounding box = [(50,50), (150, 150)]
- Add acompute_polygon_coordinatesmethod toImageDraw.#### What doescompute_polygon_coordinatesdo?compute_polygon_coordinatescreates a list of coordinates for a polygon of n sides with a given length.### UsageDraw a regular hexagon with side of length10, starting at the position(50, 50)```python# 1. Compute Co-ordinatescoordinates = compute_polygon_coordinates(nb_sides=6,side_length=10,starting_point=(50,50))# 2. Draw hexagonimg = Image.new('RGBA', size=(256, 256), color=(255, 0, 0, 0))draw = ImageDraw.Draw(img)draw.polygon(coordinates, fill=colour)