Skip to content Skip to sidebar Skip to footer

How To Add A Round Border Around An Image?

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 d

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:

Input #1

The corresponding output:

Output #1

Another input and parameter set:

Input #2

new_img = rect_with_rounded_corners(img, 20, 10, (0, 0, 128))

Output:

Output #2

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

enter image description here

Input Image

enter image description here

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.

enter image description here

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:

enter image description here

with a 15x15 median filter, you get this:

enter image description here


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

enter image description here

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):

Orange

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:

Blue

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:

enter image description here

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:

enter image description here

Threshold 2 image:

enter image description here

Border Mask image:

enter image description here

Result image:

enter image description here


Post a Comment for "How To Add A Round Border Around An Image?"