I researched quite a bit already, and all that I found was how to apply gradients to text generated with Pillow. However, I wanted to know how can I apply a gradient instead of a regular single color fill to a drawn shape (specifically a polygon).
image = Image.new('RGBA', (50, 50))
draw = ImageDraw.Draw(image)
draw.polygon([10, 10, 20, 40, 40, 20], fill=(255, 50, 210), outline=None)
Here's my attempt to create something, which might fit your use case. It has limited functionality – specifically, only supports linear and radial gradients – and the linear gradient itself is also kind of limited. But, first of all, let's see an exemplary output:
Basically, there are two methods
linear_gradient(i, poly, p1, p2, c1, c2)
and
radial_gradient(i, poly, p, c1, c2)
which both get an Pillow Image object i, a list of vertices describing a polygon poly, start and stop colors for the gradient c1 and c2, and either two points p1 and p2 describing the direction of the linear gradient (from one vertex to a second one) or a single point p describing the center of the radial gradient.
In both methods, the initial polygon is drawn on an empty canvas of the final image's size, solely using the alpha channel.
For the linear gradient, the angle between p1 and p2, is calculated. The drawn polygon is rotated by that angle, and cropped to get the needed dimensions for a proper linear gradient. That one is simply created by np.linspace. The gradient is rotated by the known angle, but in the opposite direction, and finally translated to fit the actual polygon. The gradient image is pasted on the intermediate polygon image to get the polygon with the linear gradient, and the result is pasted onto the actual image.
Limitation for the linear gradient: You better pick two vertices of the polygon "on opposite sides", or better: Such that all points inside the polygon are within the virtual space spanned by that two points. Otherwise, the current implementation might fail, e.g. when choosing two neighbouring vertices.
The radial gradient method works slightly different. The maximum distance from p to all polygon vertices is determined. Then, for all points in an intermediate image of the actual image size, the distance to p is calculated and normalized by the calculated maximum distance. We get values in the range [0.0 ... 1.0] for all points inside the polygon. The values are used to calculate appropriate colors ranging from c1 to c2. As for the linear gradient, that gradient image is pasted on the intermediate polygon image, and the result is pasted onto the actual image.
Hopefully, the code is self-explanatory using the comments. But if there are questions, please don't hesitate to ask!
Here's the full code:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image, ImageDraw
# Draw polygon with linear gradient from point 1 to point 2 and ranging
# from color 1 to color 2 on given image
def linear_gradient(i, poly, p1, p2, c1, c2):
# Draw initial polygon, alpha channel only, on an empty canvas of image size
ii = Image.new('RGBA', i.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(ii)
draw.polygon(poly, fill=(0, 0, 0, 255), outline=None)
# Calculate angle between point 1 and 2
p1 = np.array(p1)
p2 = np.array(p2)
angle = np.arctan2(p2[1] - p1[1], p2[0] - p1[0]) / np.pi * 180
# Rotate and crop shape
temp = ii.rotate(angle, expand=True)
temp = temp.crop(temp.getbbox())
wt, ht = temp.size
# Create gradient from color 1 to 2 of appropriate size
gradient = np.linspace(c1, c2, wt, True).astype(np.uint8)
gradient = np.tile(gradient, [2 * h, 1, 1])
gradient = Image.fromarray(gradient)
# Paste gradient on blank canvas of sufficient size
temp = Image.new('RGBA', (max(i.size[0], gradient.size[0]),
max(i.size[1], gradient.size[1])), (0, 0, 0, 0))
temp.paste(gradient)
gradient = temp
# Rotate and translate gradient appropriately
x = np.sin(angle * np.pi / 180) * ht
y = np.cos(angle * np.pi / 180) * ht
gradient = gradient.rotate(-angle, center=(0, 0),
translate=(p1[0] + x, p1[1] - y))
# Paste gradient on temporary image
ii.paste(gradient.crop((0, 0, ii.size[0], ii.size[1])), mask=ii)
# Paste temporary image on actual image
i.paste(ii, mask=ii)
return i
# Draw polygon with radial gradient from point to the polygon border
# ranging from color 1 to color 2 on given image
def radial_gradient(i, poly, p, c1, c2):
# Draw initial polygon, alpha channel only, on an empty canvas of image size
ii = Image.new('RGBA', i.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(ii)
draw.polygon(poly, fill=(0, 0, 0, 255), outline=None)
# Use polygon vertex with highest distance to given point as end of gradient
p = np.array(p)
max_dist = max([np.linalg.norm(np.array(v) - p) for v in poly])
# Calculate color values (gradient) for the whole canvas
x, y = np.meshgrid(np.arange(i.size[0]), np.arange(i.size[1]))
c = np.linalg.norm(np.stack((x, y), axis=2) - p, axis=2) / max_dist
c = np.tile(np.expand_dims(c, axis=2), [1, 1, 3])
c = (c1 * (1 - c) + c2 * c).astype(np.uint8)
c = Image.fromarray(c)
# Paste gradient on temporary image
ii.paste(c, mask=ii)
# Paste temporary image on actual image
i.paste(ii, mask=ii)
return i
# Create blank canvas with zero alpha channel
w, h = (800, 600)
image = Image.new('RGBA', (w, h), (0, 0, 0, 0))
# Draw first polygon with radial gradient
polygon = [(100, 200), (320, 130), (460, 300), (700, 500), (350, 550), (200, 400)]
point = (350, 350)
color1 = (255, 0, 0)
color2 = (0, 255, 0)
image = radial_gradient(image, polygon, point, color1, color2)
# Draw second polygon with linear gradient
polygon = [(500, 50), (650, 250), (775, 150), (700, 25)]
point1 = (700, 25)
point2 = (650, 250)
color1 = (255, 255, 0)
color2 = (0, 0, 255)
image = linear_gradient(image, polygon, point1, point2, color1, color2)
# Draw third polygon with linear gradient
polygon = [(50, 550), (200, 575), (200, 500), (100, 300), (25, 450)]
point1 = (100, 300)
point2 = (200, 575)
color1 = (255, 255, 255)
color2 = (255, 128, 0)
image = linear_gradient(image, polygon, point1, point2, color1, color2)
# Save image
image.save('image.png')
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
Matplotlib: 3.4.0
NumPy: 1.20.2
Pillow: 8.1.2
----------------------------------------
I'm still not sure I understand your question fully. But it sounds like you want to have a bunch of shapes with their own gradients? One approach would be to generate the gradients of each shape separately then combine the shapes after the fact.
Piggy-backing off the answer already referred to by #HansHirse, you can do something like:
from PIL import Image, ImageDraw
def channel(i, c, size, startFill, stopFill):
"""calculate the value of a single color channel for a single pixel"""
return startFill[c] + int((i * 1.0 / size) * (stopFill[c] - startFill[c]))
def color(i, size, startFill, stopFill):
"""calculate the RGB value of a single pixel"""
return tuple([channel(i, c, size, startFill, stopFill) for c in range(3)])
def round_corner(radius):
"""Draw a round corner"""
corner = Image.new("RGBA", (radius, radius), (0, 0, 0, 0))
draw = ImageDraw.Draw(corner)
draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill="blue")
return corner
def apply_grad_to_corner(corner, gradient, backwards=False, topBottom=False):
width, height = corner.size
widthIter = range(width)
if backwards:
widthIter = reversed(widthIter)
for i in range(height):
gradPos = 0
for j in widthIter:
if topBottom:
pos = (i, j)
else:
pos = (j, i)
pix = corner.getpixel(pos)
gradPos += 1
if pix[3] != 0:
corner.putpixel(pos, gradient[gradPos])
return corner
def round_rectangle(size, radius, startFill, stopFill, runTopBottom=False):
"""Draw a rounded rectangle"""
width, height = size
rectangle = Image.new("RGBA", size)
if runTopBottom:
si = height
else:
si = width
gradient = [color(i, width, startFill, stopFill) for i in range(si)]
if runTopBottom:
modGrad = []
for i in range(height):
modGrad += [gradient[i]] * width
rectangle.putdata(modGrad)
else:
rectangle.putdata(gradient * height)
origCorner = round_corner(radius)
# upper left
corner = origCorner
apply_grad_to_corner(corner, gradient, False, runTopBottom)
rectangle.paste(corner, (0, 0))
# lower left
if runTopBottom:
gradient.reverse()
backwards = True
else:
backwards = False
corner = origCorner.rotate(90)
apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
rectangle.paste(corner, (0, height - radius))
# lower right
if not runTopBottom:
gradient.reverse()
corner = origCorner.rotate(180)
apply_grad_to_corner(corner, gradient, True, runTopBottom)
rectangle.paste(corner, (width - radius, height - radius))
# upper right
if runTopBottom:
gradient.reverse()
backwards = False
else:
backwards = True
corner = origCorner.rotate(270)
apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
rectangle.paste(corner, (width - radius, 0))
return rectangle
def get_concat_h(im1, im2):
dst = Image.new("RGB", (im1.width + im2.width, im1.height))
dst.paste(im1, (0, 0))
dst.paste(im2, (im1.width, 0))
return dst
def get_concat_v(im1, im2):
dst = Image.new("RGB", (im1.width, im1.height + im2.height))
dst.paste(im1, (0, 0))
dst.paste(im2, (0, im1.height))
return dst
img1 = round_rectangle((200, 200), 70, (255, 0, 0), (0, 255, 0), True)
img2 = round_rectangle((200, 200), 70, (0, 255, 0), (0, 0, 255), True)
get_concat_h(img1, img2).save("testcombo.png")
The result looks something like this:
The only "new" stuff comes in at the end: the images are just combined. If you want to get wild you can rotate the individual shapes or allow them to overlap (by tweaking the position of the images in get_concat_h + playing around with the final image size.)
I'm making a Python script that:
gets the image from file
Looks for a papersheet (done by openCV)
Warps the image
crops the margins of papersheet
Looks for lowest amount of white pixels in a single row of pixels which makes up for width/height of a single field of a grid (goes pixel by pixel)
Recreates the labyrinth based on what are the average pixel RGB Values in each field (goes pixel by pixel) and saves it to file
It shows me the grid of 0 and 1s in the CLI
When I do it on original image (5.2MB, 4000x3000) , it takes up to 20 seconds (although I have pretty snappy 8 core ryzen cpu), actually I only need the output (0 or 1 grid). How can I speed up the process as it will run on raspberry pi and it needs to take only about few seconds to finish? I know that the code is quite long, however it's split into sections that are described so it should be fairly easy to read.
#LIBRARY IMPORT
import math
import turtle
import time
import sys
from collections import deque
import numpy as np
from skimage import exposure
import argparse
import imutils
import cv2
from PIL import Image, ImageDraw
#LOAD THE FIRST IMAGE
image = cv2.imread("./image3.jpg")
ratio = image.shape[0] / 600.0
orig = image.copy()
image = imutils.resize(image, height = 600)
#Range of the colors of paper
lower = [160, 160, 160]
upper = [255,255,255]
#create array from ranges
lower = np.array(lower, dtype="uint8")
upper = np.array(upper, dtype="uint8")
#finding contours
mask = cv2.inRange(image, lower, upper)
output = cv2.bitwise_and(image, image, mask=mask)
ret,thresh = cv2.threshold(mask, 40, 255, 0)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = max(contours, key = cv2.contourArea)
cnt = contours
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
for c in [contours]:
#contour approximation
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.01 * peri, True)
#crop of contour with 4 points
if len(approx) == 4:
screenCnt = approx
break
img = cv2.drawContours(image,[screenCnt],0,(0,0,255),2)
print(screenCnt)
pts = screenCnt.reshape(4, 2)
rect = np.zeros((4, 2), dtype = "float32")
s = pts.sum(axis = 1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis = 1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
rect *= ratio
(tl, tr, br, bl) = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
maxHeight = max(int(heightA), int(heightB))
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype = "float32")
M = cv2.getPerspectiveTransform(rect, dst)
warp = cv2.warpPerspective(orig, M, (maxWidth, maxHeight))
warp = imutils.resize(warp)
cv2.imshow("image", image)
cv2.moveWindow("image",0,450)
cv2.imwrite("warp.jpg",warp)
#warping of found labyrinth
warp = cv2.imread("warp.jpg",0)
warp = cv2.medianBlur(warp,5)
ret,warp = cv2.threshold(warp,170,255,cv2.THRESH_BINARY)
cv2.imwrite("warp.jpg",warp)
image_file = "warp.jpg"
im = Image.open(image_file)
width, height = im.size
T = height*(5/210)
B = height-(height*(5/210))
L = width*(5/210)
R = width-(width*(5/210))
im_crop = im.crop((L, T, R, B))
im=im_crop
im_crop.save('warp.png', quality=100)
white=0
#finding field-size
im = Image.open("warp.png")
width, height = im.size
minimalGridWidth2 = width
minimalGridWidth = width
MaximumGridWidth = width
for y in range (0, height):
for x in range (0, width):
if im.getpixel((x,y)) > 200:
white = white+1
if white <= minimalGridWidth:
minimalGridWidth = white
#checks out how many X how many fields the labirynt has, checks what's the average value (more black or more white) is there on that field and recreates the new "ideal" labyrinth from this data
gridWidth = int(round(width/minimalGridWidth))
gridHeight = int(round(height/minimalGridWidth))
print(gridWidth)|
print(gridHeight)
newHeight = 0
newWidth = 0
newHeight=(minimalGridWidth*gridHeight)
newWidth=(minimalGridWidth*gridWidth)
print(minimalGridWidth)
print(newWidth)
print(newHeight)
im = im.resize((newWidth, newHeight), Image.ANTIALIAS)
i=0
x, y = gridWidth, gridHeight
pixelcount = [[0 for x in range(0,gridWidth)] for y in range(0,gridHeight)]
pixelavg = [[0 for x in range(0,gridWidth)] for y in range(0,gridHeight)]
print(pixelcount)
for y in range (0, gridHeight):
for x in range (0, gridWidth):
i=0
pixel=0
for v in range (0, minimalGridWidth):
for w in range (0, minimalGridWidth):
pixel=pixel+im.getpixel((((x*minimalGridWidth)+w),((y*minimalGridWidth)+v)))
i=i+1
if (pixel/i)<127:
pixelavg[y][x]=1
elif (pixel/i)>127:
pixelavg[y][x]=0
print(np.array(pixelavg))
y,x,v,w,i=0,0,0,0,0
im2 = Image.new('RGB',(newWidth,newHeight),'white')
for y in range (0, gridHeight):
for x in range (0, gridWidth):
for v in range (0, minimalGridWidth):
for w in range (0, minimalGridWidth):
pixelx=pixelavg[y][x]
if pixelx==0:
pixelDoc=(255,255,255)
if pixelx==1:
pixelDoc=(0,0,0)
Xw=((x*minimalGridWidth)+w)
Yh=((y*minimalGridWidth)+v)
im2.putpixel((Xw,Yh),pixelDoc)
im2.save('warp3.png',quality=100)
imx=cv2.imread('warp3.png',0)
cv2.imshow('finito',imx)
cv2.imwrite('koniec.png',imx)
cv2.moveWindow("finito",750,450)
warp=cv2.imread("warp.png",0)
cv2.imshow("warp",warp)
cv2.moveWindow("warp",450,450)
When you are concerned with performance or looking to optimize code, it can help to profile your program.
You could use Python's profiler or an IDE with a profiler like PyCharm.
When profiling your code on a 4000x2665 image, I found the following:
As you can see, the getpixel and putpixel functions take ~60% of the total execution time.
This makes sense, as they are called for every image pixel in a nested loop:
for y in range (0, height):
white = 0
for x in range (0, width):
if im.getpixel((x,y)) > 200:
white = white+1
if white <= minimalGridWidth:
minimalGridWidth = white
The above code can be fixed by replacing the nested loops by image-wide operations.
np_im = np.array(im)
white_per_row = np.sum(np_im > 200, axis=1)
minimalGridWidth = np.min(white_per_row)
Replacing this single operation cuts down the total execution time by 5644 ms or ~32%
How can I make watermark image transparent? For example, 60% transparent. I have tried with putalpha but seems that it's doesn't work as expected
from PIL import Image
temp_image = Image.open('test1.jpg')
watermark = Image.open('watermark.png')
x, y = temp_image.size
image_with_watermark = Image.new('RGBA', (x, y), (0, 0, 0, 0))
image_with_watermark.paste(temp_image, (0, 0))
image_with_watermark.paste(watermark, (0, 0), mask=watermark)
image_with_watermark.show()
EDIT:
ok, this works, need to figure out how to set it up using %
from PIL import Image
temp_image = Image.open('test1.jpg')
watermark = Image.open('watermark.png')
x, y = temp_image.size
watermask = watermark.convert("L").point(lambda x: min(x, 50))
watermark.putalpha(watermask)
image_with_watermark = Image.new('RGBA', (x, y), (0, 0, 0, 0))
image_with_watermark.paste(temp_image, (0, 0))
image_with_watermark.paste(watermark, (0, 0), mask=watermark)
image_with_watermark.show()
meh, quality of the watermark is very low after:
watermask = watermark.convert("L").point(lambda x: min(x, 50))
watermark.putalpha(watermask)
What is the best way to achieve what I need?
Here's a solution that will work for both RGB and RGBA watermark images:
from PIL import Image
TRANSPARENCY = 65 # percentage
temp_image = Image.open('test1.jpg')
watermark = Image.open('watermark.png')
if watermark.mode!='RGBA':
alpha = Image.new('L', watermark.size, 255)
watermark.putalpha(alpha)
paste_mask = watermark.split()[3].point(lambda i: i * TRANSPARENCY / 100.)
temp_image.paste(watermark, (0,0), mask=paste_mask)
temp_image.save('res.png')
Sample image (author - Neil Howard):
Sample watermark (the background is transparent):
Sample result:
I am working on a problem to get the coordinates of only particular color in an image. So i came across this following code, where i have no idea of the fourth and fifth line . Can someone explain the concept why 0.01 * dst.max() is used and all. Thanks in advance.
b, g, r = cv2.split(img)
gray = np.float32(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY))
dst = cv2.cornerHarris(gray, 2, 3, 0.04)
dst = np.where((dst > 0.01 * dst.max()) & (r > 130) & (g < 100) & (b < 100), dst, 0)
img[dst > 0.01 * dst.max()] = [0, 255, 0]
coord = np.where(np.all(img == (0, 255, 0), axis=-1))
coorarray = zip(coord[0], coord[1])
the code gives the coordinate of the 'corners' in your image.
dst > 0.01 * dst.max()
is a threshold on the 'corner' detection made by 'cornerHarris'.
Where the 'corner' signal is significative (dst > 0.01 * dst.max()) you create a new blank image and black only the interesting pixels :
img[dst > 0.01 * dst.max()] = [0, 255, 0].
Then it determine the coordinates of those black pixels :
coord = np.where(np.all(img == (0, 255, 0), axis=-1))
In your case, if you want to make a particular color detection, you do not need the 'dst' lines, you have to do a selection on the color :
colorSelection = (r == 130) & (g == 100) & (b == 100) ### select your color rgb
img[colorSelection] = [0, 255, 0] ### create a black/white image
coord = np.where(np.all(img == (0, 255, 0), axis=-1)) ### find the coordinate of your interesting pixels
I'm trying to make a number of white pixels transparent, however I'm doing something wrong.
I can alter the colour of the pixels, however my code seems to ignore any alterations I make to the alpha value. I'm fairly new to PIL, and Python in general, so it's probably a relatively simple mistake.
Here's the code:
image_two = Image.open ("image_two.bmp")
image_two = image_two.convert ("RGBA")
pixels = image_two.load()
for y in xrange (image_two.size[1]):
for x in xrange (image_two.size[0]):
if pixels[x, y] == (0, 0, 0, 255):
pixels[x, y] = (0, 0, 0, 255)
else:
pixels[x, y] = (255, 255, 255, 0)
image_two.save("image_two")
My version of PIL does not support alpha channel in BMP files. I was able to use your code to load a PNG file with alpha. When I try to write that back out to a BMP file, I get a python exception that tells me "IOError: cannot write mode RGBA as BMP".
Your code says:
if pixels[x, y] == (0, 0, 0, 255): #black with alpha of 255
pixels[x, y] = (0, 0, 0, 255) #black with alpha of 255
else:
pixels[x, y] = (255, 255, 255, 0) #white with alpha of 255
A white pixel would have the r,g, and b set to "255". So probably what you want to do is something like this:
if pixels[x,y] == (255,255,255,255):
pixels[x,y] = (pixels[x,y][0], pixels[x,y][1], pixels[x,y][2], 0) #just set this pixel's alpha channel to 0
You may not need an else here, because if the pixels are not white with alpha of 255, you probably don't want to touch them.
I modified your code like this:
import Image
image_two = Image.open ("image_two.png")
image_two = image_two.convert ("RGBA")
pixels = image_two.load()
for y in xrange (image_two.size[1]):
for x in xrange (image_two.size[0]):
if pixels[x, y][3] == 255:
pixels[x, y] = (255, 0, 0, 255)
else:
pixels[x, y] = (255, 255, 255, 255)
image_two.save("image_two2.png")
This code takes my image and writes out a mask - a white pixel where the alpha is 0 and a red pixel where the alpha is 255.