import cv2
import numpy as np
import pandas as pd
from google.colab.patches import cv2_imshow
Calculating the Ratio of Corners in an Image
Background
In this notebook I’ll walk through an algorithm suggested by Claude to distinguish one typeface (like display
) from another (like serif
) in which we calculate the ratio of the number of corners to number of pixels in a text image.
This algorithm is part of my exploration of non-ML baselines to classify text images into various typeface categories (e.g., “humanist sans,” “grotesque sans,” “script,” “display,” etc.). Once the non-ML baseline is established, I’ll train a neural network for this task. This is one of many notebooks in my TypefaceClassifier project series.
Load Image and Binarize It
As usual, we load the image and binarize it so it’s easier to distinguish between background (black pixels) and text (white pixels).
= 'serif-76px.png'
path = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
_, binary binary
ndarray (512, 512)
array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
Detect Corners
I’ll use the Harris Corner Detection algorithm to identify the pixels at which there exists a corner in the image.
= cv2.cornerHarris(binary, blockSize=2, ksize=3, k=0.04) corners
Dilating the corners makes them brighter
= cv2.dilate(corners, None) corners
corners.shape
(512, 512)
5] corners[:
array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]], dtype=float32)
threshold
ing the pixels so that pixels with a corners
value above 1% of the maximum are set to white (255).
= cv2.threshold(corners, 0.01 * corners.max(), 255, 0) _, corners
The end result is a set of pixels that are the locations of corners in the image. Notice how the straight segments of the letters are (correctly) not identified as corners, while curves are identified as corners.
cv2_imshow(corners)
Calculating the Ratio of Corner Pixels to Total Pixels
The number of corners are the number of corners
pixels that have a value greater than 0
.
sum(corners > 0) np.
15127
The ratio of number of corners to pixels in an image is given as the ratio of the number of corners
pixels greater than 0
divided by the number of binary
pixels greater than 0
. In this case, 67% of the pixels represent corners.
sum(corners > 0), np.sum(binary > 0), np.sum(corners > 0) / np.sum(binary > 0) np.
(15127, 22475, 0.6730589543937708)
For a different image, notably display
(sans serif) text of the same font-size (76px), this ratio is considerably smaller (39% < 67%).
= 'display-76px.png'
path = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
_, binary binary
ndarray (512, 512)
array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
= cv2.cornerHarris(binary, blockSize=2, ksize=3, k=0.04)
corners = cv2.dilate(corners, None)
corners = cv2.threshold(corners, 0.01 * corners.max(), 255, 0)
_, corners sum(corners > 0), np.sum(binary > 0), np.sum(corners > 0) / np.sum(binary > 0) np.
(13650, 34992, 0.3900891632373114)
Visually, you can see how much fewer corners are detected in this image than in the image with serif text—this makes sense! Serifs are intricate flourishes added to the ends of letters and contain more changes in stroke (corners).
cv2_imshow(corners)
Calculating Contour Ratio for Different Images
I’ll now wrap the code above into a function and apply it to a wide variety of images (of two typefaces, display
and serif
and 8 different font sizes). I expect, on average, the serif
images to have a higher corner ratio than display
images, since serifs introduce more corners.
def corner_ratio(path):
= cv2.imread(path, cv2.IMREAD_GRAYSCALE)
img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
_, binary
= cv2.cornerHarris(binary, blockSize=2, ksize=3, k=0.04)
corners
= cv2.dilate(corners, None)
corners = cv2.threshold(corners, 0.01 * corners.max(), 255, 0)
_, corners = np.uint8(corners)
corners
= np.sum(corners > 0)
num_corners = np.sum(binary > 0)
total_pixels
= num_corners / total_pixels if total_pixels > 0 else 0
corner_ratio
return corner_ratio
On average, images with serif
fonts have more corners present.
= [8, 18, 24, 36, 76, 240, 330, 420]
szs = ['display', 'serif']
ts = []
res
for t in ts:
for sz in szs:
= f"{t}-{sz}px.png"
image_path = corner_ratio(image_path)
sr
res.append([t, sz, sr])
= pd.DataFrame(res, columns=['typeface', 'font-size', 'corner-ratio'])
res 'typeface')['corner-ratio'].agg(['mean', 'median']) res.groupby(
mean | median | |
---|---|---|
typeface | ||
display | 0.989564 | 0.734144 |
serif | 1.287083 | 1.134465 |
This trend holds up for each font size:
='font-size') res.sort_values(by
typeface | font-size | corner-ratio | |
---|---|---|---|
0 | display | 8 | 2.827288 |
8 | serif | 8 | 2.874287 |
1 | display | 18 | 1.784839 |
9 | serif | 18 | 2.573949 |
2 | display | 24 | 1.609162 |
10 | serif | 24 | 2.192324 |
3 | display | 36 | 1.078198 |
11 | serif | 36 | 1.595871 |
4 | display | 76 | 0.390089 |
12 | serif | 76 | 0.673059 |
5 | display | 240 | 0.113082 |
13 | serif | 240 | 0.173800 |
6 | display | 330 | 0.061339 |
14 | serif | 330 | 0.123992 |
7 | display | 420 | 0.052513 |
15 | serif | 420 | 0.089383 |
Final Thoughts
Similar to the contour ratio algorithm there is a clear and consistent difference in value between serif and sans serif fonts for this corner ratio algorithm, making this a good candidate for distinguishing between typefaces.
This is also another relatively simple algorithm, and each step can be easily visualized.
I hope you enjoyed this blog post! Follow me on Twitter @vishal_learner.