Adding a Border to the Image using NumPy

Adding a Border to the Image using NumPy

Introduction

In this article, we will learn how to add/draw a border to the image. We use OpenCV to read the image. The rest of the part is handled by NumPy to code from scratch. We rely on it for the matrix operations that can be achieved with so much ease.

There are two aspects in which we can start to think:

  • If the image is read in grayscale, we can simply keep the default color as black. This is because the length of the shape of the image matrix would be 2. Therefore we cannot add a color border whose color value would be of size 3 and thus it cannot be mapped easily.
  • If the image is read in RGB, we can have a choice to pick the color for the border. This is because the length of the shape of the image matrix would be 3. Hence we can add a color border whose color value would be of size 3 which can be mapped easily.

Credits of Cover Image - Photo by Kanan Khasmammadov on Unsplash

Before proceeding further, we need to make sure we have enough colors (based on the user’s choice). I have extracted the possible color values from this website.

The code of the same can be seen below. The result is stored in a JSON file named color_names_data.json.

import requests
import json
from bs4 import BeautifulSoup

def extract_table(url):
    res = requests.get(url=url)
    con = res.text
    soup = BeautifulSoup(con, features='lxml')
    con_table = soup.find('table', attrs={'class' : 'color-list'})
    headings = [th.get_text().lower() for th in con_table.find("tr").find_all("th")]
    table_rows = [headings]
    for row in con_table.find_all("tr")[1:]:
        each_row = [td.get_text().lower() for td in row.find_all("td")]
        table_rows.append(each_row)
    return table_rows

col_url = "https://www.colorhexa.com/color-names"
color_rows_ = extract_table(url=col_url)

color_dict = {}
for co in color_rows_[1:]:
    color_dict[co[0]] = {
        'r' : int(co[2]),
        'g' : int(co[3]),
        'b' : int(co[4]),
        'hex' : co[1]
    }

with open(file='color_names_data.json', mode='w') as col_json:
    json.dump(obj=color_dict, fp=col_json, indent=2)

It is necessary to grab R, G, and B values in order for the mapping after the separation of pixels. We follow split and merge methods using NumPy.

The structure of the color data can be seen below.

{
  "air force blue": {
    "r": 93,
    "g": 138,
    "b": 168,
    "hex": "#5d8aa8"
  },
  "alizarin crimson": {
    "r": 227,
    "g": 38,
    "b": 54,
    "hex": "#e32636"
  },
  "almond": {
    "r": 239,
    "g": 222,
    "b": 205,
    "hex": "#efdecd"
  },
  ...
}

Time to Code

The packages that we mainly use are:

  • NumPy
  • Matplotlib
  • OpenCV

python_packages.png

Import the Packages

import numpy as np
import cv2
import json
from matplotlib import pyplot as plt

Read the Image

def read_this(image_file, gray_scale=False):
    image_src = cv2.imread(image_file)
    if gray_scale:
        image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
    else:
        image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
    return image_src

The above function reads the image either in grayscale or RGB and returns the image matrix.

Code Implementation with Library

For adding/drawing borders around the image, the important arguments can be named as below:

  • image_file → Image file location or image name if the image is stored in the same directory.
  • bt → Border thickness
  • color_name → By default it takes 0 (black color). Otherwise, any color name can be taken.

We use the method copyMakeBorder() available in the library OpenCV that is used to create a new bordered image with a specified thickness. In the code, we make sure to convert the color name into values from the color data we collected earlier.

The below function works for both RGB image and grayscale image as expected.

def add_border(image_file, bt=5, with_plot=False, gray_scale=False, color_name=0):
    image_src = read_this(image_file=image_file, gray_scale=gray_scale)
    if gray_scale:
        color_name = 0
        value = [color_name for i in range(3)]
    else:
        color_name = str(color_name).strip().lower()

        with open(file='color_names_data.json', mode='r') as col_json:
            color_db = json.load(fp=col_json)

        colors_list = list(color_db.keys())
        if color_name not in colors_list:
            value = [0, 0, 0]
        else:
            value = [color_db[color_name][i] for i in 'rgb']

    image_b = cv2.copyMakeBorder(image_src, bt, bt, bt, bt, cv2.BORDER_CONSTANT, value=value)

    if with_plot:
        cmap_val = None if not gray_scale else 'gray'
        fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))

        ax1.axis("off")
        ax1.title.set_text('Original')

        ax2.axis("off")
        ax2.title.set_text("Bordered")

        ax1.imshow(image_src, cmap=cmap_val)
        ax2.imshow(image_b, cmap=cmap_val)
        return True
    return image_b

