Python `aggdraw` module bug (?): visible alpha channel - python

Imagine simple script:
from PIL import Image
from aggdraw import Draw, Brush
im = Image.new("RGBA", (600, 600), (0, 0, 0, 0))
draw = Draw(im)
brush = Brush("yellow")
draw.polygon(
(
50, 50,
550, 60,
550, 550,
60, 550,
),
None, brush
)
draw.flush()
im.save("2.png")
And the result:
(Sorry for big size but that is more clearly)
And the problem:
Can you see non-yellow and non-white edges? This is the alpha-channel or something.
When I trying to do this only with PIL's Draw object - it looks clearly and good but not anti-aliased.
But with aggdraw's Draw object it looks anti-aliased but with that ugly dirty edges.
I need exactly polygons with non-standard side angles. Simple box is not what I want.
Please, help me with some good optimistic answer how to solve this problem.

It's because your background is black, but transparent. If you set your image background to white, you don't get the visible edges. Either transparent or solid white works in my simple tests.
Try these values:
transBlack = (0, 0, 0, 0) # shows your example with visible edges
solidBlack = (0, 0, 0, 255) # shows shape on a black background
transWhite = (255, 255, 255, 0)
solidWhite = (255, 255, 255, 255)
im = Image.new("RGBA", (600, 600), solidWhite)

Related

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))

How to get the bounding box of regions excluding specific RGB values

