Shape Analysis
Shape Analysis¶
Overview¶
Learn methods for analyzing and classifying characteristics of shapes extracted from contours. This lesson covers various shape analysis techniques including moments, centroids, bounding shapes, convex hulls, and shape matching.
Table of Contents¶
- Image Moments
- Centroid Calculation
- Bounding Rectangles
- Minimum Enclosing Shapes
- Convex Hull
- Shape Matching
- Shape Classification System
- Practice Problems
1. Image Moments¶
What are Moments?¶
Image Moments:
Feature values calculated as weighted averages of image pixel values
Mathematical Definition:
Mij = Σ Σ x^i × y^j × I(x, y)
- M00: Area (0th moment)
- M10, M01: 1st moments (for centroid calculation)
- M20, M02, M11: 2nd moments (orientation, variance)
Applications:
- Area and perimeter calculation
- Centroid
- Orientation
- Ellipse fitting
- Hu Moments - invariant features
cv2.moments() Function¶
import cv2
import numpy as np
def calculate_moments(image_path):
"""Calculate image moments"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for i, contour in enumerate(contours):
# Calculate moments
M = cv2.moments(contour)
print(f"Contour {i}:")
print(f" M00 (area): {M['m00']:.0f}")
print(f" M10: {M['m10']:.0f}")
print(f" M01: {M['m01']:.0f}")
# Centroid
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
print(f" Centroid: ({cx}, {cy})")
# Central Moments
print(f" mu20: {M['mu20']:.0f}")
print(f" mu11: {M['mu11']:.0f}")
print(f" mu02: {M['mu02']:.0f}")
# Normalized Central Moments
print(f" nu20: {M['nu20']:.6f}")
print(f" nu11: {M['nu11']:.6f}")
print(f" nu02: {M['nu02']:.6f}")
calculate_moments('shapes.jpg')
Types of Moments¶
Spatial Moments:
m00, m10, m01, m20, m11, m02, m30, m21, m12, m03
┌─────────────────────────────────────────────────────────┐
│ m00 = Σ I(x,y) → Area (white pixels count) │
│ m10 = Σ x × I(x,y) → Sum of x coordinates │
│ m01 = Σ y × I(x,y) → Sum of y coordinates │
└─────────────────────────────────────────────────────────┘
Central Moments:
mu20, mu11, mu02, mu30, mu21, mu12, mu03
┌─────────────────────────────────────────────────────────┐
│ Calculated relative to centroid │
│ mu20 = Σ (x - cx)² × I(x,y) │
│ → Translation Invariant │
└─────────────────────────────────────────────────────────┘
Normalized Central Moments:
nu20, nu11, nu02, nu30, nu21, nu12, nu03
┌─────────────────────────────────────────────────────────┐
│ nuij = muij / m00^((i+j)/2 + 1) │
│ → Translation + Scale Invariant │
└─────────────────────────────────────────────────────────┘
Hu Moments¶
import cv2
import numpy as np
def hu_moments_analysis(contour):
"""Calculate and analyze Hu moments"""
# Regular moments
M = cv2.moments(contour)
# Hu moments (7 invariant features)
huMoments = cv2.HuMoments(M)
# Log scale transformation (easier to compare)
huMoments_log = -np.sign(huMoments) * np.log10(np.abs(huMoments) + 1e-10)
print("Hu Moments:")
for i, h in enumerate(huMoments_log.flatten()):
print(f" h{i+1}: {h:.4f}")
return huMoments
def compare_shapes_hu(contour1, contour2):
"""Compare two shapes using Hu moments"""
hu1 = cv2.HuMoments(cv2.moments(contour1)).flatten()
hu2 = cv2.HuMoments(cv2.moments(contour2)).flatten()
# Log scale transformation
hu1_log = -np.sign(hu1) * np.log10(np.abs(hu1) + 1e-10)
hu2_log = -np.sign(hu2) * np.log10(np.abs(hu2) + 1e-10)
# Euclidean distance
distance = np.linalg.norm(hu1_log - hu2_log)
return distance
# Usage example
img = cv2.imread('shapes.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) >= 2:
dist = compare_shapes_hu(contours[0], contours[1])
print(f"Shape similarity distance: {dist:.4f}")
# The smaller the distance, the more similar the shapes
2. Centroid Calculation¶
Centroid Formula¶
Centroid:
The center of mass of a shape
cx = M10 / M00
cy = M01 / M00
(x1,y1)
*
/ \
/ \
/ • \ ← (cx, cy) centroid
/ \
*---------*
(x2,y2) (x3,y3)
Characteristics:
- Always located inside the shape
- Maintains relative position regardless of rotation or scale
Centroid Calculation and Visualization¶
import cv2
import numpy as np
def find_centroids(image_path):
"""Find centroids of all contours"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
centroids = []
for contour in contours:
# Calculate moments
M = cv2.moments(contour)
# Only if area is non-zero
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
centroids.append((cx, cy))
# Draw contour
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
# Mark centroid
cv2.circle(result, (cx, cy), 5, (0, 0, 255), -1)
cv2.putText(result, f'({cx},{cy})', (cx + 10, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
cv2.imshow('Centroids', result)
cv2.waitKey(0)
return centroids
centroids = find_centroids('shapes.jpg')
print(f"Centroid coordinates: {centroids}")
Shape Orientation Calculation¶
import cv2
import numpy as np
def calculate_orientation(contour):
"""Calculate the major axis orientation of a shape"""
M = cv2.moments(contour)
if M['m00'] == 0:
return None, None
# Centroid
cx = M['m10'] / M['m00']
cy = M['m01'] / M['m00']
# Calculate orientation using 2nd central moments
# theta = 0.5 * arctan(2 * mu11 / (mu20 - mu02))
if (M['mu20'] - M['mu02']) != 0:
theta = 0.5 * np.arctan2(2 * M['mu11'], (M['mu20'] - M['mu02']))
else:
theta = 0
return (cx, cy), theta
def draw_orientation(image, contour):
"""Display shape orientation with an arrow"""
result = image.copy()
center, theta = calculate_orientation(contour)
if center is None:
return result
cx, cy = int(center[0]), int(center[1])
# Major axis arrow
length = 50
dx = int(length * np.cos(theta))
dy = int(length * np.sin(theta))
# Contour
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
# Centroid
cv2.circle(result, (cx, cy), 5, (255, 0, 0), -1)
# Direction arrow
cv2.arrowedLine(result, (cx, cy), (cx + dx, cy + dy),
(0, 0, 255), 2, tipLength=0.3)
# Display angle
angle_deg = np.degrees(theta)
cv2.putText(result, f'{angle_deg:.1f} deg', (cx + 10, cy - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
return result
# Usage example
img = cv2.imread('elongated_shape.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
result = draw_orientation(img, contours[0])
cv2.imshow('Orientation', result)
cv2.waitKey(0)
3. Bounding Rectangles¶
cv2.boundingRect()¶
Bounding Rectangle:
Minimum upright rectangle that completely encloses a contour
┌───────────────┐
│ ╱╲ │
│ ╱ ╲ │ (x, y): top-left
│ ╱ ╲ │ w: width
│ ╲ ╱ │ h: height
│ ╲ ╱ │
│ ╲╱ │
└───────────────┘
import cv2
import numpy as np
def bounding_rect_example(image_path):
"""Bounding rectangle example"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
# Bounding rectangle
x, y, w, h = cv2.boundingRect(contour)
# Draw rectangle
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
# Display information
aspect_ratio = w / h
extent = cv2.contourArea(contour) / (w * h)
print(f"Position: ({x}, {y})")
print(f"Size: {w} x {h}")
print(f"Aspect ratio: {aspect_ratio:.2f}")
print(f"Extent: {extent:.2f}") # Actual area / rectangle area ratio
cv2.imshow('Bounding Rectangle', result)
cv2.waitKey(0)
bounding_rect_example('shapes.jpg')
cv2.minAreaRect() - Rotated Bounding Rectangle¶
Rotated Bounding Rectangle:
Smallest rotated rectangle that encloses the shape
╱╲
╱ ╲
╱ ╲
╱──────╲
╱ ╲
╲──────────╱
Return value: ((cx, cy), (w, h), angle)
- (cx, cy): center point
- (w, h): width, height
- angle: rotation angle
import cv2
import numpy as np
def min_area_rect_example(image_path):
"""Minimum area rotated rectangle"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
# Minimum area rotated rectangle
rect = cv2.minAreaRect(contour)
center, size, angle = rect
print(f"Center: {center}")
print(f"Size: {size}")
print(f"Angle: {angle:.1f}")
# Calculate corner coordinates
box = cv2.boxPoints(rect)
box = np.int_(box)
# Draw rectangle
cv2.drawContours(result, [box], 0, (0, 255, 0), 2)
# Mark center point
cv2.circle(result, (int(center[0]), int(center[1])), 5, (0, 0, 255), -1)
cv2.imshow('Min Area Rect', result)
cv2.waitKey(0)
min_area_rect_example('rotated_shapes.jpg')
4. Minimum Enclosing Shapes¶
cv2.minEnclosingCircle()¶
import cv2
import numpy as np
def min_enclosing_circle(image_path):
"""Minimum enclosing circle"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
# Minimum enclosing circle
(x, y), radius = cv2.minEnclosingCircle(contour)
center = (int(x), int(y))
radius = int(radius)
# Draw circle
cv2.circle(result, center, radius, (0, 255, 0), 2)
cv2.circle(result, center, 3, (0, 0, 255), -1)
# Area ratio (indirect circularity measure)
contour_area = cv2.contourArea(contour)
circle_area = np.pi * radius * radius
fill_ratio = contour_area / circle_area
print(f"Center: {center}, Radius: {radius}")
print(f"Fill ratio: {fill_ratio:.2f}")
cv2.imshow('Min Enclosing Circle', result)
cv2.waitKey(0)
min_enclosing_circle('shapes.jpg')
cv2.fitEllipse()¶
import cv2
import numpy as np
def fit_ellipse_example(image_path):
"""Ellipse fitting"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
# fitEllipse requires at least 5 points
if len(contour) >= 5:
# Fit ellipse
ellipse = cv2.fitEllipse(contour)
center, axes, angle = ellipse
print(f"Center: {center}")
print(f"Axes: {axes}") # (major axis, minor axis)
print(f"Angle: {angle:.1f}")
# Draw ellipse
cv2.ellipse(result, ellipse, (0, 255, 0), 2)
# Center point
cv2.circle(result, (int(center[0]), int(center[1])), 3, (0, 0, 255), -1)
cv2.imshow('Fitted Ellipse', result)
cv2.waitKey(0)
fit_ellipse_example('ellipse_shapes.jpg')
Comparing Bounding Shapes¶
import cv2
import numpy as np
import matplotlib.pyplot as plt
def compare_bounding_shapes(image_path):
"""Compare various bounding shapes"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
if cv2.contourArea(contour) < 100:
continue
# 1. Bounding rectangle (blue)
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(result, (x, y), (x+w, y+h), (255, 0, 0), 2)
# 2. Rotated bounding rectangle (green)
rect = cv2.minAreaRect(contour)
box = cv2.boxPoints(rect)
box = np.int_(box)
cv2.drawContours(result, [box], 0, (0, 255, 0), 2)
# 3. Minimum enclosing circle (red)
(cx, cy), radius = cv2.minEnclosingCircle(contour)
cv2.circle(result, (int(cx), int(cy)), int(radius), (0, 0, 255), 2)
# 4. Fitted ellipse (yellow)
if len(contour) >= 5:
ellipse = cv2.fitEllipse(contour)
cv2.ellipse(result, ellipse, (0, 255, 255), 2)
# Legend
cv2.putText(result, 'Blue: Bounding Rect', (10, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
cv2.putText(result, 'Green: Min Area Rect', (10, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
cv2.putText(result, 'Red: Min Enclosing Circle', (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
cv2.putText(result, 'Yellow: Fitted Ellipse', (10, 80),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
cv2.imshow('Bounding Shapes', result)
cv2.waitKey(0)
compare_bounding_shapes('shapes.jpg')
5. Convex Hull¶
cv2.convexHull()¶
Convex Hull:
Smallest convex polygon that encloses a set of points
→ Like wrapping with a rubber band
• • •
• •
• Original •
• •
• • • •
┌──────┐
│ │
│ Convex │
│ Hull │
└────────┘
import cv2
import numpy as np
def convex_hull_example(image_path):
"""Convex hull example"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
for contour in contours:
# Convex hull
hull = cv2.convexHull(contour)
# Original contour (green)
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
# Convex hull (red)
cv2.drawContours(result, [hull], 0, (0, 0, 255), 2)
# Compare areas (Solidity)
contour_area = cv2.contourArea(contour)
hull_area = cv2.contourArea(hull)
solidity = contour_area / hull_area if hull_area > 0 else 0
print(f"Contour area: {contour_area:.0f}")
print(f"Convex hull area: {hull_area:.0f}")
print(f"Solidity: {solidity:.2f}")
# Solidity close to 1 indicates convex shape
cv2.imshow('Convex Hull', result)
cv2.waitKey(0)
convex_hull_example('star_shape.jpg')
Convexity Defects¶
Convexity Defects:
Concave parts between contour and convex hull
→ Used for finger detection
╱╲
╱ ╲
start end
╲ ╱
╲╱ ← far (deepest point)
Return value: [start, end, far, depth]
- start: starting point index
- end: ending point index
- far: deepest point index
- depth: depth (divide by 256 for use)
import cv2
import numpy as np
def convexity_defects_example(image_path):
"""Detect convexity defects (finger counting)"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if not contours:
return
# Select largest contour
contour = max(contours, key=cv2.contourArea)
result = img.copy()
# Convex hull (return indices)
hull = cv2.convexHull(contour, returnPoints=False)
# Convexity defects
defects = cv2.convexityDefects(contour, hull)
if defects is None:
return
# Analyze defects
finger_count = 0
for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(contour[s][0])
end = tuple(contour[e][0])
far = tuple(contour[f][0])
# Convert depth (divide by 256)
depth = d / 256.0
# Only count if depth is above threshold (space between fingers)
if depth > 20:
finger_count += 1
# Visualization
cv2.circle(result, far, 5, (0, 0, 255), -1)
cv2.line(result, start, far, (0, 255, 0), 2)
cv2.line(result, far, end, (0, 255, 0), 2)
# Number of fingers = defects count + 1
print(f"Finger count: {finger_count + 1}")
# Contour
cv2.drawContours(result, [contour], 0, (255, 0, 0), 2)
cv2.imshow('Convexity Defects', result)
cv2.waitKey(0)
convexity_defects_example('hand.jpg')
6. Shape Matching¶
cv2.matchShapes()¶
Shape Matching:
Compare similarity of two contours (based on Hu moments)
cv2.matchShapes(contour1, contour2, method, parameter)
method:
- cv2.CONTOURS_MATCH_I1: Σ|1/mA - 1/mB|
- cv2.CONTOURS_MATCH_I2: Σ|mA - mB|
- cv2.CONTOURS_MATCH_I3: Σ|mA - mB| / |mA|
Return value: smaller is more similar (0 = identical)
import cv2
import numpy as np
def shape_matching_example():
"""Shape matching example"""
# Create template shape
template = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(template, (100, 100), 80, 255, -1)
# Test shapes
shapes = {
'circle': cv2.circle(np.zeros((200, 200), dtype=np.uint8),
(100, 100), 60, 255, -1),
'ellipse': cv2.ellipse(np.zeros((200, 200), dtype=np.uint8),
(100, 100), (80, 50), 0, 0, 360, 255, -1),
'square': cv2.rectangle(np.zeros((200, 200), dtype=np.uint8),
(30, 30), (170, 170), 255, -1),
}
# Template contour
contours_t, _ = cv2.findContours(template, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
template_contour = contours_t[0]
print("Shape matching results (lower is more similar):")
for name, shape in shapes.items():
contours_s, _ = cv2.findContours(shape, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours_s:
match = cv2.matchShapes(template_contour, contours_s[0],
cv2.CONTOURS_MATCH_I1, 0)
print(f" {name}: {match:.4f}")
shape_matching_example()
Template-based Shape Detection¶
import cv2
import numpy as np
def find_similar_shapes(image_path, template_path, threshold=0.1):
"""Find shapes similar to template"""
# Load template
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
_, template_bin = cv2.threshold(template, 127, 255, cv2.THRESH_BINARY)
template_contours, _ = cv2.findContours(
template_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
template_contour = max(template_contours, key=cv2.contourArea)
# Target image
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
similar_shapes = []
for contour in contours:
if cv2.contourArea(contour) < 100:
continue
# Shape matching
match = cv2.matchShapes(
template_contour, contour, cv2.CONTOURS_MATCH_I1, 0
)
if match < threshold:
similar_shapes.append(contour)
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
# Display matching score
M = cv2.moments(contour)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
cv2.putText(result, f'{match:.3f}', (cx, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
print(f"Number of similar shapes: {len(similar_shapes)}")
cv2.imshow('Similar Shapes', result)
cv2.waitKey(0)
return similar_shapes
# Usage example
find_similar_shapes('shapes.jpg', 'template_circle.jpg', threshold=0.15)
7. Shape Classification System¶
Comprehensive Shape Classifier¶
import cv2
import numpy as np
class ShapeClassifier:
"""Shape classifier"""
def __init__(self):
self.shape_names = {
3: 'Triangle',
4: 'Rectangle',
5: 'Pentagon',
6: 'Hexagon'
}
def classify(self, contour):
"""Classify shape from contour"""
# Basic properties
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
if area < 100 or perimeter == 0:
return None, {}
# Polygon approximation
approx = cv2.approxPolyDP(contour, 0.04 * perimeter, True)
vertices = len(approx)
# Bounding rectangle
x, y, w, h = cv2.boundingRect(contour)
aspect_ratio = w / h
# Circularity
circularity = 4 * np.pi * area / (perimeter ** 2)
# Solidity
hull = cv2.convexHull(contour)
hull_area = cv2.contourArea(hull)
solidity = area / hull_area if hull_area > 0 else 0
# Classification
properties = {
'vertices': vertices,
'area': area,
'perimeter': perimeter,
'aspect_ratio': aspect_ratio,
'circularity': circularity,
'solidity': solidity
}
# Shape identification
if vertices == 3:
shape = 'Triangle'
elif vertices == 4:
if 0.95 <= aspect_ratio <= 1.05:
shape = 'Square'
else:
shape = 'Rectangle'
elif vertices == 5:
shape = 'Pentagon'
elif vertices == 6:
shape = 'Hexagon'
elif circularity > 0.85:
shape = 'Circle'
elif 0.6 < circularity < 0.85 and solidity > 0.9:
shape = 'Ellipse'
elif solidity < 0.7:
shape = 'Star' if vertices > 6 else 'Irregular'
else:
shape = f'Polygon-{vertices}'
return shape, properties
def process_image(self, image_path):
"""Classify all shapes in image"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
classifications = []
for contour in contours:
shape, props = self.classify(contour)
if shape is None:
continue
classifications.append((shape, props))
# Centroid
M = cv2.moments(contour)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
# Draw contour
color = self._get_shape_color(shape)
cv2.drawContours(result, [contour], 0, color, 2)
# Label
cv2.putText(result, shape, (cx - 30, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
cv2.imshow('Classified Shapes', result)
cv2.waitKey(0)
return classifications
def _get_shape_color(self, shape):
"""Color for each shape type"""
colors = {
'Circle': (0, 0, 255), # Red
'Ellipse': (0, 128, 255), # Orange
'Triangle': (0, 255, 0), # Green
'Square': (255, 0, 0), # Blue
'Rectangle': (255, 128, 0), # Sky blue
'Pentagon': (255, 0, 255), # Purple
'Hexagon': (128, 0, 128), # Purple
'Star': (0, 255, 255), # Yellow
}
return colors.get(shape, (128, 128, 128))
# Usage example
classifier = ShapeClassifier()
results = classifier.process_image('various_shapes.jpg')
print("\nClassification results:")
for shape, props in results:
print(f" {shape}:")
print(f" Area: {props['area']:.0f}")
print(f" Circularity: {props['circularity']:.2f}")
print(f" Vertices: {props['vertices']}")
Real-time Shape Detection¶
import cv2
import numpy as np
def realtime_shape_detection():
"""Real-time shape detection with webcam"""
classifier = ShapeClassifier()
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Adaptive thresholding
binary = cv2.adaptiveThreshold(
blurred, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2
)
# Morphology operations
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for contour in contours:
shape, props = classifier.classify(contour)
if shape is None:
continue
# Centroid
M = cv2.moments(contour)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
# Draw
cv2.drawContours(frame, [contour], 0, (0, 255, 0), 2)
cv2.putText(frame, shape, (cx - 30, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.imshow('Shape Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
# realtime_shape_detection()
8. Practice Problems¶
Problem 1: Sort Shapes¶
Sort detected shapes by area and display rankings.
Solution Code
import cv2
import numpy as np
def rank_shapes_by_area(image_path):
"""Sort shapes by area"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# Store with area
contour_areas = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 100:
contour_areas.append((contour, area))
# Sort by area (descending)
contour_areas.sort(key=lambda x: x[1], reverse=True)
result = img.copy()
for rank, (contour, area) in enumerate(contour_areas, 1):
# Centroid
M = cv2.moments(contour)
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
# Draw
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
cv2.putText(result, f'#{rank}', (cx - 15, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
cv2.putText(result, f'{area:.0f}', (cx - 25, cy + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1)
cv2.imshow('Ranked Shapes', result)
cv2.waitKey(0)
rank_shapes_by_area('shapes.jpg')
Problem 2: Find Specific Aspect Ratio¶
Detect rectangles with a 2:1 aspect ratio.
Solution Code
import cv2
import numpy as np
def find_2to1_rectangles(image_path, tolerance=0.2):
"""Find 2:1 aspect ratio rectangles"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
result = img.copy()
found = []
target_ratio = 2.0
for contour in contours:
if cv2.contourArea(contour) < 100:
continue
# Polygon approximation
peri = cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, 0.04 * peri, True)
# Check if it has 4 vertices
if len(approx) != 4:
continue
# Check ratio using bounding rectangle
x, y, w, h = cv2.boundingRect(contour)
aspect_ratio = max(w, h) / min(w, h)
# Check 2:1 ratio (with tolerance)
if abs(aspect_ratio - target_ratio) < tolerance:
found.append(contour)
cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
# Display ratio
cv2.putText(result, f'{aspect_ratio:.2f}:1', (x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
print(f"2:1 rectangle count: {len(found)}")
cv2.imshow('2:1 Rectangles', result)
cv2.waitKey(0)
find_2to1_rectangles('rectangles.jpg')
Problem 3: Find Most Circular Shape¶
Find and display the shape with the highest circularity.
Solution Code
import cv2
import numpy as np
def find_most_circular(image_path):
"""Find the most circular shape"""
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
best_circularity = 0
best_contour = None
for contour in contours:
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
if area < 100 or perimeter == 0:
continue
circularity = 4 * np.pi * area / (perimeter ** 2)
if circularity > best_circularity:
best_circularity = circularity
best_contour = contour
result = img.copy()
if best_contour is not None:
# All contours (gray)
cv2.drawContours(result, contours, -1, (128, 128, 128), 1)
# Most circular (green)
cv2.drawContours(result, [best_contour], 0, (0, 255, 0), 3)
# Display information
M = cv2.moments(best_contour)
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
cv2.putText(result, f'Circularity: {best_circularity:.3f}',
(cx - 50, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
print(f"Maximum circularity: {best_circularity:.4f}")
cv2.imshow('Most Circular', result)
cv2.waitKey(0)
find_most_circular('shapes.jpg')
Recommended Problems¶
| Difficulty | Topic | Description |
|---|---|---|
| ⭐ | Centroids | Display centroids of all shapes |
| ⭐⭐ | Orientation | Display major axis orientation of elongated shapes |
| ⭐⭐ | Similarity | Classify shapes using matchShapes |
| ⭐⭐⭐ | Finger counting | Use convexity defects |
| ⭐⭐⭐ | Card recognition | Detect rectangles + classification |
Next Steps¶
- 11_Hough_Transform.md - HoughLines, HoughLinesP, HoughCircles