Feathered edges on image with Pillow - image

I'm trying to figure out how to feather the edges of an image using Pillow with Python.
I need something like this cute cat (ignore the visible edges):
I tried im.filter(ImageFilter.BLUR) but is not what I'm looking for.

Have a look at this example:
from PIL import Image
from PIL import ImageFilter
RADIUS = 10
# Open an image
im = Image.open(INPUT_IMAGE_FILENAME)
# Paste image on white background
diam = 2*RADIUS
back = Image.new('RGB', (im.size[0]+diam, im.size[1]+diam), (255,255,255))
back.paste(im, (RADIUS, RADIUS))
# Create blur mask
mask = Image.new('L', (im.size[0]+diam, im.size[1]+diam), 255)
blck = Image.new('L', (im.size[0]-diam, im.size[1]-diam), 0)
mask.paste(blck, (diam, diam))
# Blur image and paste blurred edge according to mask
blur = back.filter(ImageFilter.GaussianBlur(RADIUS/2))
back.paste(blur, mask=mask)
back.save(OUTPUT_IMAGE_FILENAME)
Original image (author - Irene Mei):
Pasted on white background:
Blur region (paste mask):
Result:

Providing modified solution with gradient paste mask (as requested by #Crickets).
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFilter
RADIUS = 10
# Open an image
im = Image.open(INPUT_IMAGE_FILENAME)
# Paste image on white background
diam = 2*RADIUS
back = Image.new('RGB', (im.size[0]+diam, im.size[1]+diam), (255,255,255))
back.paste(im, (RADIUS, RADIUS))
# Create paste mask
mask = Image.new('L', back.size, 0)
draw = ImageDraw.Draw(mask)
x0, y0 = 0, 0
x1, y1 = back.size
for d in range(diam+RADIUS):
x1, y1 = x1-1, y1-1
alpha = 255 if d<RADIUS else int(255*(diam+RADIUS-d)/diam)
draw.rectangle([x0, y0, x1, y1], outline=alpha)
x0, y0 = x0+1, y0+1
# Blur image and paste blurred edge according to mask
blur = back.filter(ImageFilter.GaussianBlur(RADIUS/2))
back.paste(blur, mask=mask)
back.save(OUTPUT_IMAGE_FILENAME)
Paste mask:
Result:

Related

How to resize an image with PIL getbbox to the maximum possible size?

I'm trying to resize sprites to a 96x96 boundary while retaining the aspect ratio.
The following code:
im.getbbox()
Returns a tuple containing the bounds of the sprite (original backgrounds are transparent), and I am stuck on this next part - I want to take that part of the image and resize it as large as it can possibly go within a 96x96 boundary
Here's an example of some sprites from Pokemon:
Since some are 80x80, some are 64x64, and the largest are 96x96, I would like to effectively select the contents of the sprite with im.getbbox() and then enlarge it to fit on top of a 96px white background.
Could anyone please help? I'm not sure how to maximise it within the bounds
My current code is as follows:
x = 0
for dirname, dirs, files in os.walk(path):
for filename in files:
x+=1
path = dirname + "/" + filename
print(path)
im = Image.open(path)
fill_color = (255,255,255)
im = im.convert("RGBA")
if im.mode in ('RGBA', 'LA'):
background = Image.new(im.mode[:-1], im.size, fill_color)
background.paste(im, im.split()[-1])
im = background
imResize = ImageOps.fit(im, (96, 96), Image.BOX, 0, (0.5, 0.5))
imResize.save("dataset/images/" + str(x) + '.png', 'PNG')
It takes the image and pastes it on to a white background and sets the size as 96px. This is fine for the native 96px images but the aspect ratio of the smaller images is ruined. By being able to enlarge them to the maximum bounds of a 96px image then it should prevent this from happening
Thanks!
ok, made my own pics, here:
zY28f_64.png, zY28f_80.png , zY28f_96.png
ended up with code :
from PIL import Image, ImageOps
import os
path= 'images'
x = 0
for dirname, dirs, files in os.walk(path):
for filename in files:
x+=1
path = dirname + "/" + filename
print(path)
im = Image.open(path)
im.show()
fill_color = (255,255,255)
im = im.convert("RGBA")
if im.mode in ('RGBA', 'LA'):
background = Image.new(im.mode[:-1], im.size, fill_color)
# background.paste(im, im.split()[-1])
background.paste(im)
im = background
im_inv = ImageOps.invert(im) ### make white background black
imResize = ImageOps.fit(im.crop(im_inv.getbbox()), (96, 96), Image.BOX, 0, (0.5, 0.5)) # im.crop crop on box got from image with black background, Image.getbbox works only with black bacground returning biggest box containing non zero(non black ) pixels
imResize.show()
# imResize.save("dataset/images/" + str(x) + '.png', 'PNG')
output:
zY28f_64.png2.png , zY28f_80.png1.png , zY28f_96.png3.png
main changes here:
make white background black
im_inv = ImageOps.invert(im)
im.crop crop on box got from image with black background, Image.getbbox works only with black background returning biggest box containing non zero(non black ) pixels
imResize = ImageOps.fit(im.crop(im_inv.getbbox()), (96, 96), Image.BOX, 0, (0.5, 0.5))
Since ImageOps.fit(...) line return chopped images, not sure how it works, to keep the entire figures I used code from PIL Image.resize() not resizing the picture to get:
from PIL import Image, ImageOps
import os
import math
path= 'images'
x = 0
for dirname, dirs, files in os.walk(path):
for filename in files:
x+=1
path = dirname + "/" + filename
print(path)
im = Image.open(path)
fill_color = (255,255,255)
im = im.convert("RGBA")
if im.mode in ('RGBA', 'LA'):
background = Image.new(im.mode[:-1], im.size, fill_color)
# background.paste(im, im.split()[-1])
background.paste(im)
im = background
im_inv = ImageOps.invert(im)
imResize = im.crop(im_inv.getbbox())
width, height = imResize.size
print(width, height)
if height > width:
ratio = math.floor(height / width)
newheight = ratio * 96
print(imResize, ratio, newheight)
imResize = imResize.resize((96, newheight))
else:
ratio = math.floor(width /height )
newwidth = ratio * 96
print(imResize, ratio, newwidth)
imResize = imResize.resize((newwidth, 96))
imResize.show()
print(imResize.size)
imResize.save(path + str(x) + '.png', 'PNG')
output:
zY28f_64.png2.png , zY28f_80.png1.png , zY28f_96.png3.png

How can I make the `cv2.imshow` output the same as the `plt.imshow` output?

How can I make the cv2.imshow output the same as the plt.imshow output?
# loading image
img0 = cv2.imread("image.png")
# converting to gray scale
gray = cv2.cvtColor(img0, cv2.COLOR_BGR2GRAY)
# remove noise
img = cv2.GaussianBlur(gray, (3, 3), 0)
# convolute with proper kernels
laplacian = cv2.Laplacian(img, cv2.CV_64F)
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5) # x
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5) # y
imgboth = cv2.addWeighted(sobelx, 0.5, sobely, 0.5, 0)
plt.imshow(imgboth, cmap='gray')
plt.show()
cv2.imshow("img", cv2.resize(imgboth, (960, 540)))
cv2.waitKey(0)
cv2.destroyAllWindows()
original image
plt.output
cv2.imshow
# ...
canvas = imgboth.astype(np.float32)
canvas /= np.abs(imgboth).max()
canvas += 0.5
cv.namedWindow("canvas", cv.WINDOW_NORMAL)
cv.imshow("canvas", canvas)
cv.waitKey()
cv.destroyWindow("canvas")
only looks different because you posted thumbnails, not the original size image.
when you give imshow floating point values, which you do because laplacian and sobelx are floating point, then it assumes a range of 0.0 .. 1.0 as black .. white.
matplotlib automatically scales data. OpenCV's imshow doesn't. both behaviors have pros and cons.

