How to draw a transparent ellipse in Python with Pillow without outline - python

I am trying to draw multiple transparent ellipses with Pillow, and I want to draw them without outlines. I can't seem to make it work without outlines.
Here is some test code:
from PIL import Image, ImageDraw
w,h = 100,100
img = Image.new('RGB', (w,h),(255,255,255))
drw = ImageDraw.Draw(img,"RGBA")
drw.polygon([(50, 0), (100, 100), (0, 100)], (255, 0, 0, 125))
drw.polygon([(50,100), (100, 0), (0, 0)], (0, 255, 0, 125))
drw.ellipse([(40, 40), (w - 10, h - 10)], fill=(0,0,255,125), outline=None)
img.save('out.png', 'PNG')
(from here, with some modifications)
Output
Only the ellipse gets an outline. Why? How can I avoid this?

After playing around with some codes, looking at the documentation and some source codes, I'm quite sure, that most likely there's some issue with the functions like arc, chord, ellipse, that all share the same code under the hood.
I created the following example:
from matplotlib import pyplot as plt
from PIL import Image, ImageDraw
def example(outline_alpha=None, width=None):
if outline_alpha is None:
outline = None
else:
outline = (255, 255, 0, outline_alpha)
if width is None:
width = 0
img = Image.new('RGB', (100, 100), (255, 255, 255))
drw = ImageDraw.Draw(img, 'RGBA')
drw.line([(0, 40), (100, 40)], (0, 0, 0, 255))
drw.line([(50, 100), (100, 0)], (0, 0, 0, 255))
drw.polygon([(50, 100), (100, 0), (0, 0)], (0, 255, 0, 128), outline)
drw.ellipse([(40, 40), (90, 90)], (0, 0, 255, 128), outline, width)
return img
plt.figure(1, figsize=(15, 10))
plt.subplot(2, 3, 1), plt.imshow(example()), plt.title('No outlines specified, width = 0')
plt.subplot(2, 3, 2), plt.imshow(example(255)), plt.title('Opaque outlines specified, width = 0')
plt.subplot(2, 3, 3), plt.imshow(example(128)), plt.title('Semi-transparent outlines specified, width = 0')
plt.subplot(2, 3, 4), plt.imshow(example(None, 5)), plt.title('No outlines specified, width = 5')
plt.subplot(2, 3, 5), plt.imshow(example(255, 5)), plt.title('Opaque outlines specified, width = 5')
plt.subplot(2, 3, 6), plt.imshow(example(20, 5)), plt.title('Semi-transparent outlines specified, width = 5')
plt.tight_layout()
plt.show()
The output is the following:
Looking at the polygon, if no outline is specified (top left image), we see that the black line is visible, which is one of the polygon's borders. Specifying an opaque outline (top center image), the black line's no longer visible. Setting a semi-transparent outline (top right image) reveals, that the outline is identical to the polygon's border.
Now, the same for the ellipse: If no outline is set (top left), an outline is shown nevertheless, most likely the same color as used for the fill parameter, but without incorporating an alpha value. Setting an opaque outline (top center) "overwrites" the unexpected existent outline, but when setting a semi-transparent outline, we see that the unexpected outline is still there.
This effect becomes even more obvious, when setting width > 1 in ellipse, see the bottom row. The unexpected outline still seems to have width = 1, whereas the explicitly set outline has width = 5.
Again, I'm quite sure, that this behavior isn't intended – and I will open an issue in their GitHub issue tracker. EDIT: I just opened this issue. ANOTHER EDIT: It's fixed.
Hope that helps – somehow...

Related

To cut a piece from a circle, I have to do the math manually first. Is there a smarter way to do the job using Python along with opencv?

I'm trying to cut a piece from a circle using Python along with opencv, here is the code
firstly, I constructed the circle
layer1 = np.zeros((48, 48, 4))
cv2.circle(layer1, (24, 24), 23, (0, 0, 0, 255), -1)
res = layer1[:]
and I got
and then, I drew a smaller square on it
start_point = (24, 0); end_point = (48, 24); color = (255, 0, 0)
cv2.rectangle(res, start_point, end_point, color, -1)
which gives
similarly, I drew a triangle on the circle
pt1 = (24, 0); pt2 = (48, 0); pt3 = (24, 24)
triangle_cnt = np.array( [pt1, pt2, pt3] )
cv2.drawContours(res, [triangle_cnt], 0, (255,0,0), -1)
which gives
I can go along this way to draw a smaller triangle, 1/16, 1/32 and so on.
I have to do the math manually to get the vertices.
Is there a smarter (more elegant) way to do the job?
import cv2
import numpy as np
# Colors (B, G, R)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
# Create new blank 300x150 white image
width, height = 800, 500
img = np.zeros((height, width, 3), np.uint8)
img[...] = BLACK
center = (width//2, height//2)
axes = (200, 200) # axes radius, keep equal to draw circle.
angle = 0 #clockwise first axis
startAngle = 0
endAngle = 90
color = WHITE
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=-1)
cv2.imshow('image', img)
cv2.waitKey(-1)
You can play with startAngle and endAngle to change the position of the white part.
Another option is to change the angle option (to -90 for example to rotate counter clockwise).
EDIT to show the different end angles add
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/2, (255, 0, 0), thickness=-1)
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/4, (0, 255, 0), thickness=-1)
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/8, (0, 0, 255), thickness=-1)

