Contour Detection

Contour Detection

Overview

A contour is a curve of continuous points having the same color or brightness, representing the shape of an object. This lesson covers contour detection using findContours(), hierarchy structure, approximation, area/perimeter calculation, and more.


Table of Contents

  1. Contour Basics
  2. findContours() Function
  3. Contour Hierarchy
  4. Drawing and Approximating Contours
  5. Calculating Contour Properties
  6. Object Counting and Separation
  7. Exercises

1. Contour Basics

What is a Contour?

Contour:
- A curve of continuous points with the same color/brightness
- Represents object boundaries
- Extracted from binary images

Original Image     Binarization      Contour Detection
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
  ┌───┐             ■■■■■             ┌───┐      
                 ■■■■■                      
  └───┘       ──▶   ■■■■■       ──▶   └───┘      
        ┌──┐              ■■■              ┌──┐ 
        └──┘              ■■■              └──┘ 
└─────────────┘     └─────────────┘     └─────────────┘
                      (White regions)    (Boundaries only)

Contour Detection Process

1. Read image
      
      
2. Convert to grayscale
      
      
3. Binarization (threshold)
      
      
4. Detect contours (findContours)
      
      
5. Analyze/draw contours

Basic Example

import cv2
import numpy as np

# Read image
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Binarization
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# Detect contours
contours, hierarchy = cv2.findContours(
    binary,
    cv2.RETR_EXTERNAL,      # External contours only
    cv2.CHAIN_APPROX_SIMPLE  # Compression
)

print(f"Number of detected contours: {len(contours)}")

# Draw contours
result = img.copy()
cv2.drawContours(result, contours, -1, (0, 255, 0), 2)

cv2.imshow('Contours', result)
cv2.waitKey(0)

2. findContours() Function

Function Signature

contours, hierarchy = cv2.findContours(image, mode, method)
Parameter Description
image Input binary image (8-bit single channel)
mode Contour retrieval mode (RETR_*)
method Contour approximation method (CHAIN_*)
contours List of detected contours
hierarchy Contour hierarchy structure

Retrieval Mode

┌────────────────────────────────────────────────────────────────────┐
│                         RETR_EXTERNAL                              │
├────────────────────────────────────────────────────────────────────┤
│  Detect only outermost contours                                    │
│                                                                    │
│  ┌──────────────┐                                                  │
│  │  ┌────────┐  │   → Detect only outer rectangle                 │
│  │  │ ┌────┐ │  │                                                  │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_LIST                                │
├────────────────────────────────────────────────────────────────────┤
│  Detect all contours, no hierarchy (same level)                    │
│                                                                    │
│  ┌──────────────┐                                                  │
│  │  ┌────────┐  │   → Detect all 3, no parent-child relationship  │
│  │  │ ┌────┐ │  │                                                  │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_CCOMP                               │
├────────────────────────────────────────────────────────────────────┤
│  2-level hierarchy                                                 │
│  - Level 1: Outer contours                                         │
│  - Level 2: Holes (inner contours)                                 │
│                                                                    │
│  ┌──────────────┐   Level 1 (outer)                                │
│  │  ┌────────┐  │   Level 2 (holes)                                │
│  │  │ ■■■■■■ │  │   (White areas inside are Level 2)               │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_TREE                                │
├────────────────────────────────────────────────────────────────────┤
│  Complete hierarchy (parent-child relationship)                    │
│                                                                    │
│  ┌──────────────┐   Level 0 (outermost)                            │
│  │  ┌────────┐  │   Level 1                                        │
│  │  │ ┌────┐ │  │   Level 2                                        │
│  │  │ │ ■■ │ │  │   Level 3                                        │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

Approximation Method

┌────────────────────────────────────────────────────────────────────┐
│                      CHAIN_APPROX_NONE                             │
├────────────────────────────────────────────────────────────────────┤
│  Store all contour points                                          │
│                                                                    │
│      • • • • • •                                                   │
│    •           •    → Store all boundary pixels                   │
│    •           •       High memory usage                          │
│    •           •                                                   │
│      • • • • • •                                                   │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                     CHAIN_APPROX_SIMPLE                            │
├────────────────────────────────────────────────────────────────────┤
│  Store only endpoints of straight segments (compression)           │
│                                                                    │
│      •         •                                                   │
│                      → Store only 4 vertices                      │
│                         Memory efficient                          │
│                                                                    │
│      •         •                                                   │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                    CHAIN_APPROX_TC89_L1                            │
│                    CHAIN_APPROX_TC89_KCOS                          │
├────────────────────────────────────────────────────────────────────┤
│  Teh-Chin chain approximation algorithm                            │
│  → More aggressive compression                                     │
└────────────────────────────────────────────────────────────────────┘

