Python-Imaging-Library only paste imageparts with 255 alpha - python

I want to use paste of the python PIL library to paste a image to a black background.
I know I can use the image itself as a alpha mask, but I only want to have the parts of the image where the alpha value is 255.
How is this possible?
Here is my code so far:
import PIL
from PIL import Image
img = Image.open('in.png')
background = Image.new('RGBA', (825, 1125), (0, 0, 0, 255))
offset = (50, 50)
background.paste(img, offset, img) #image as alpha mask as third param
background.save('out.png')
I can't find anything in the official but bad documentation

If I understand your question correctly, then
this is a possible solution. It generates
a dedicated mask, which is used for the paste:
from PIL import Image
img = Image.open('in.png')
# Extract alpha band from img
mask = img.split()[-1]
width, height = mask.size
# Iterate through alpha pixels,
# perform desired conversion
pixels = mask.load()
for x in range(0, width):
for y in range(0, height):
if pixels[x,y] < 255:
pixels[x,y] = 0
# Paste image with converted alpha mask
background = Image.new('RGBA', (825, 1125), (0, 0, 0, 255))
background.paste(img, (50, 50), mask)
background.save('out.png')
As a note, the alpha channel of the background image is fairly useless.
If you don't need it later on, you could also load the background with:
background = Image.new('RGB', (825, 1125), (0, 0, 0))

Related

Replace specific pixels by rgb with white color

There is an image like that
I used a website to detect the rgb of the background and it is 42,44,54. Aiming at replacing the pixels with that rgb to white
Here's my try but I didn't get the expected output
import cv2
import numpy as np
# Load image
im = cv2.imread('Sample.png')
# Make all perfectly green pixels white
im[np.all(im == (42,44,54), axis=-1)] = (255, 255, 255)
# Save result
cv2.imwrite('Output.png',im)
I have searched again and found the following code (works to somewhat)
from PIL import Image
img = Image.open("Sample.png")
img = img.convert("RGB")
datas = img.getdata()
new_image_data = []
for item in datas:
# change all white (also shades of whites) pixels to yellow
if item[0] in list(range(42, 44)):
new_image_data.append((255, 255, 255))
else:
new_image_data.append(item)
# update image data
img.putdata(new_image_data)
# save new image
img.save("Output.png")
# show image in preview
img.show()
I need also to change any other rgb to be black except white pixels. Simply to get all colored characters into black after removing the background color
I am stil trying (waiting for experts to contribute and offer a better solution). The following is quite good but not so perfect till now
from PIL import Image
import numpy as np
img = Image.open("Sample.png")
width = img.size[0]
height = img.size[1]
for i in range(0,width):
for j in range(0,height):
data = img.getpixel((i,j))
if (data[0]>=36 and data[0]<=45) and (data[1]>=38 and data[1]<=45) and (data[2]>=46 and data[2]<=58):
img.putpixel((i,j),(255, 255, 255))
if (data[0]==187 and data[1]==187 and data[2]==191):
img.putpixel((i,j),(255, 255, 255))
img.save("Output.png")
I thought of converting the image to grayscale using Pillow
from PIL import Image
img = Image.open('Sample.png').convert('LA')
img.save('Grayscale.png')
The image became cleared but how to replace rgb pixels in such mode? I tried the same previous code and changed the rgb values but didn't work and there are errors as the mode is L
You can do both steps in one go:
from PIL import Image
def is_background(item, bg):
# Tweak the ranges if the result is still unsatisfying
return (item[0] in range(bg[0] - 20, bg[0] + 20)) or \
(item[1] in range(bg[1] - 20, bg[1] + 20)) or \
(item[2] in range(bg[2] - 20, bg[2] + 20))
img = Image.open("Sample.png")
img = img.convert("RGB")
datas = img.getdata()
bg = [42, 44, 54] # Background RGB color
new_image_data = []
for item in datas:
# change all background to white and keep all white
if is_background(item, bg) or item == (255, 255, 255):
new_image_data.append((255, 255, 255))
else:
# change non-background and non-white to black
new_image_data.append((0, 0, 0))
img.putdata(new_image_data)
img.save("Output.png")
img.show()
Here is the result.
Note that:
We need is_background because the background is not of the exact same color, there is a very slight variation
This method of detecting background is very basic and there are much more sophisticated ones.
The issue is OpenCV follows BGR format and your pixel value is RGB. Fix that as follows.
# Make all perfectly green pixels white
im[np.all(im == (54,44,42), axis=-1)] = (255, 255, 255)