batch crop quad images with diffenrent sizes to a circle

I have lots of images of planets in differing sizes like
They are all positioned exactly in the middle of the square images but with different height.
Now I want to crop them and make the black border transparent. I tried with convert (ImageMagick 6.9.10-23) like this:
for i in planet_*.jpg; do
nr=$(echo ${i/planet_/}|sed s/.jpg//g|xargs)
convert $i -fuzz 1% -transparent black trans/planet_${nr}.png
done
But this leaves some artifacts like:
Is there a command to crop all images in a circle, so the planet is untouched? (It mustn't be imagemagick).
I could also imagine a solution where I would use a larger -fuzz value and then fill all transparent pixels in the inner planet circle with black.
Those are all planets, I want to convert: download zip
Here is one way using Python Opencv from the minEclosingCircle.
Input:
import cv2
import numpy as np
import skimage.exposure
# read image
img = cv2.imread('planet.jpg')
h, w, c = img.shape
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
# get contour
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
# get enclosing circle
center, radius = cv2.minEnclosingCircle(big_contour)
cx = int(round(center[0]))
cy = int(round(center[1]))
rr = int(round(radius))
# draw outline circle over input
circle = img.copy()
cv2.circle(circle, (cx,cy), rr, (0, 0, 255), 1)
# draw white filled circle on black background as mask
mask = np.full((h,w), 0, dtype=np.uint8)
cv2.circle(mask, (cx,cy), rr, 255, -1)
# antialias
blur = cv2.GaussianBlur(mask, (0,0), sigmaX=2, sigmaY=2, borderType = cv2.BORDER_DEFAULT)
mask = skimage.exposure.rescale_intensity(blur, in_range=(127,255), out_range=(0,255))
# put mask into alpha channel to make outside transparent
imgT = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
imgT[:,:,3] = mask
# crop the image
ulx = int(cx-rr+0.5)
uly = int(cy-rr+0.5)
brx = int(cx+rr+0.5)
bry = int(cy+rr+0.5)
print(ulx,brx,uly,bry)
crop = imgT[uly:bry+1, ulx:brx+1]
# write result to disk
cv2.imwrite("planet_thresh.jpg", thresh)
cv2.imwrite("planet_circle.jpg", circle)
cv2.imwrite("planet_mask.jpg", mask)
cv2.imwrite("planet_transparent.png", imgT)
cv2.imwrite("planet_crop.png", crop)
# display it
cv2.imshow("thresh", thresh)
cv2.imshow("circle", circle)
cv2.imshow("mask", mask)
cv2.waitKey(0)
Threshold image:
Circle on input:
Mask image:
Transparent image:
Cropped transparent image:
packages to install
sudo apt install python3-opencv python3-sklearn python3-skimage

How Can I only keep text with specific color from image via opencv and python?

I have some invoice image with some text overlapping, which make some trouble for later processing, and what I only is the text in black. some I want to remove the text which is in other colors.
is there any way to achieve this?
the image is attached as example.
I have tried to solve it with opencv, but i still can't solve this:
import numpy as np import cv2
img = cv2.imread('11.png')
lower = np.array([150,150,150])
upper = np.array([200,200,200])
mask = cv2.inRange(img, lower, upper)
res = cv2.bitwise_and(img, img, mask=mask)
cv2.imwrite('22.png',res)
[image with multiple color][1]
[1]: https://i.stack.imgur.com/nWQrV.pngstrong text
The text is darker and less saturated. And as suggested as #J.D. the HSV color space is good. But his range is wrong.
In OpenCV, the H ranges in [0, 180], while the S/V ranges in [0, 255]
Here is a colormap I made in the last year, I think it's helpful.
(1) Use cv2.inRange
(2) Just threshold the V(HSV) channel:
th, threshed = cv2.threshold(v, 150, 255, cv2.THRESH_BINARY_INV)
(3) Just threshold the S(HSV) channel:
th, threshed2 = cv2.threshold(s, 30, 255, cv2.THRESH_BINARY_INV)
The result:
The demo code:
# 2018/12/30 22:21
# 2018/12/30 23:25
import cv2
img = cv2.imread("test.png")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)
mask = cv2.inRange(hsv, (0,0,0), (180, 50, 130))
dst1 = cv2.bitwise_and(img, img, mask=mask)
th, threshed = cv2.threshold(v, 150, 255, cv2.THRESH_BINARY_INV)
dst2 = cv2.bitwise_and(img, img, mask=threshed)
th, threshed2 = cv2.threshold(s, 30, 255, cv2.THRESH_BINARY_INV)
dst3 = cv2.bitwise_and(img, img, mask=threshed2)
cv2.imwrite("dst1.png", dst1)
cv2.imwrite("dst2.png", dst2)
cv2.imwrite("dst3.png", dst3)
How to detect colored patches in an image using OpenCV?
How to define a threshold value to detect only green colour objects in an image :Opencv
Converting to the HSV colorspace makes selecting colors easier.
The code below does what you want.
Result:
import numpy as np
import cv2
kernel = np.ones((2,2),np.uint8)
# load image
img = cv2.imread("image.png")
# Convert BGR to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# define range of black color in HSV
lower_val = np.array([0,0,0])
upper_val = np.array([179,100,130])
# Threshold the HSV image to get only black colors
mask = cv2.inRange(hsv, lower_val, upper_val)
# Bitwise-AND mask and original image
res = cv2.bitwise_and(img,img, mask= mask)
# invert the mask to get black letters on white background
res2 = cv2.bitwise_not(mask)
# display image
cv2.imshow("img", res)
cv2.imshow("img2", res2)
cv2.waitKey(0)
cv2.destroyAllWindows()
To change the level of black selected, tweak from the upper_val, the value currently set at 130. Higher = allow lighter shades (it's called the Value). Also the value currently at 100: lower = allow less color (actually: saturation).
Read more about the HSV colorspace here.
I always find the image below very helpfull. The bottom 'disc' is all black. As you move up in Value, lighter pixels are also selected. The pixels with low saturation stay shades of gray until white (the center), the pixels with high saturation get colored(the edge).That's why you tweak those values.
Edit: As #Silencer pointed out, my range was off. Fixed it.

How to color the mask image obtained out of a polygon

I am getting a mask out of a polygon and below is the image. Now I see that there is a white boundary but I want not just the boundary but inside boundary as white too.
Here is my code:
sar_polygon = Image.new('L', (int(range_samples), int(azimuth_lines)), 0)
draw = ImageDraw.Draw(sar_polygon)
for vertex in range(len(sar_ver)):
st = sar_ver[vertex]
try:
end = sar_ver[vertex + 1]
except IndexError:
end = sar_ver[0]
draw.line((st[0], st[1], end[0], end[1]), fill=1)
sar_polygon.save('polygon.jpg', 'JPEG')
You are currently drawing lines along the edges. You are interested in the polygon method, or perhaps the rectangle method - http://pillow.readthedocs.io/en/5.2.x/reference/ImageDraw.html#PIL.ImageDraw.PIL.ImageDraw.ImageDraw.polygon.
from PIL import Image, ImageDraw
sar_polygon = Image.new('RGB', (500, 500))
draw = ImageDraw.Draw(sar_polygon)
sar_ver = ((100,100),(200,100),(200,200),(100,200))
draw.polygon(sar_ver, fill='#f00')
sar_polygon.save('polygon.jpg', 'JPEG')

Resources