I am currently using PIL.Image.Image.getbbox() to get the bounding box for the non-zero (non-transparent) regions of an image.
What if I have an image that has a background of a specific color? How can I get the bounding box of the image then? Same idea as getbbox() but instead of non-zero, I specify the RGB values.
I'm afraid, my comment didn't properly express, what I wanted to suggest. So, here's a full answer:
Make a copy of your image (that one, that has a specific background color).
On that copy, replace the specific background color with black.
Call getbbox on that copy.
Maybe, the following code and examples make things more clear:
import numpy as np
from PIL import Image, ImageDraw
# Black background
img = Image.new('RGB', (400, 400), (0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rectangle((40, 40, 160, 160), (255, 0, 0))
draw.rectangle((280, 260, 380, 330), (0, 255, 0))
img.save('black_bg.png')
print(img.getbbox(), '\n')
# Specific color background
bg_color = (255, 255, 0)
img = Image.new('RGB', (400, 400), bg_color)
draw = ImageDraw.Draw(img)
draw.rectangle((40, 40, 160, 160), (255, 0, 0))
draw.rectangle((280, 260, 380, 330), (0, 255, 0))
img.save('color_bg.png')
print(img.getbbox(), '\n')
# Suggested color replacing (on image copy) - Pillow only, slow
img_copy = img.copy()
for y in range(img_copy.size[1]):
for x in range(img_copy.size[0]):
if img_copy.getpixel((x, y)) == bg_color:
img_copy.putpixel((x, y), (0, 0, 0))
print(img_copy.getbbox(), '\n')
# Suggested color replacing (on image copy) - NumPy, fast
img_copy = np.array(img)
img_copy[np.all(img_copy == bg_color, axis=2), :] = 0
print(Image.fromarray(img_copy).getbbox())
There's one image with black background:
The corresponding output of getbbox is:
(40, 40, 381, 331)
Also, there's an image with a specific background color (yellow):
Calling getbbox on that image – obviously – returns:
(0, 0, 400, 400)
By simply replacing yellow with black in some copy of the second image, we again get the correct results from getbbox (both proposed methods):
(40, 40, 381, 331)
(40, 40, 381, 331)
Since iterating single pixels in Pillow is kinda slow, you could also use NumPy's vectorization abilities to speed up that task.
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.8.5
NumPy: 1.19.5
Pillow: 8.1.0
----------------------------------------

How to create overlapping semitransparent shapes on semitransparent background with PIL

I'm trying to create image with semitransparent shapes drawn on transparent background. For some reason instead of staying transparent, the shapes are completely covering those beneath them. My code:
from PIL import Image, ImageDraw
img = Image.new("RGBA", (256, 256), (255,0,0,127))
drawing = ImageDraw.Draw(img, "RGBA")
drawing.ellipse((127-79, 127 - 63, 127 + 47, 127 + 63), fill=(0, 255, 0, 63), outline=(0, 255, 0, 255))
drawing.ellipse((127-47, 127 - 63, 127 + 79, 127 + 63), fill=(0, 0, 255, 63), outline=(0, 0, 255, 255))
img.save("foo.png", "png")
I would expect the result to look something like (except for background not being transparent):but it looks like:
When I try to save it as GIF with img.save("foo.gif", "gif"), result is even worse. Circles are solid, no difference between outline and fill.
As I mentioned in a comment, ImageDraw.Draw doesn't do blending—whatever is drawn replaces whatever pixels that were there previously. To get the effect you want requires drawing things in a two-step process. The ellipse must first be drawn on a blank transparent background, and then that must be alpha-composited with current image (bg_img) to preserve transparency.
In the code below this has been implementing in re-usable function:
from PIL import Image, ImageDraw
def draw_transp_ellipse(img, xy, **kwargs):
""" Draws an ellipse inside the given bounding box onto given image.
Supports transparent colors
"""
transp = Image.new('RGBA', img.size, (0,0,0,0)) # Temp drawing image.
draw = ImageDraw.Draw(transp, "RGBA")
draw.ellipse(xy, **kwargs)
# Alpha composite two images together and replace first with result.
img.paste(Image.alpha_composite(img, transp))
bg_img = Image.new("RGBA", (256, 256), (255, 0, 0, 127)) # Semitransparent background.
draw_transp_ellipse(bg_img, (127-79, 127-63, 127+47, 127+63),
fill=(0, 255, 0, 63), outline=(0, 255, 0, 255))
draw_transp_ellipse(bg_img, (127-47, 127-63, 127+79, 127+63),
fill=(0, 0, 255, 63), outline=(0, 0, 255, 255))
bg_img.save("foo.png")
This is the image it created viewed in my image file editor app which renders semi-transparent portions of images with a checker-board pattern. As you can see the opaque outlines are the only part that isn't.
All 4th dimensions-transparency must be half. Also maybe problem is with drawing. Make mask for each ellipse and then sum up images. Last option- check pil function for bending images. That will propably be most certain and easiest solution.
The function for blending is:
PIL.Image. blend (im1, im2, alpha)

Stacking different types of transparency in pygame

I have a custom GUI module which uses objects in a tree to manage the interface, and blits them in the right order above one another.
Now amongst my objects, I have some which are just surfaces with per-pixel transparency, and the others use a colorkey.
My problem is that when blitting a surface with per-pixel transparency on another filled with a colorkey, the first surface changes the color of some of the pixels in the second, which means they are no longer transparent. How could I mix those without having to get rid of the per-pixel transparency ?
You could just convert your Surfaces that use a colorkey to use per-pixel transparency before blitting another per-pixel transparency Surface on it using convert_alpha.
Example:
COLORKEY=(127, 127, 0)
TRANSPARENCY=(0, 0, 0, 0)
import pygame
pygame.init()
screen = pygame.display.set_mode((200, 200))
for x in xrange(0, 200, 20):
pygame.draw.line(screen, (255, 255, 255), (x, 0),(x, 480))
# create red circle using a colorkey
red_circle = pygame.Surface((200, 200))
red_circle.fill(COLORKEY)
red_circle.set_colorkey(COLORKEY)
pygame.draw.circle(red_circle, (255, 0, 0), (100, 100), 25)
#create a green circle using alpha channel (per-pixel transparency)
green_circle = pygame.Surface((100, 100)).convert_alpha()
green_circle.fill(TRANSPARENCY)
pygame.draw.circle(green_circle, (0, 255, 0, 127), (50, 50), 25)
# convert colorkey surface to alpha channel surface before blitting
red_circle = red_circle.convert_alpha()
red_circle.blit(green_circle, (75, 75))
screen.blit(red_circle, (0, 0))
pygame.display.flip()
Result:

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