Mode Examples

import cv2
import numpy as np

def compare_retrieval_modes(image_path):
    """Compare contour retrieval modes"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    modes = [
        (cv2.RETR_EXTERNAL, 'RETR_EXTERNAL'),
        (cv2.RETR_LIST, 'RETR_LIST'),
        (cv2.RETR_CCOMP, 'RETR_CCOMP'),
        (cv2.RETR_TREE, 'RETR_TREE')
    ]

    for mode, name in modes:
        contours, hierarchy = cv2.findContours(
            binary.copy(),
            mode,
            cv2.CHAIN_APPROX_SIMPLE
        )

        result = img.copy()
        cv2.drawContours(result, contours, -1, (0, 255, 0), 2)

        print(f"{name}: {len(contours)} contours")
        cv2.imshow(name, result)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

compare_retrieval_modes('nested_shapes.jpg')

3. Contour Hierarchy

hierarchy Structure

hierarchy[i] = [Next, Previous, First_Child, Parent]

Next:        Index of next contour at same level (-1: none)
Previous:    Index of previous contour at same level (-1: none)
First_Child: Index of first child contour (-1: none)
Parent:      Index of parent contour (-1: none)

Example:
┌───────────────────────────────────┐
 ┌─────────────┐ ┌─────────────┐  
    ┌───┐                     
     A             B        
    └───┘                     
       C                      
 └─────────────┘ └─────────────┘  
                  D                
└───────────────────────────────────┘

RETR_TREE result:
Index 0 (D): Next=-1, Prev=-1, Child=1, Parent=-1  (outermost)
Index 1 (C): Next=2,  Prev=-1, Child=3, Parent=0
Index 2 (B): Next=-1, Prev=1,  Child=-1, Parent=0
Index 3 (A): Next=-1, Prev=-1, Child=-1, Parent=1

Traversing Hierarchy

import cv2
import numpy as np

def analyze_hierarchy(image_path):
    """Analyze contour hierarchy"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    contours, hierarchy = cv2.findContours(
        binary,
        cv2.RETR_TREE,
        cv2.CHAIN_APPROX_SIMPLE
    )

    if hierarchy is None:
        print("No contours found.")
        return

    hierarchy = hierarchy[0]  # (1, N, 4) -> (N, 4)

    print("Hierarchy analysis:")
    print("-" * 50)

    for i, h in enumerate(hierarchy):
        next_c, prev_c, first_child, parent = h

        # Calculate level
        level = 0
        p = parent
        while p != -1:
            level += 1
            p = hierarchy[p][3]  # Parent's parent

        indent = "  " * level
        print(f"{indent}Contour {i}:")
        print(f"{indent}  Level: {level}")
        print(f"{indent}  Parent: {parent}")
        print(f"{indent}  Child: {first_child}")
        print(f"{indent}  Area: {cv2.contourArea(contours[i]):.0f}")

analyze_hierarchy('nested_shapes.jpg')

Extract Contours at Specific Level

import cv2
import numpy as np

def get_contours_at_level(contours, hierarchy, level):
    """Return contours at specific level only"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    for i in range(len(contours)):
        # Calculate current contour's level
        current_level = 0
        parent = hierarchy[i][3]
        while parent != -1:
            current_level += 1
            parent = hierarchy[parent][3]

        if current_level == level:
            result.append(contours[i])

    return result

def get_outer_contours(contours, hierarchy):
    """Return only outermost contours (those without parent)"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    for i in range(len(contours)):
        if hierarchy[i][3] == -1:  # No parent
            result.append(contours[i])

    return result