Numpy create an empty alpha image

I wanted to create a blank alpha image to parse some data from py-opencv and save it on an transparent background png file.
I tried :
blank_image = np.zeros((H,W,4), np.uint8)
and
blank_image = np.full((H, W, 4) , (0, 0, 0, 0), np.uint8)
(H and W are Height and Width)
Both still render a black background instead of a transparent one.
how to get a blank alpha transparent image?
Thanks in advance :)
Edits:
as mentioned by Mark Setchell: you need to specify the alpha channel on other colors involved:
# size of the image
(H , W) = 1080, 1080
# Blank image with RGBA = (0, 0, 0, 0)
blank_image = np.full((H, W, 4), (0, 0, 0, 0), np.uint8)
# Green color with Alpha=255
RGBA_GREEN = (0, 255, 0, 255)
# Opencv element using the RGBA color
cv2.putText(blank_image, 'my opencv element', (20 , 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, RGBA_GREEN, 2)
cv2.imwrite('image_alpha.png', blank_image)
You need to make the alpha channel = 255 to see anything.
import numpy as np
H, W = 128, 256
blank_image = np.zeros((H,W,4), np.uint8)
# Make first 10 rows red and opaque
blank_image[:10] = [255,0,0,255]
# Make first 10 columns green and opaque
blank_image[:,:10] = [0,255,0,255]
You can also make your RGB image as you wish, then create an alpha layer entirely separately and add it afterwards:
# Make solid red image
RGB = np.full((H, W, 3) , (255, 0, 0), np.uint8)
# Make a gradient alpha channel, left-to-right, 0..255
alpha = np.repeat(np.arange(256,dtype=np.uint8)[np.newaxis,:], 128, axis=0)
# Apply alpha to RGB image to yield RGBA image
RGBA = np.dstack((RGB,alpha))

How to change the opacity of boxes (cv2.rectangle)?

I draw some rectangles in OpenCV and put text in them. My general approach looks like this:
# Draw rectangle p1(x,y) p2(x,y) Student name box
cv2.rectangle(frame, (500, 650), (800, 700), (42, 219, 151), cv2.FILLED )
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (510, 685), font, 1.0, (255, 255, 255), 1
Everything works so far. The only thing is, that the opacity in all boxes is at 100 %. My question is: How can I change the opacity?
The final result should look like this:
I would like to add a small optimization to the #HansHirse answer, Instead of creating the canvas for whole image, we can crop the rectangle first from the src image and then later swap it with the cv2.addWeighted result as:
import cv2
import numpy as np
img = cv2.imread("lena.png")
# First we crop the sub-rect from the image
x, y, w, h = 100, 100, 200, 100
sub_img = img[y:y+h, x:x+w]
white_rect = np.ones(sub_img.shape, dtype=np.uint8) * 255
res = cv2.addWeighted(sub_img, 0.5, white_rect, 0.5, 1.0)
# Putting the image back to its position
img[y:y+h, x:x+w] = res
EDIT: Since this answer seems to have some importance, I decided to edit it again, incorporating the proper blending from ZdaR's answer, which initially was an improvement to my original answer (check the timeline if interested). Also, I incorporated Jon's comments to include an example of a non-rectangular shape.
At least from my point of view, built-in functions like cv2.rectangle don't support opacity, even on BGRA images, see here. So, as I described in the linked answer, the only possibility to achieve, what you want, is to use the cv2.addWeighted function. You can simply set up a blank mask image, and draw all possible shapes on that. Doing so, you can also use that as an actual mask to limit the blending to that part only.
An example could be:
import cv2
import numpy as np
# Load image
img = cv2.imread('images/paddington.png')
# Initialize blank mask image of same dimensions for drawing the shapes
shapes = np.zeros_like(img, np.uint8)
# Draw shapes
cv2.rectangle(shapes, (5, 5), (100, 75), (255, 255, 255), cv2.FILLED)
cv2.circle(shapes, (300, 300), 75, (255, 255, 255), cv2.FILLED)
# Generate output by blending image with shapes image, using the shapes
# images also as mask to limit the blending to those parts
out = img.copy()
alpha = 0.5
mask = shapes.astype(bool)
out[mask] = cv2.addWeighted(img, alpha, shapes, 1 - alpha, 0)[mask]
# Visualization
cv2.imshow('Image', img)
cv2.imshow('Shapes', shapes)
cv2.imshow('Output', out)
cv2.waitKey(0)
cv2.destroyAllWindows()
The original Paddington img:
The intermediate image to draw the shapes on shapes:
And, the final result out:
After drawing the shapes and blending the images, you can add your texts as before.
Hope that helps!
Simply install pyshine and use putBText, it has following inputs and output.
pip install pyshine
"""
Inputs:
img: cv2 image img
text_offset_x, text_offset_x: X,Y location of text start
vspace, hspace: Vertical and Horizontal space between text and box boundaries
font_scale: Font size
background_RGB: Background R,G,B color
text_RGB: Text R,G,B color
font: Font Style e.g. cv2.FONT_HERSHEY_DUPLEX,cv2.FONT_HERSHEY_SIMPLEX,cv2.FONT_HERSHEY_PLAIN,cv2.FONT_HERSHEY_COMPLEX
cv2.FONT_HERSHEY_TRIPLEX, etc
thickness: Thickness of the text font
alpha: Opacity 0~1 of the box around text
gamma: 0 by default
Output:
img: CV2 image with text and background
"""
Example tested on Python3 and complete demonstration is here:
lena.jpg
simple.py
import pyshine as ps
import cv2
image = cv2.imread('lena.jpg')
text = 'HELLO WORLD!'
image = ps.putBText(image,text,text_offset_x=20,text_offset_y=20,vspace=10,hspace=10, font_scale=2.0,background_RGB=(0,250,250),text_RGB=(255,250,250))
cv2.imshow('Output', image)
cv2.imwrite('out.jpg',image)
cv2.waitKey(0)
out.jpg
another.py
import pyshine as ps
import cv2
import time
image = cv2.imread('lena.jpg')
text = 'ID: '+str(123)
image = ps.putBText(image,text,text_offset_x=20,text_offset_y=20,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(1,1,1))
text = str(time.strftime("%H:%M %p"))
image = ps.putBText(image,text,text_offset_x=image.shape[1]-170,text_offset_y=20,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(1,1,1))
text = '6842'
image = ps.putBText(image,text,text_offset_x=80,text_offset_y=372,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text = "Lena Fors'en"
image = ps.putBText(image,text,text_offset_x=80,text_offset_y=425,vspace=20,hspace=10, font_scale=1.0,background_RGB=(20,210,4),text_RGB=(255,255,255))
text = 'Status: '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-130,text_offset_y=200,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text = 'On time'
image = ps.putBText(image,text,text_offset_x=image.shape[1]-130,text_offset_y=242,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text = 'Attendence: '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-200,text_offset_y=394,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
text = '96.2% '
image = ps.putBText(image,text,text_offset_x=image.shape[1]-200,text_offset_y=436,vspace=10,hspace=10, font_scale=1.0,background_RGB=(228,225,222),text_RGB=(255,255,255))
cv2.imshow('Output', image)
cv2.imwrite('out.jpg',image)
cv2.waitKey(0)
out.jpg
A simpler solution (although a bit less efficient in terms of memory) is:
create a copy of the original image
draw the desired shapes/text on the the original image
get the overlay with: alpha*img + (1-alpha)*img_cpy
In this way each original pixel will not change it's value (since alpha*px + (1-alpha)px = px), whereas pixels which were drawn on will be affected by the overlay.
This eliminates the need to perform crops and pesky calculations seen in the other answers.
...and applying to to the OP's code:
frame_cpy = frame.copy()
cv2.rectangle(frame, (500, 650), (800, 700), (42, 219, 151), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (510, 685), font, 1.0, (255, 255, 255), 1)
alpha = 0.4
frame_overlay=cv2.addWeighted(frame, alpha, frame_cpy,1-alpha, gamma=0)
cv2.imshow("overlay result",frame_overlay)
cv2.waitKey(0)
Disclaimer: this answer was inspired by a post on www.pyimagesearch.com

Blend overlapping images in python

I am taking two images in python and overlapping the first image onto the second image. What I would like to do is blend the images where they overlap. Is there a way to do this in python other than a for loop?
PIL has a blend function which combines two RGB images with a fixed alpha:
out = image1 * (1.0 - alpha) + image2 * alpha
However, to use blend, image1 and image2 must be the same size.
So to prepare your images you'll need to paste each of them into a new image of
the appropriate (combined) size.
Since blending with alpha=0.5 averages the RGB values from both images equally,
we need to make two versions of the panorama -- one with img1 one top and one with img2 on top. Then regions with no overlap have RGB values which agree (so their averages will remain unchanged) and regions of overlap will get blended as desired.
import operator
from PIL import Image
from PIL import ImageDraw
# suppose img1 and img2 are your two images
img1 = Image.new('RGB', size=(100, 100), color=(255, 0, 0))
img2 = Image.new('RGB', size=(120, 130), color=(0, 255, 0))
# suppose img2 is to be shifted by `shift` amount
shift = (50, 60)
# compute the size of the panorama
nw, nh = map(max, map(operator.add, img2.size, shift), img1.size)
# paste img1 on top of img2
newimg1 = Image.new('RGBA', size=(nw, nh), color=(0, 0, 0, 0))
newimg1.paste(img2, shift)
newimg1.paste(img1, (0, 0))
# paste img2 on top of img1
newimg2 = Image.new('RGBA', size=(nw, nh), color=(0, 0, 0, 0))
newimg2.paste(img1, (0, 0))
newimg2.paste(img2, shift)
# blend with alpha=0.5
result = Image.blend(newimg1, newimg2, alpha=0.5)
img1:
img2:
result:
If you have two RGBA images here is a way to perform alpha compositing.
If you'd like a soft edge when stitching two images together you could blend them with a sigmoid function.
Here is a simple grayscale example:
import numpy as np
import matplotlib.image
import math
def sigmoid(x):
y = np.zeros(len(x))
for i in range(len(x)):
y[i] = 1 / (1 + math.exp(-x[i]))
return y
sigmoid_ = sigmoid(np.arange(-1, 1, 1/50))
alpha = np.repeat(sigmoid_.reshape((len(sigmoid_), 1)), repeats=100, axis=1)
image1_connect = np.ones((100, 100))
image2_connect = np.zeros((100, 100))
out = image1_connect * (1.0 - alpha) + image2_connect * alpha
matplotlib.image.imsave('blend.png', out, cmap = 'gray')
If you blend white and black squares result will look something like that:
+ =

Specify image filling color when rotating in python with PIL and setting expand argument to true

I'm trying to rotate an image in Python using PIL and having the expand argument to true. It seems that when the background of my image is black, the resulting image saved as a bmp will be a lot smaller than if I have a white background for my image, and then I replace the black due to expand with white. In either case, my original image is always of two colors, and right now i need the file size to be small, since I'm putting these images on an embedded device.
Any ideas if i can force rotate to fill in another color when expanding or if there is another way to rotate my picture in order to make it small?
If your original image has no alpha layer, you can use an alpha layer as a mask to convert the background to white. When rotate creates the "background", it makes it fully transparent.
# original image
img = Image.open('test.png')
# converted to have an alpha layer
im2 = img.convert('RGBA')
# rotated image
rot = im2.rotate(22.2, expand=1)
# a white image same size as rotated image
fff = Image.new('RGBA', rot.size, (255,)*4)
# create a composite image using the alpha layer of rot as a mask
out = Image.composite(rot, fff, rot)
# save your work (converting back to mode='1' or whatever..)
out.convert(img.mode).save('test2.bmp')
There is a parameter fillcolor in a rotate method to specify color which will be use for expanded area:
white = (255,255,255)
pil_image.rotate(angle, PIL.Image.NEAREST, expand = 1, fillcolor = white)
https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.rotate
Here is a working version, inspired by the answer, but it works without opening or saving images and shows how to rotate a text.
The two images have colored background and alpha channel different from zero to show what's going on. Changing the two alpha channels from 92 to 0 will make them completely transparent.
from PIL import Image, ImageFont, ImageDraw
text = 'TEST'
font = ImageFont.truetype(r'C:\Windows\Fonts\Arial.ttf', 50)
width, height = font.getsize(text)
image1 = Image.new('RGBA', (200, 150), (0, 128, 0, 92))
draw1 = ImageDraw.Draw(image1)
draw1.text((0, 0), text=text, font=font, fill=(255, 128, 0))
image2 = Image.new('RGBA', (width, height), (0, 0, 128, 92))
draw2 = ImageDraw.Draw(image2)
draw2.text((0, 0), text=text, font=font, fill=(0, 255, 128))
image2 = image2.rotate(30, expand=1)
px, py = 10, 10
sx, sy = image2.size
image1.paste(image2, (px, py, px + sx, py + sy), image2)
image1.show()

Categories

Resources