How to change a direction of lines in Pillow python?

I'm trying to make a grid, and I made a lines in one way but other way I it doesn't seem to work, I got some weird lines going in other directions. Any idea how to get it right?
from PIL import Image, ImageDraw
img = Image.new('RGB', (1000, 1000), (255, 255, 255))
draw = ImageDraw.Draw(img)
for y in range (-2000, 2000, 200):
draw.line(((y, 2000), (2000, y)), (0, 0, 0), 20)
img
Output produced by code sample
Thanks in advance!
you are trying to draw : y = -x, so:
from PIL import Image, ImageDraw
img = Image.new('RGB', (1000, 1000), (255, 255, 255))
draw = ImageDraw.Draw(img)
for x in range (-1000, 1000, 100):
draw.line(((x,-x),(x+img.size[0], -x+img.size[1])), (0, 0, 0), 20)
img.show()
I used img.size to dynamically choose the second point of the line , you can hardcode it to 2000 (x+2000,-x+2000) , If you want to keep it that way

How to lower transparency to line in Pillow?

How to lower opacity to line? I would like to lower opacity to one of line in example bellow.
from PIL import Image, ImageDraw
img = Image.new('RGB', (100, 100), (255, 255, 255))
draw = ImageDraw.Draw(img)
draw.line((100, 30, 0, 30), (0, 0, 0), 20)
draw.line((100, 70, 0, 70), (0, 0, 0), 20)
img.show()
I have seen in one example they created opacity like this...
TRANSPARENCY = .25 # Degree of transparency, 0-100%
OPACITY = int(255 * TRANSPARENCY)
But don't know how to apply to one of lines. Any ideas?
EDIT
I made some changes (based on answer of #Pedro Maia), it still doesn't work, just changes a color, it doesn't lower opacity to see background color.
from PIL import Image, ImageDraw
img = Image.new('RGBA', (500, 500), (255, 255, 255))
draw = ImageDraw.Draw(img)
TRANSPARENCY = .25 # Degree of transparency, 0-100%
draw.line((200, 0, 200, 600),(255, 0, 0), 60)
draw.line((500, 100, 0, 100), (0, 0, 0, int(255 * TRANSPARENCY)), 60)
draw.line((500, 400, 0, 400),(0, 0, 0), 60)
img
And I have to convert it to RGB to export it as 'jpg'
You would have to do something like this, which is similar to how the example code works, to do what (I think) you want to. I changed the code you added to your question in the EDIT slightly so it better demonstrates that lines of different amounts of transparency can be drawn.
from PIL import Image, ImageDraw
RED = (255, 0, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
# Calculate alpha given a 0-100% opacity value.
opacity = lambda transparency: (int(255 * (transparency/100.)),) # Returns a monuple.
def draw_transp_line(image, xy, color, width=1, joint=None):
""" Draw line with transparent color on the specified image. """
if len(color) < 4: # Missing alpha?
color += opacity(100) # Opaque since alpha wasn't specified.
# Make an overlay image the same size as the specified image, initialized to
# a fully transparent (0% opaque) version of the line color, then draw a
# semi-transparent line on it.
overlay = Image.new('RGBA', image.size, color[:3]+opacity(0))
draw = ImageDraw.Draw(overlay) # Create a context for drawing things on it.
draw.line(xy, color, width, joint)
# Alpha composite the overlay image onto the original.
image.alpha_composite(overlay)
# Create opaque white RGBA background image.
img = Image.new('RGBA', (500, 500), (255, 255, 255)+opacity(100))
draw_transp_line(img, ((200, 0), (200, 600)), RED+opacity(100), 60)
draw_transp_line(img, ((500, 100), (0, 100)), BLACK+opacity(25), 60)
draw_transp_line(img, ((150, 50), (600, 400)), BLACK+opacity(50), 60)
img = img.convert("RGB") # Remove alpha for saving in jpg format.
img.save('transparent_lines.jpg')
img.show()
JPG image created:
With draw.line you can pass as argument RGB or RGBA just pass the value of the transparency:
draw.line((100, 30, 0, 30), (0, 0, 0, int(255 * TRANSPARENCY)), 20)
Also when creating the image set it as RGBA:
img = Image.new('RGBA', (100, 100), (255, 255, 255))

Why does Pillow convert return colours outside the specified palette?

Using Pillow 5.4.1, Python 3.6.8
Given an image image.png with 9 distinct colours, and given a data palette with 5 distinct colours, one would expect that asking pillow to reduce the image to the described palette that the resulting image would contain colours from only that palette.
However, using the im.im.convert method returns an image with colours outside the specified palette; specifically they are always greyscale images (R==B==G values)
Sample Code, outputting the unique set of colours for the original image, palette, and converted image.
from PIL import Image
im = Image.open("image.png")
# create palette from raw data
# colours: Red, Green, Blue, Black, and White (5 total)
RGBBW = [(255,0,0), (0,255,0), (0,0,255), (0,0,0), (255,255,255)]
data = sum([list(x) for x in RGBBW], [])[:256]
pimg = Image.new("P",(16,16))
pimg.putpalette(data)
# Hack
im.convert("RGB")
cim_ = im.im.convert("P", 0, pimg.im)
cim = im._new(cim_).convert("RGB")
def colors(im):
cs = []
for x in range(im.width):
for y in range(im.height):
cs.append(im.getpixel((x,y)))
return list(set(cs))
print("Original: %s" % colors(im))
print("Palette: %s" % RGBBW)
print("Convert: %s" % colors(cim))
Input image: -> <- (3x3 pixel image, all pixels unique colours)
(Larger version, for visualisation only: )
Output:
Original: [(85, 85, 85, 255), (0, 0, 255, 255), (0, 0, 0, 255), (255, 0, 0, 255), (0, 255, 255, 255), (255, 255, 255, 255), (255, 255, 0, 255), (255, 0, 255, 255), (0, 255, 0, 255)]
Palette: [(255, 0, 0), (0, 255, 0), (0, 0, 255), (0, 0, 0), (255, 255, 255)]
Convert: [(252, 252, 252), (0, 0, 255), (255, 0, 0), (0, 0, 0), (170, 170, 170), (0, 255, 0), (84, 84, 84)]
(Note that the hack to prevent dither is a workaround, pending a fix I've contributed to master (yet to be cut into a new release))
The values [(170, 170, 170), (84, 84, 84), (252, 252, 252)] appear in the converted image, but were not specified in the original palette. They all happen to be greyscale.
I think there's something in src/libImaging/Palette.c that's effecting this, but I'm not sure if this is a bug of the code, or a 'feature' of libjpeg
Turns out this issue is both user error and an unexpected initialisation issue.
The initialisation issue: As pointed out in the comments, the palette for a new image is specifically initialised as greyscale.
If we replace the entire palette with our own, then we're fine. Except, I wasn't.
data = sum([list(x) for x in RGBBW], [])[:256]
This line is logically incorrect.
The palette expects a flattened list of up to 256 triples of RGB, that is, an array of max len 768. If the array is anything less than this, then the rest of the greyscale will still be in play.
The better way to re-initialise the palette is to ensure we repeat a value as to override the greyscale.
In this case:
data = (sum([list(x) for x in RGBBW], []) + (RGBBW[-1] * (256 - len(RGBBW))))[:256*3]
That is:
data = (
sum([list(x) for x in RGBBW], []) # flatten the nested array
+ (RGBBW[-1] * (256 - len(RGBBW))) # extend with the last value, to our required length, if needed
)[:256*3] # and trim back, if needed.
This will result in the palette always being 768 length.
Using the last value from our provided array is an arbitrary choice, as is only used as a valid padding value.

Merging background with transparent image in PIL

I have a png image as background and I want to add a transparent mesh to this background but this doesn't work as expected. The background image is converted to transparent on places where I apply transparent mesh.
I am doing:
from PIL import Image, ImageDraw
map_background = Image.open(MAP_BACKGROUND_FILE).convert('RGBA')
map_mesh = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(map_mesh)
# Create mesh using: draw.line([...], fill=(255, 255, 255, 50), width=1)
...
map_background.paste(map_mesh, (0, 0), map_mesh)
But the result is:
You can see a chessboard pattern if you look carefully (used in graphics programs as no background). Transparent lines makes the background layer transparent too in places where both layers met. But I only want the transparent line to be added on top of the background.
I can solve it with:
map_background.paste((255,255,255), (0, 0), map_mesh)
but as I use different colors for different lines, I would have to make for every color this process. If I had 100 colors, then I need 100 layers what is not very good solution.
What you are trying to do is to composite the grid onto the background, and for that you need to use Image.blend or Image.composite. Here's an example using the latter to composite red lines with random alpha values onto a white background:
import Image, ImageDraw, random
background = Image.new('RGB', (100, 100), (255, 255, 255))
foreground = Image.new('RGB', (100, 100), (255, 0, 0))
mask = Image.new('L', (100, 100), 0)
draw = ImageDraw.Draw(mask)
for i in range(5, 100, 10):
draw.line((i, 0, i, 100), fill=random.randrange(256))
draw.line((0, i, 100, i), fill=random.randrange(256))
result = Image.composite(background, foreground, mask)
From left to right:
[background] [mask]
[foreground]
[result]
(If you are happy to write the result back to the background image, then you can use one of the masked versions of Image.paste, as pointed out by Paulo Scardine in a deleted answer.)
I had trouble getting the above examples to work well. Instead, this worked for me:
import numpy as np
import Image
import ImageDraw
def add_craters(image, craterization=20.0, width=256, height=256):
foreground = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(foreground)
for c in range(0, craterization):
x = np.random.randint(10, width-10)
y = np.random.randint(10, height-10)
radius = np.random.randint(2, 10)
dark_color = (0, 0, 0, 128)
draw.ellipse((x-radius, y-radius, x+radius, y+radius), fill=dark_color)
image_new = Image.composite(foreground, image, foreground)
return image_new

Categories