def get_inner_contours(contours, hierarchy, parent_idx):
    """Return child (inner) contours of specific contour"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    # First child
    child = hierarchy[parent_idx][2]

    while child != -1:
        result.append(contours[child])
        child = hierarchy[child][0]  # Next sibling

    return result

# Usage example
img = cv2.imread('nested.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

contours, hierarchy = cv2.findContours(
    binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)

# Level 0 contours
level0 = get_contours_at_level(contours, hierarchy, 0)

# Outermost contours
outer = get_outer_contours(contours, hierarchy)

result = img.copy()
cv2.drawContours(result, outer, -1, (0, 255, 0), 2)
cv2.imshow('Outer Contours', result)
cv2.waitKey(0)

4. Drawing and Approximating Contours

cv2.drawContours() Function

cv2.drawContours(image, contours, contourIdx, color, thickness)
Parameter Description
image Image to draw on
contours List of contours
contourIdx Index of contour to draw (-1: all)
color Color (B, G, R)
thickness Line thickness (-1: fill)
import cv2
import numpy as np

def draw_contours_examples(image, contours):
    """Draw contours in various ways"""

    # Draw all contours
    result1 = image.copy()
    cv2.drawContours(result1, contours, -1, (0, 255, 0), 2)

    # Draw specific contour only
    result2 = image.copy()
    if len(contours) > 0:
        cv2.drawContours(result2, contours, 0, (255, 0, 0), 3)

    # Fill contours
    result3 = image.copy()
    cv2.drawContours(result3, contours, -1, (0, 0, 255), -1)

    # Each contour different color
    result4 = image.copy()
    for i, contour in enumerate(contours):
        color = tuple(np.random.randint(0, 255, 3).tolist())
        cv2.drawContours(result4, [contour], 0, color, 2)

    return result1, result2, result3, result4

cv2.approxPolyDP() - Polygon Approximation

Douglas-Peucker Algorithm:
Approximate contour with fewer points

epsilon (precision):
- Smaller: Closer to original (more points)
- Larger: Simplified (fewer points)

Example:
Original (many points)   epsilon=0.01         epsilon=0.05
                                                
                                                 
                                                  
                                                
                             
                                                
import cv2
import numpy as np

def approximate_contour(contour, epsilon_ratio=0.02):
    """
    Polygon approximation of contour
    epsilon_ratio: Allowed error ratio relative to perimeter
    """
    # Calculate contour perimeter
    perimeter = cv2.arcLength(contour, True)

    # epsilon = perimeter * ratio
    epsilon = epsilon_ratio * perimeter

    # Approximation
    approx = cv2.approxPolyDP(contour, epsilon, True)

    return approx

def compare_approximations(image, contour):
    """Compare approximations with various epsilon values"""
    epsilons = [0.001, 0.01, 0.02, 0.05, 0.1]

    for eps in epsilons:
        result = image.copy()
        approx = approximate_contour(contour, eps)

        cv2.drawContours(result, [approx], 0, (0, 255, 0), 2)

        # Mark vertices
        for point in approx:
            x, y = point[0]
            cv2.circle(result, (x, y), 5, (0, 0, 255), -1)

        cv2.putText(result, f'epsilon={eps}, points={len(approx)}',
                    (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)

        cv2.imshow(f'Approximation {eps}', result)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

# Usage example
img = cv2.imread('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:
    compare_approximations(img, contours[0])

Shape Identification (by vertex count)

import cv2
import numpy as np

def identify_shape(contour):
    """Identify shape by vertex count"""
    perimeter = cv2.arcLength(contour, True)
    approx = cv2.approxPolyDP(contour, 0.04 * perimeter, True)
    vertices = len(approx)

    if vertices == 3:
        return "Triangle"
    elif vertices == 4:
        # Distinguish square vs rectangle
        x, y, w, h = cv2.boundingRect(approx)
        aspect_ratio = w / float(h)
        if 0.95 <= aspect_ratio <= 1.05:
            return "Square"
        else:
            return "Rectangle"
    elif vertices == 5:
        return "Pentagon"
    elif vertices == 6:
        return "Hexagon"
    elif vertices > 6:
        # Check if circular
        area = cv2.contourArea(contour)
        circularity = 4 * np.pi * area / (perimeter ** 2)
        if circularity > 0.8:
            return "Circle"
        else:
            return f"Polygon ({vertices} vertices)"
    else:
        return "Unknown"

def label_shapes(image_path):
    """Identify and label 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()

    for contour in contours:
        # Ignore very small contours
        if cv2.contourArea(contour) < 100:
            continue

        # Identify shape
        shape = identify_shape(contour)

        # Calculate centroid
        M = cv2.moments(contour)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
        else:
            cx, cy = 0, 0

        # Draw contour
        cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

        # Display label
        cv2.putText(result, shape, (cx - 40, cy),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)

    cv2.imshow('Shapes', result)
    cv2.waitKey(0)

