-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added regular_polygon draw method #4846
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. |
docs/reference/ImageDraw.rst
Outdated
@@ -254,6 +254,22 @@ Methods | |||
:param outline: Color to use for the outline. | |||
: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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, |
docs/reference/ImageDraw.rst
Outdated
: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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
: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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/PIL/ImageDraw.py
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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") |
src/PIL/ImageDraw.py
Outdated
|
||
# 3. Variable Declarations | ||
vertices = [] | ||
*centroid, polygon_radius = b_circle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*centroid, polygon_radius = b_circle |
src/PIL/ImageDraw.py
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one small thing.
: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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_polygon
method toImageDraw
Usage
Draw a red hexagon, rotated by 90 degrees, inscribed by the following bounding box = [(50,50), (150, 150)]
- Add acompute_polygon_coordinates
method toImageDraw
.#### What doescompute_polygon_coordinates
do?compute_polygon_coordinates
creates 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)