Let’s test the above function —

add_border(image_file='lena_original.png', with_plot=True, color_name='green')

borderlc_lena.png

add_border(image_file='lena_original.png', with_plot=True, gray_scale=True, color_name='pink')

borderlg_lena.png

We can clearly see the borders are been added/drawn. For the grayscale image, though we have mentioned pink, a black border is drawn.

Code Implementation from Scratch

When we talk about the border, it is basically a constant pixel value of one color around the entire image. It is important to take note of the thickness of the border to be able to see. Considering the thickness we should append a constant value around the image that matches the thickness level.

In order to do so, we can use the pad() method available in the library NumPy. This method appends a constant value that matches the level of the pad_width argument which is mentioned.

Example

>>> import numpy as np
>>> m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> m = np.array(m)
>>> m
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>>
>>> pm = np.pad(array=m, pad_width=1, mode='constant', constant_values=12)
>>> pm
array([[12, 12, 12, 12, 12],
       [12,  1,  2,  3, 12],
       [12,  4,  5,  6, 12],
       [12,  7,  8,  9, 12],
       [12, 12, 12, 12, 12]])
>>>
>>> pmm = np.pad(array=m, pad_width=2, mode='constant', constant_values=24)
>>> pmm
array([[24, 24, 24, 24, 24, 24, 24],
       [24, 24, 24, 24, 24, 24, 24],
       [24, 24,  1,  2,  3, 24, 24],
       [24, 24,  4,  5,  6, 24, 24],
       [24, 24,  7,  8,  9, 24, 24],
       [24, 24, 24, 24, 24, 24, 24],
       [24, 24, 24, 24, 24, 24, 24]])
>>>

More examples can be found in the documentation.

We just need to change the constant_values argument by taking the actual color value.

  • For a grayscale image, we simply pad the image matrix with black color i.e., 0.
  • For the RGB image, we are to grab the color value from the data collected, split the image into 3 matrices, and pad each matrix with each color value. The below function would explain the flow more clearly.

The below function would explain the flow more clearly.

def draw_border(image_file, bt=5, with_plot=False, gray_scale=False, color_name=0):
    image_src = read_this(image_file=image_file, gray_scale=gray_scale)    
    if gray_scale:
        color_name = 0
        image_b = np.pad(array=image_src, pad_width=bt, mode='constant', constant_values=color_name)
        cmap_val = 'gray'
    else:
        color_name = str(color_name).strip().lower()

        with open(file='color_names_data.json', mode='r') as col_json:
            color_db = json.load(fp=col_json)

        colors_list = list(color_db.keys())
        if color_name not in colors_list:
            r_cons, g_cons, b_cons = [0, 0, 0]
        else:
            r_cons, g_cons, b_cons = [color_db[color_name][i] for i in 'rgb']

        r_, g_, b_ = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
        rb = np.pad(array=r_, pad_width=bt, mode='constant', constant_values=r_cons)
        gb = np.pad(array=g_, pad_width=bt, mode='constant', constant_values=g_cons)
        bb = np.pad(array=b_, pad_width=bt, mode='constant', constant_values=b_cons)

        image_b = np.dstack(tup=(rb, gb, bb))
        cmap_val = None

    if with_plot:
        fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))

        ax1.axis("off")
        ax1.title.set_text('Original')

        ax2.axis("off")
        ax2.title.set_text("Bordered")

        ax1.imshow(image_src, cmap=cmap_val)
        ax2.imshow(image_b, cmap=cmap_val)
        return True
    return image_b

Let’s test the above function —

draw_border(image_file='lena_original.png', with_plot=True, color_name='cyan')

bordersc_lena.png

draw_border(image_file='lena_original.png', bt=10, with_plot=True, gray_scale=True)

bordersg_lena.png

Yay! We did it. We coded the entire thing including color choice completely from scratch except for the part where we read the image file. We relied mostly on NumPy as it is very fast in computing matrix operations (We could have done it with for loops if we wanted our code to execute very slow).

Personally, this was a great learning for me. I am starting to think about how difficult and fun that would be for the people who actually work on open source libraries.

You should definitely check out my other articles on the same subject in my profile.


If you liked it, you can buy coffee for me from here.

bmc-button.png