How To Add A Round Border Around An Image?
Solution 1:
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
----------------------------------------
Solution 2:
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:
- You can also create a rounded corner by drawing a white rectangle in a black box and running a median filter, or some morphological erosion, over it. If you filter this:
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.
Solution 3:
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
----------------------------------------
Solution 4:
Here is one more approach using Python/OpenCV. However, in this approach, the border will be inside the bounds of the input image.
- Read the input
- Create a white image of the size of the input
- Pad the white image with black all around of the desired border thickness
- Apply Gaussian blur to the padded image
- Threshold the blurred image to form a binary image
- Erode the thresholded image to form a second binary image
- Get the difference between the two binary images to form the border shaped mask
- Shave the border mask by the thickness to get it back to size of the input image
- Create a color image the size of the input
- Combine the input and the color image using the mask
- Put the first thresholded image into the alpha channel of the combined image to make the outside transparent
- Save the results
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 1 image:
Threshold 2 image:
Border Mask image:
Result image:
Post a Comment for "How To Add A Round Border Around An Image?"