label_shapes('shapes.jpg')

5. Calculating Contour Properties

Perimeter and Area

import cv2
import numpy as np

def contour_properties(contour):
    """Calculate basic contour properties"""

    # Area
    area = cv2.contourArea(contour)

    # Perimeter (closed=True: closed curve)
    perimeter = cv2.arcLength(contour, True)

    # Bounding rectangle
    x, y, w, h = cv2.boundingRect(contour)
    bounding_area = w * h

    # Area ratio (Extent)
    extent = area / bounding_area if bounding_area > 0 else 0

    # Circularity
    circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0

    # Convex hull
    hull = cv2.convexHull(contour)
    hull_area = cv2.contourArea(hull)

    # Solidity
    solidity = area / hull_area if hull_area > 0 else 0

    return {
        'area': area,
        'perimeter': perimeter,
        'extent': extent,
        'circularity': circularity,
        'solidity': solidity
    }

# Usage example
img = cv2.imread('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)

for i, contour in enumerate(contours):
    props = contour_properties(contour)
    print(f"Contour {i}:")
    print(f"  Area: {props['area']:.0f}")
    print(f"  Perimeter: {props['perimeter']:.1f}")
    print(f"  Extent: {props['extent']:.2f}")
    print(f"  Circularity: {props['circularity']:.2f}")
    print(f"  Solidity: {props['solidity']:.2f}")

Bounding Shapes

import cv2
import numpy as np

def bounding_shapes(image, contour):
    """Various bounding shapes for contour"""
    result = image.copy()

    # 1. Bounding Rectangle
    x, y, w, h = cv2.boundingRect(contour)
    cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 0), 2)

    # 2. Rotated Rectangle
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.int_(box)
    cv2.drawContours(result, [box], 0, (255, 0, 0), 2)

    # 3. Minimum Enclosing Circle
    (cx, cy), radius = cv2.minEnclosingCircle(contour)
    cv2.circle(result, (int(cx), int(cy)), int(radius), (0, 0, 255), 2)

    # 4. Fitting Ellipse
    if len(contour) >= 5:  # Minimum 5 points required
        ellipse = cv2.fitEllipse(contour)
        cv2.ellipse(result, ellipse, (255, 255, 0), 2)

    # 5. Fitting Line
    rows, cols = image.shape[:2]
    [vx, vy, x, y] = cv2.fitLine(contour, cv2.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(result, (cols-1, righty), (0, lefty), (0, 255, 255), 2)

    return result

# Usage example
img = cv2.imread('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 = bounding_shapes(img, contours[0])
    cv2.imshow('Bounding Shapes', result)
    cv2.waitKey(0)

Convex Hull

Convex Hull:
The smallest convex polygon that encloses a set of points

                            ┌──────────┐
                                                                           └──────────┘
   Original contour    Convex hull

Convexity Defects:
Deepest points between contour and convex hull Used for finger detection etc.
import cv2
import numpy as np

def convex_hull_analysis(image, contour):
    """Convex hull analysis"""
    result = image.copy()

    # Calculate convex hull
    hull = cv2.convexHull(contour)

    # Original contour
    cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

    # Convex hull
    cv2.drawContours(result, [hull], 0, (0, 0, 255), 2)

    # Convexity defects (useful for finger detection etc.)
    hull_indices = cv2.convexHull(contour, returnPoints=False)
    if len(hull_indices) > 3 and len(contour) > 3:
        defects = cv2.convexityDefects(contour, hull_indices)

        if defects is not None:
            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])

                # Display only defects with certain depth
                if d / 256 > 10:  # Depth threshold
                    cv2.circle(result, far, 5, (255, 0, 255), -1)
                    cv2.line(result, start, far, (255, 0, 255), 1)
                    cv2.line(result, far, end, (255, 0, 255), 1)

    return result

