I have a rectangle image, and I would like to round its corners and then add a black border to it (so the border is also round).
Is there an easy way to achieve it?
That'd be the desired output:
I have a rectangle image, and I would like to round its corners and then add a black border to it (so the border is also round).
Is there an easy way to achieve it?
That'd be the desired output:
After some discussion with Mark in the comments on my first answer, I decided to make another solution using OpenCV and NumPy, which is able to easily feed some real images, e.g. photos, to the method and get the image including a border with rounded corners, and transparency outside the border!
import cv2
import numpy as np
def rect_with_rounded_corners(image, r, t, c):
"""
:param image: image as NumPy array
:param r: radius of rounded corners
:param t: thickness of border
:param c: color of border
:return: new image as NumPy array with rounded corners
"""
c += (255, )
h, w = image.shape[:2]
# Create new image (three-channel hardcoded here...)
new_image = np.ones((h+2*t, w+2*t, 4), np.uint8) * 255
new_image[:, :, 3] = 0
# Draw four rounded corners
new_image = cv2.ellipse(new_image, (int(r+t/2), int(r+t/2)), (r, r), 180, 0, 90, c, t)
new_image = cv2.ellipse(new_image, (int(w-r+3*t/2-1), int(r+t/2)), (r, r), 270, 0, 90, c, t)
new_image = cv2.ellipse(new_image, (int(r+t/2), int(h-r+3*t/2-1)), (r, r), 90, 0, 90, c, t)
new_image = cv2.ellipse(new_image, (int(w-r+3*t/2-1), int(h-r+3*t/2-1)), (r, r), 0, 0, 90, c, t)
# Draw four edges
new_image = cv2.line(new_image, (int(r+t/2), int(t/2)), (int(w-r+3*t/2-1), int(t/2)), c, t)
new_image = cv2.line(new_image, (int(t/2), int(r+t/2)), (int(t/2), int(h-r+3*t/2)), c, t)
new_image = cv2.line(new_image, (int(r+t/2), int(h+3*t/2)), (int(w-r+3*t/2-1), int(h+3*t/2)), c, t)
new_image = cv2.line(new_image, (int(w+3*t/2), int(r+t/2)), (int(w+3*t/2), int(h-r+3*t/2)), c, t)
# Generate masks for proper blending
mask = new_image[:, :, 3].copy()
mask = cv2.floodFill(mask, None, (int(w/2+t), int(h/2+t)), 128)[1]
mask[mask != 128] = 0
mask[mask == 128] = 1
mask = np.stack((mask, mask, mask), axis=2)
# Blend images
temp = np.zeros_like(new_image[:, :, :3])
temp[(t-1):(h+t-1), (t-1):(w+t-1)] = image.copy()
new_image[:, :, :3] = new_image[:, :, :3] * (1 - mask) + temp * mask
# Set proper alpha channel in new image
temp = new_image[:, :, 3].copy()
new_image[:, :, 3] = cv2.floodFill(temp, None, (int(w/2+t), int(h/2+t)), 255)[1]
return new_image
img = cv2.imread('path/to/your/image.png')
cv2.imshow('img', img)
new_img = rect_with_rounded_corners(img, 50, 20, (0, 0, 0))
cv2.imshow('new_img', new_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
It's the same concept as used in my other answer with some more code on the correct transparency stuff.
Some exemplary input:
The corresponding output:
Another input and parameter set:
new_img = rect_with_rounded_corners(img, 20, 10, (0, 0, 128))
Output:
Hope that also helps!
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.8.1
NumPy: 1.18.1
OpenCV: 4.2.0
----------------------------------------
I fancied my hand at drawing rounded rectangles with SVG for a change - not least because somebody thinks I always use ImageMagick ;-)
#!/usr/bin/env python3
from PIL import ImageOps, Image
from cairosvg import svg2png
from io import BytesIO
def frame(im, thickness=5):
# Get input image width and height, and calculate output width and height
iw, ih = im.size
ow, oh = iw+2*thickness, ih+2*thickness
# Draw outer black rounded rect into memory as PNG
outer = f'<svg width="{ow}" height="{oh}" style="background-color:none"><rect rx="20" ry="20" width="{ow}" height="{oh}" fill="black"/></svg>'
png = svg2png(bytestring=outer)
outer = Image.open(BytesIO(png))
# Draw inner white rounded rect, offset by thickness into memory as PNG
inner = f'<svg width="{ow}" height="{oh}"><rect x="{thickness}" y="{thickness}" rx="20" ry="20" width="{iw}" height="{ih}" fill="white"/></svg>'
png = svg2png(bytestring=inner)
inner = Image.open(BytesIO(png)).convert('L')
# Expand original canvas with black to match output size
expanded = ImageOps.expand(im, border=thickness, fill=(0,0,0)).convert('RGB')
# Paste expanded image onto outer black border using inner white rectangle as mask
outer.paste(expanded, None, inner)
return outer
# Open image, frame it and save
im = Image.open('monsters.jpg')
result = frame(im, thickness=10)
result.save('result.png')
Output Image
Input Image
You can play with rx
and ry
to change the radius of the corners.
Here are outer
, inner
and expanded
- as you can see they are all the same size as each other for easy composing atop each other.
Other ideas:
with a 15x15 median filter, you get this:
Just in case anyone wants an ImageMagick solution:
#!/bin/bash
# Get width and height of input image
read iw ih < <(identify -format "%w %h" monsters.jpg)
# Calculate size of output image, assumes thickness=10
((ow=iw+20))
((oh=ih+20))
magick -size ${ow}x${oh} xc:none -fill black -draw "roundrectangle 0,0 $ow,$oh 20,20" \
\( -size ${iw}x${ih} xc:black -fill white -draw "roundrectangle 0,0,$iw,$ih 20,20" monsters.jpg -compose darken -composite \) \
-gravity center -compose over -composite result.png
Keywords: Python, image processing, round corners, rounded corners, border, SVG, cairo, cairosvg, SVG to PNG, SVG as PNG, SVG to PIL, PIL, Pillow.
Surely, Mark will provide a fancy solution using ImageMagick. But, since your question is tagged with Pillow, and other people might also looking for a solution, here's my manual implementation, because I doubt, that there's a ready in-built method for that:
from matplotlib import pyplot as plt # Just for visualization
from PIL import Image, ImageDraw
def rect_with_rounded_corners(image, r, t, c):
"""
:param image: PIL image, assumption: uni color filled rectangle
:param r: radius of rounded corners
:param t: thickness of border
:param c: color of border
:return: new PIL image of rectangle with rounded corners
"""
# Some method to extract the main color of the rectangle needed here ...
mc = img.getpixel((image.width/2, image.height/2))
# Create new image
new_image = Image.new(image.mode, (image.width + 2*t, image.height + 2*t), (255, 255, 255))
draw = ImageDraw.Draw(new_image)
# Draw four rounded corners
draw.arc([(0, 0), (2*r-1, 2*r-1)], 180, 270, c, t)
draw.arc([(image.width-2*r+2*t, 0), (image.width+2*t, 2*r-1)], 270, 0, c, t)
draw.arc([(image.width-2*r+2*t, image.height-2*r+2*t), (image.width+2*t, image.height+2*t)], 0, 90, c, t)
draw.arc([(0, image.height-2*r+2*t), (2*r-1, image.height+2*t)], 90, 180, c, t)
# Draw four edges
draw.line([(r-1, t/2-1), (image.width-r+2*t, t/2-1)], c, t)
draw.line([(t/2-1, r-1), (t/2-1, image.height-r+2*t)], c, t)
draw.line([(image.width+1.5*t, r-1), (image.width+1.5*t, image.height-r+2*t)], c, t)
draw.line([(r-1, image.height+1.5*t), (image.width-r+2*t, image.height+1.5*t)], c, t)
# Fill rectangle with main color
ImageDraw.floodfill(new_image, (image.width/2+t, image.height/2+t), mc)
return new_image
img = Image.new('RGB', (640, 480), (255, 128, 255))
plt.figure(1)
plt.imshow(img)
new_img = rect_with_rounded_corners(img, 100, 20, (0, 0, 0))
plt.figure(2)
plt.imshow(new_img)
plt.show()
Basically, it's calculating and manually drawing four arcs, four edges with the desired thickness and color of the border, and then flood filling the rectangle with the color of the initial rectangle. Put that in some method and re-use it as needed, so there's no mess in the main code.
For the stated image and parameter set, we get that output (Matplotlib figure here):
For another image and parameter set
img = Image.new('RGB', (400, 300), (0, 64, 255))
plt.figure(1)
plt.imshow(img)
new_img = rect_with_rounded_corners(img, 25, 10, (255, 0, 0))
plt.figure(2)
plt.imshow(new_img)
we get, for example:
Hope that helps!
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.8.1
Matplotlib: 3.2.0rc3
Pillow: 7.0.0
----------------------------------------
import
command: Matplotlib is just used for visualization here! The implementation itself is solely done using Pillow. –
Asleep Here is one more approach using Python/OpenCV. However, in this approach, the border will be inside the bounds of the input image.
Input:
import cv2
import numpy as np
# set thickness, rounding and color of border
t = 21
r = 21
c = (0,0,255)
# read image
img = cv2.imread("bear.png")
hh, ww = img.shape[0:2]
# create white image of size of input
white = np.full_like(img, (255,255,255))
# add black border of thickness
border = cv2.copyMakeBorder(white, t, t, t, t, borderType=cv2.BORDER_CONSTANT, value=(0,0,0))
# blur image by rounding amount as sigma
blur = cv2.GaussianBlur(border, (0,0), r, r)
# threshold blurred image
thresh1 = cv2.threshold(blur, 128, 255, cv2.THRESH_BINARY)[1]
# create thesh2 by eroding thresh1 by 2*t
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*t,2*t))
thresh2 = cv2.morphologyEx(thresh1, cv2.MORPH_ERODE, kernel, iterations=1)
# subtract the two thresholded images to make a border mask
mask = thresh1 - thresh2
# shave border mask by t
mask = mask[t:hh+t,t:ww+t]
# create colored image the same size as input
color = np.full_like(img, c)
# combine input and color with mask
result = cv2.bitwise_and(color, mask) + cv2.bitwise_and(img, 255-mask)
# add thresh1 as alpha channel
thresh1 = thresh1[t:hh+t,t:ww+t][:,:,0]
result = np.dstack([result,thresh1])
# write
cv2.imwrite("bear_thresh1.png", thresh1)
cv2.imwrite("bear_thresh2.png", thresh2)
cv2.imwrite("bear_mask.png", mask)
cv2.imwrite("bear_red_border.png", result)
# display it
cv2.imshow("IMAGE", img)
cv2.imshow("BORDER", border)
cv2.imshow("BLUR", blur)
cv2.imshow("THRESHOLD1", thresh1)
cv2.imshow("THRESHOLD2", thresh2)
cv2.imshow("MASK", mask)
cv2.imshow("RESULT", result)
cv2.waitKey(0)
Threshold 2 image:
Border Mask image:
Result image:
ADDITION
Here is a correction to the above that permits more varied thickness and radius values. Example uses thickness of 21 and radius of 81.
import cv2
import numpy as np
import skimage.exposure
# set thickness, rounding and color of border
t = 21
r = 81
c = (0,0,255)
# read image
img = cv2.imread("bear.png")
hh, ww = img.shape[0:2]
# create white image of size of input
white = np.full_like(img, (255,255,255))
# add black border of thickness r
border = cv2.copyMakeBorder(white, r,r,r,r, borderType=cv2.BORDER_CONSTANT, value=(0,0,0)).astype(np.float64)
# blur image by rounding amount as sigma
blur = cv2.GaussianBlur(border, (0,0), r, r)
# threshold blurred image
thresh1 = cv2.threshold(blur, 128, 255, cv2.THRESH_BINARY)[1]
# create thesh2 by eroding thresh1 by 2*t
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*t,2*t))
thresh2 = cv2.morphologyEx(thresh1, cv2.MORPH_ERODE, kernel, iterations=1)
# subtract the two thresholded images to make a border mask
mask = thresh1 - thresh2
# antialias
mask = mask.astype(np.float64)
smooth = cv2.GaussianBlur(mask, (0,0), sigmaX=3, sigmaY=3, borderType = cv2.BORDER_DEFAULT)
mask = skimage.exposure.rescale_intensity(smooth, in_range=(96,160), out_range=(0,255))
# shave border mask by r
mask = mask[r:hh+r,r:ww+r]
# create colored image the same size as input
color = np.full_like(img, c)
# combine input and color with mask
#result = cv2.bitwise_and(color, mask) + cv2.bitwise_and(img, 255-mask)
result = ((color*mask + img*(255-mask))/255).clip(0,255).astype(np.uint8)
# add thresh1 as alpha channel
thresh1 = thresh1[r:hh+r,r:ww+r][:,:,0]
result = np.dstack([result,thresh1])
# write
cv2.imwrite("bear2_thresh1.png", thresh1)
cv2.imwrite("bear2_thresh2.png", thresh2)
cv2.imwrite("bear2_mask.png", mask)
cv2.imwrite("bear2_red_border.png", result)
# display it
cv2.imshow("IMAGE", img)
cv2.imshow("BORDER", border)
cv2.imshow("BLUR", blur)
cv2.imshow("THRESHOLD1", thresh1)
cv2.imshow("THRESHOLD2", thresh2)
cv2.imshow("MASK", mask)
cv2.imshow("RESULT", result)
cv2.waitKey(0)
Result:
Here is a simple implementation only using pillow
:
from PIL import Image, ImageDraw
def add_round_border(
image, border_color=(232, 232, 232), border_radius=30, border_width=3
):
image = image.convert("RGBA")
# Create an out mask and an in mask
mask = Image.new("L", image.size, 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle(
[0, 0, image.size[0], image.size[1]], radius=border_radius, fill=255
)
mask_in = Image.new("L", image.size, 0)
draw = ImageDraw.Draw(mask_in)
draw.rounded_rectangle(
[
border_width,
border_width,
image.size[0] - border_width,
image.size[1] - border_width,
],
radius=border_radius - border_width,
fill=255,
)
border_image = Image.new("RGBA", image.size, color=border_color)
new_image = Image.new("RGBA", image.size, color=0)
# Add the border by pasting the border images onto the new image
new_image.paste(border_image, mask=mask)
new_image.paste(image, mask=mask_in)
return new_image
# Driver Code
image = Image.open("YOUR_IMAGE_PATH")
image_with_border = add_round_border(image, border_color="red", border_radius=30, border_width=15)
image.save("test.png")
image_with_border.save("test_with_border.png")
Before:
After:
© 2022 - 2024 — McMap. All rights reserved.