Draw outline of a text in an image

I need to draw an outline of text in an image. Consider the below image as an example.

Can anyone suggest me a direction to achieve this? I don't have experience in image processing. Any tools/lib or anything that can generate this kind of image will be helpful.

• Would a convex hull suffice? – Emre Jul 18 '14 at 0:38
• I have no experience in image processing, I have no idea about the concepts. – NEO Jul 18 '14 at 13:56

Here's a rudimentary answer using MATLAB that provides a good alternative to the convex hull as Emre has mentioned. The basic algorithm is the following:

1. Binarize the image and invert the text so white is text while black is background
2. Find the minimum spanning bounding boxes for each of the characters
3. Create a new output image that takes the co-ordinates of each of the bounding boxes and fills them in with white.
4. Do a morphological closing with a relatively large structuring square element to mash all of the text into a single object
5. Find the perimeter of this object - This is your boundary of your text

A small note: I had to play around with the size of my structuring element until the results looked right. You should choose the size of it based on how big the tallest letter is, and a few more pixels around the structuring element so that when you close, you are able to join neighbouring bounding boxes together. Also, as noted by Selim Arikan in the comment below, it is recommended to add a few pixels of dilation to the perimeter depending on the typeface size. I haven't done that in this implementation, but it's fairly simple to do.

Without further ado, here's my code, along with the example image that I used:

Code

% Clear all varibles and close all windows
clear all;
close all;

% Threshold and invert
im = ~im2bw(imorig);

% Find bounding boxes of all characters
s = regionprops(im, 'BoundingBox');
bbox = round(reshape([s.BoundingBox], 4, []).');

% Create a blank image and for each box, fill in with white
outImg = false(size(im));
for idx = 1 : size(bbox,1)
outImg(bbox(idx,2):bbox(idx,2)+bbox(idx,4), ...
bbox(idx,1):bbox(idx,1)+bbox(idx,3)) = true;
end

% Close with a very large structuring element
se = strel('square', 20);
outImg2 = imclose(outImg, se);

% Find the perimeter of this object, then take the original image and
% the pixels that belong to this boundary.  Colour all of these pixels red
% for illustration
redChan = imorig(:,:,1);
greenChan = imorig(:,:,2);
blueChan = imorig(:,:,3);
perim = bwperim(outImg2,8);
redChan(perim) = 255;
greenChan(perim) = 0;
blueChan(perim) = 0;
out = cat(3,redChan,greenChan,blueChan);

figure;
subplot(1,2,1);
imshow(imorig);
title('Original image');
subplot(1,2,2);
imshow(out);
title('Image with perimeter overlaid');


Edit: July 20th, 2014

Here is an OpenCV / Python implementation of what I did above. The main differences are:

1. The regionprops method in MATLAB does not exist in Python / OpenCV. However, there is a findContours method which provides a list of points that traces over every boundary of each of the letters in the image. This is essentially a list of a list of points... woah! Basically, this returns a list. For each element in this list, this contains a list of 2D points that trace around a particular object.
2. For each object, I find the minimum and maximum of the rows and columns and this denotes the minimum spanning bounding box for each object. I then fill in this box with white.
3. It's also a good idea where if an image serves as input into the function, you provide a copy of the image as the input to avoid shadowing and accidental overwriting. You use the .copy() method for each of these images.
4. I padded the boundaries of the image by 20 pixels to allow for a tighter boundary as per your request.

You will need the numpy, scipy and opencv modules before you run this method. If you need help in installing these modules, let me know in a comment.

Without further ado, here's the code.

import cv2 # For OpenCV modules (Morphology and Contour Finding)
import numpy as np # For general purpose array manipulation

# Create a new image that pads each side by 20 pixels
# This will allow the outlining of the text to be tighter
# Create an image of all white
imgPad = 255*np.ones((img.shape[0] + 40, img.shape[1] + 40),
dtype='uint8')

# Place the original image in the middle

# Invert image
# White becomes black and black becomes white
imgBW = 255*imgBW.astype('uint8')

# Find all of the contours in the image
contours,hierarchy = cv2.findContours(imgBW.copy(), cv2.RETR_LIST,
cv2.CHAIN_APPROX_NONE)

# New image that places square blocks over all letters
imgBlocks = np.zeros(imgBW.shape, dtype='uint8')

# For each contour...
for idx in range(len(contours)):
# Reshape each contour into a 2D array
# First co-ordinate is the column, second is the row
cnt = np.reshape(contours[idx], (contours[idx].shape[0],
contours[idx].shape[2]))
# Transpose to allow for max and min calls
cnt = cnt.T

# Find the max and min of each contour
maxCol = np.max(cnt[0])
minCol = np.min(cnt[0])
maxRow = np.max(cnt[1])
minRow = np.min(cnt[1])

# Use the previous to fill in a minimum spanning bounding
# box around each contour
for row in np.arange(minRow, maxRow+1):
for col in np.arange(minCol, maxCol+1):
imgBlocks[row,col] = 255

# Morphological closing on the image with a 20 x 20 structuring element
structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT, (20,20))
imgClose = cv2.morphologyEx(imgBlocks.copy(), cv2.MORPH_CLOSE, structuringElement)

# Find the contour of this structure
contoursFinal,hierarchyFinal = cv2.findContours(imgClose.copy(), cv2.RETR_LIST,
cv2.CHAIN_APPROX_NONE)

# Take the padded image, and draw a red boundary around the font
# First, create a colour image

# Reshape the contour points like we did before
cnt = np.reshape(contoursFinal[0], (contoursFinal[0].shape[0],
contoursFinal[0].shape[2]))

# Careful - pixels are packed in BGR format
# As such, for each point in the outer shape, set to red
for (col,row) in cnt:

# Crop out image for final one

# Show both images
cv2.imshow('Original Image', img)
cv2.imshow('Image with red outline over text', imgFinal)
cv2.waitKey(0)
cv2.destroyAllWindows()


The output will show you two windows. One of the original image, and the other is the output. You'll need to move one of the windows out of the way, as they will appear one on top of the other. Note that you need to download the images to your computer before you run this method.

• Additionally, it would be better to add a few pixels of dilation to the perimeter depending on the typeface size. – Tyathalae Jul 18 '14 at 7:24
• @SelimArikan - Nice! I'll add that into my edit. Nice spot. – rayryeng Jul 18 '14 at 7:25
• Thank you for this. If possible can you also give me any example in python? I am a python/java coder, since I have no experience in image processing, I would at least be comfortable if its in python/java. – NEO Jul 18 '14 at 14:00
• @thefragmenter Sure! Which platform do you prefer? I can code in either one. Btw python will be easier – rayryeng Jul 18 '14 at 14:14
• Then python is ok for me. – NEO Jul 18 '14 at 14:31

You just need to binarize the image then find the hull. In Mathematica it looks like this:

<< ComputationalGeometry
img = EdgeDetect@Rasterize["managers. In\n more lean\n result of\nss\nkforce and\nt \
points and"]
pts = Position[ImageData@img, 1]
pgp = PlanarGraphPlot[pts, ConvexHull@pts][[1, 2]]
Graphics[{Black, Point@pts, pgp}]
`

If this is inadequate I would find the convex hull of the text two lines at a time, then fuse the results.