# Usage example (hand image)
img = cv2.imread('hand.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:
    # Select largest contour
    largest = max(contours, key=cv2.contourArea)
    result = convex_hull_analysis(img, largest)
    cv2.imshow('Convex Hull', result)
    cv2.waitKey(0)

6. Object Counting and Separation

Object Counting

import cv2
import numpy as np

def count_objects(image_path, min_area=100):
    """Count objects in image"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Adaptive binarization
    binary = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV,
        11, 2
    )

    # Remove noise with morphological operations
    kernel = np.ones((3, 3), np.uint8)
    binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
    binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

    # Detect contours
    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    # Size filtering
    valid_contours = [c for c in contours if cv2.contourArea(c) >= min_area]

    result = img.copy()
    for i, contour in enumerate(valid_contours):
        # Centroid
        M = cv2.moments(contour)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])

            # Display number
            cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
            cv2.putText(result, str(i + 1), (cx - 10, cy + 5),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)

    print(f"Detected objects: {len(valid_contours)}")

    cv2.imshow('Counted Objects', result)
    cv2.waitKey(0)

    return len(valid_contours)

# Coin counting example
count_objects('coins.jpg', min_area=500)

Object Separation and Extraction

import cv2
import numpy as np

def extract_objects(image_path, output_dir='objects/'):
    """Separate and save individual objects"""
    import os
    os.makedirs(output_dir, exist_ok=True)

    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
    )

    objects = []
    for i, contour in enumerate(contours):
        # Ignore very small objects
        if cv2.contourArea(contour) < 100:
            continue

        # Bounding rectangle
        x, y, w, h = cv2.boundingRect(contour)

        # Add padding
        padding = 10
        x1 = max(0, x - padding)
        y1 = max(0, y - padding)
        x2 = min(img.shape[1], x + w + padding)
        y2 = min(img.shape[0], y + h + padding)

        # Extract object region
        roi = img[y1:y2, x1:x2].copy()
        objects.append(roi)

        # Save
        cv2.imwrite(f'{output_dir}object_{i:03d}.jpg', roi)

    print(f"Extracted {len(objects)} objects")
    return objects

# Usage example
objects = extract_objects('multiple_objects.jpg')

Detect Specific Shapes Only

import cv2
import numpy as np

def find_circles(image_path):
    """Detect only circular objects"""
    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()
    circles = []

    for contour in contours:
        area = cv2.contourArea(contour)
        perimeter = cv2.arcLength(contour, True)

        if perimeter == 0:
            continue

        # Calculate circularity
        circularity = 4 * np.pi * area / (perimeter ** 2)

        # Consider as circle if circularity >= 0.8
        if circularity > 0.8 and area > 100:
            circles.append(contour)
            cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

            # Mark center point
            M = cv2.moments(contour)
            if M["m00"] != 0:
                cx = int(M["m10"] / M["m00"])
                cy = int(M["m01"] / M["m00"])
                cv2.circle(result, (cx, cy), 5, (0, 0, 255), -1)

    print(f"Circular objects: {len(circles)}")
    cv2.imshow('Circles', result)
    cv2.waitKey(0)

    return circles

def find_rectangles(image_path):
    """Detect only rectangular objects"""
    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()
    rectangles = []

    for contour in contours:
        area = cv2.contourArea(contour)
        if area < 100:
            continue

        # Polygon approximation
        peri = cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, 0.04 * peri, True)

        # Rectangle if 4 vertices
        if len(approx) == 4:
            rectangles.append(contour)
            cv2.drawContours(result, [approx], 0, (0, 255, 0), 2)

    print(f"Rectangular objects: {len(rectangles)}")
    cv2.imshow('Rectangles', result)
    cv2.waitKey(0)

    return rectangles

# Usage example
find_circles('shapes.jpg')
find_rectangles('shapes.jpg')

7. Exercises

Problem 1: Coin Counter

Count coins in a coin image and calculate total amount (distinguish by size).

Hint Classify coin types based on coin size (area or radius).
Solution Code
import cv2
import numpy as np

def count_coins_by_size(image_path):
    """Classify and count coins by size"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (11, 11), 0)

    # Canny edge detection + closing operation
    edges = cv2.Canny(blurred, 30, 150)
    kernel = np.ones((3, 3), np.uint8)
    edges = cv2.dilate(edges, kernel, iterations=2)
    edges = cv2.erode(edges, kernel, iterations=1)

    # Detect contours
    contours, _ = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()

    # Classify by size (radius-based)
    small_coins = []   # 10 won
    medium_coins = []  # 50 won
    large_coins = []   # 100 won

    for contour in contours:
        area = cv2.contourArea(contour)
        if area < 500:  # Ignore noise
            continue

        # Minimum enclosing circle
        (x, y), radius = cv2.minEnclosingCircle(contour)

        # Check circularity
        perimeter = cv2.arcLength(contour, True)
        if perimeter > 0:
            circularity = 4 * np.pi * area / (perimeter ** 2)
            if circularity < 0.7:  # Not circular
                continue

        # Classify by size (example thresholds)
        if radius < 30:
            small_coins.append((int(x), int(y), int(radius)))
            color = (255, 0, 0)  # Blue - 10 won
        elif radius < 40:
            medium_coins.append((int(x), int(y), int(radius)))
            color = (0, 255, 0)  # Green - 50 won
        else:
            large_coins.append((int(x), int(y), int(radius)))
            color = (0, 0, 255)  # Red - 100 won

        cv2.circle(result, (int(x), int(y)), int(radius), color, 2)

    # Output results
    total = (len(small_coins) * 10 +
             len(medium_coins) * 50 +
             len(large_coins) * 100)

    print(f"10 won: {len(small_coins)}")
    print(f"50 won: {len(medium_coins)}")
    print(f"100 won: {len(large_coins)}")
    print(f"Total: {total} won")

    cv2.imshow('Coins', result)
    cv2.waitKey(0)

count_coins_by_size('coins.jpg')

Problem 2: Document Rectangle Detection

Find document (paper) contour in image and return 4 vertices.

Hint Find the largest 4-sided contour. Approximate to 4 points using approxPolyDP.
Solution Code
import cv2
import numpy as np

def find_document(image_path):
    """Find 4 vertices of document area"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)

    # Edge detection
    edges = cv2.Canny(blurred, 50, 150)

    # Detect contours
    contours, _ = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    # Sort by area
    contours = sorted(contours, key=cv2.contourArea, reverse=True)

    document_corners = None

    for contour in contours[:5]:  # Check top 5 only
        peri = cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, 0.02 * peri, True)

        # If 4 vertices, it's a document
        if len(approx) == 4:
            document_corners = approx
            break

    if document_corners is not None:
        result = img.copy()
        cv2.drawContours(result, [document_corners], 0, (0, 255, 0), 3)

        # Mark vertices
        for point in document_corners:
            x, y = point[0]
            cv2.circle(result, (x, y), 10, (0, 0, 255), -1)

        cv2.imshow('Document', result)
        cv2.waitKey(0)

        return document_corners.reshape(4, 2)
    else:
        print("Document not found.")
        return None

corners = find_document('document.jpg')
if corners is not None:
    print("Document vertices:", corners)

Problem 3: Detect Empty Spaces

Count the number of holes (empty spaces) in a binary image.

Hint Use RETR_CCOMP or RETR_TREE to find inner contours (holes).
Solution Code
import cv2
import numpy as np

def count_holes(image_path):
    """Count holes inside objects"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    # RETR_CCOMP: 2-level hierarchy (outer + holes)
    contours, hierarchy = cv2.findContours(
        binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
    )

    if hierarchy is None:
        return 0

    hierarchy = hierarchy[0]

    result = img.copy()
    holes = []

    for i, h in enumerate(hierarchy):
        # Contour with parent = hole
        if h[3] != -1:  # Has parent
            area = cv2.contourArea(contours[i])
            if area > 50:  # Ignore noise
                holes.append(contours[i])
                cv2.drawContours(result, [contours[i]], 0, (0, 0, 255), 2)

    print(f"Number of holes: {len(holes)}")

    cv2.imshow('Holes', result)
    cv2.waitKey(0)

    return len(holes)

count_holes('donut.jpg')
Difficulty Topic Description
Basic Detection Count objects with findContours
⭐⭐ Area Filter Detect only objects in specific size range
⭐⭐ Shape Classification Distinguish triangles, rectangles, circles
⭐⭐⭐ Document Scanner Detect document then perspective transform
⭐⭐⭐ Finger Counter Count fingers using convexity defects

Next Steps


References

to navigate between lessons