Computer vision

Region Processing

From a black-and-white mask to counted, measured and named objects.

Connected components

A connected component is a group of connected pixels with the same value.

4-connectivity: up, down, left, right

8-connectivity: plus diagonals

Use cases: count objects, label regions, extract features.

Connected components labeling


        import cv2
        import numpy as np
        # binary image
        _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
        # label connected components
        num_labels, labels = cv2.connectedComponents(binary)  # ans:
        # ans: num_labels includes the background (label 0)
        # ans: actual objects = num_labels - 1
        # labels is the same size as binary
        # ans: each pixel has a label (0=background, 1,2,3...=objects)
        # visualize labels
        labels_vis = np.uint8(255 * labels / labels.max())  # ans:
      

Labels and stats, live

Connected components with stats


        # get statistics for each component
        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(  # ans:
            binary, connectivity=8)  # ans:
        # stats: [x, y, width, height, area] for each label
        for i in range(1, num_labels):  # ans:
            x, y, w, h, area = stats[i]  # ans:
            cx, cy = centroids[i]  # ans:
            print(f"Object {i}: area={area}, center=({cx}, {cy})")  # ans:
        # filter by area
        min_area = 100  # ans:
        large_components = [i for i in range(1, num_labels)  # ans:
                            if stats[i, cv2.CC_STAT_AREA] > min_area]  # ans:
      

Contour properties

A contour is the boundary of a region. Click a shape to measure it.

Contour area and perimeter


        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        for cnt in contours:  # ans:
            # area
            area = cv2.contourArea(cnt)  # ans:
            # perimeter
            perimeter = cv2.arcLength(cnt, True)  # ans:
            # ans: True means a closed contour
            # aspect ratio
            x, y, w, h = cv2.boundingRect(cnt)  # ans:
            aspect_ratio = float(w) / h  # ans:
      

contourArea measures the polygon through the boundary pixels, so it comes out a little smaller than the pixel count in stats.

Bounding rectangle


        # straight bounding rectangle
        x, y, w, h = cv2.boundingRect(cnt)  # ans:
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)  # ans:
        # minimum area rectangle (can be rotated)
        rect = cv2.minAreaRect(cnt)  # ans:
        box = cv2.boxPoints(rect)  # ans:
        box = np.intp(box)  # ans:
        cv2.drawContours(img, [box], 0, (0, 0, 255), 2)  # ans:
      

minAreaRect returns ((cx, cy), (w, h), angle). Older code uses np.int0, which NumPy 2 removed; np.intp is the same type.

Centroid

The centroid is the centre of mass of a region, computed from its moments.


        # compute moments
        M = cv2.moments(cnt)  # ans:
        # centroid coordinates
        if M['m00'] != 0:  # ans:
            cx = int(M['m10'] / M['m00'])  # ans:
            cy = int(M['m01'] / M['m00'])  # ans:
        else:  # ans:
            cx, cy = 0, 0  # ans:
        # draw centroid
        cv2.circle(img, (cx, cy), 5, (255, 0, 0), -1)  # ans:
      

Convex hull

The convex hull is the smallest convex polygon containing the contour: a rubber band around the shape.

Convexity defects are the gaps between the contour and its hull (pink).


            # compute convex hull
            hull = cv2.convexHull(cnt)  # ans:
            cv2.drawContours(img, [hull], 0, (0, 255, 0), 2)  # ans:
            # check if contour is convex
            is_convex = cv2.isContourConvex(cnt)  # ans:
            # convexity defects
            hull = cv2.convexHull(cnt, returnPoints=False)  # ans:
            defects = cv2.convexityDefects(cnt, hull)  # ans:
          

Shape matching

cv2.matchShapes compares two contours using Hu moments, which do not change with rotation or scale. Lower distance = more similar; 0 means identical.

Click a shape to compare every other shape with it.

matchShapes in code


        # compare two contours
        cnt1 = contours[0]
        cnt2 = contours[1]
        distance = cv2.matchShapes(cnt1, cnt2, cv2.CONTOURS_MATCH_I1, 0)  # ans:
        # ans: lower distance = more similar shapes
        # ans: 0 means identical, larger = more different
      

Approximating contours

approxPolyDP simplifies a contour to a polygon with fewer points (the Douglas-Peucker algorithm). Count the vertices to name the shape.

approxPolyDP in code


        # approximate contour
        epsilon = 0.01 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)  # ans:
        # ans: epsilon: approximation accuracy (smaller = more accurate)
        # detect shapes by number of vertices
        if len(approx) == 3:  # ans:
            shape = "Triangle"  # ans:
        elif len(approx) == 4:  # ans:
            shape = "Rectangle"  # ans:
        elif len(approx) > 4:  # ans:
            shape = "Circle"  # ans:
      

A crude rule: on the previous slide the plus signs and digits have many vertices too, and get called circles.

Exercises

Answer first, then press → to check.

Exercises 1 · concepts

What is a connected component?

A group of connected pixels with the same value.

4 vs 8 connectivity?

4 uses up/down/left/right; 8 adds the diagonals.

What is a contour?

The boundary of a shape or region.

What is a centroid?

The centre of mass of a region.

What is a convex hull?

The smallest convex polygon containing the contour.

Exercises 2 · concepts

What are moments?

Weighted sums over a shape's pixels that describe it: area, centre, spread, orientation.

How do you compute the centroid from moments?

cx = M10/M00, cy = M01/M00.

What is approxPolyDP?

It simplifies a contour to a polygon with fewer points.

What is shape matching?

Comparing the similarity of two contours.

Exercises 3 · coding


        # label connected components
        _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
        num_labels, labels = cv2.connectedComponents(binary)  # ans:
        print(f"Found {num_labels - 1} objects")  # ans:
        # connected components with stats
        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary)  # ans:
        for i in range(1, num_labels):  # ans:
            area = stats[i, cv2.CC_STAT_AREA]  # ans:
            cx, cy = centroids[i]  # ans:
      

Exercises 4 · coding


        # find and draw contours
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        for cnt in contours:  # ans:
            area = cv2.contourArea(cnt)  # ans:
            if area > 100:  # ans:
                x, y, w, h = cv2.boundingRect(cnt)  # ans:
                cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)  # ans:
      

Exercises 5 · mixed


        # compute centroid
        M = cv2.moments(cnt)
        cx = int(M['m10'] / M['m00'])  # ans:
        cy = int(M['m01'] / M['m00'])  # ans:
        cv2.circle(img, (cx, cy), 5, (255, 0, 0), -1)  # ans:
        # convex hull
        hull = cv2.convexHull(cnt)  # ans:
        cv2.drawContours(img, [hull], 0, (0, 255, 0), 2)  # ans:
        # approximate contour
        epsilon = 0.02 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)  # ans:
        print(f"Vertices: {len(approx)}")  # ans: