AI & ML / Computer vision / 07_contours_and_polygons.md

Contours, polygons and vector geometry

Updated 4 interview angles 4 min read source
On this page7
  1. Mask to contour
  2. Simplification, and the tolerance that matters
  3. Shapely for the operations that follow
  4. Snapping and regularisation
  5. Where it breaks in production
  6. Related
  7. Interview angle

Contours, polygons and vector geometry

A model outputs a raster mask. A takeoff system, a CAD export or a quantity calculation needs vector geometry. The conversion between them is its own engineering problem with its own failure modes, and it is where a lot of “the model is fine but the numbers are wrong” originates.

Mask to contour

python
import cv2

contours, hierarchy = cv2.findContours(
    mask.astype("uint8"),
    cv2.RETR_CCOMP,             # two levels: outer boundaries and holes
    cv2.CHAIN_APPROX_SIMPLE,    # collapse straight runs to endpoints
)

Two arguments carry all the meaning:

  • Retrieval mode. RETR_EXTERNAL gives outer boundaries only and silently discards holes — wrong for a room with a column in it, where the column is a hole that must be subtracted from the floor area. RETR_CCOMP gives outers plus holes; RETR_TREE gives the full nesting.
  • Approximation. CHAIN_APPROX_NONE stores every boundary pixel; CHAIN_APPROX_SIMPLE collapses straight runs to their endpoints. Use SIMPLE unless you need per-pixel boundary.

The hierarchy array is what tells you which contour is a hole in which parent, and ignoring it is the standard way to double-count area.

Simplification, and the tolerance that matters

A raw contour follows every pixel step, so a straight wall becomes a staircase of hundreds of vertices. Douglas-Peucker simplification removes vertices that lie within a tolerance of the line between their neighbours.

python
eps = 0.002 * cv2.arcLength(cnt, True)     # relative, not absolute
simple = cv2.approxPolyDP(cnt, eps, closed=True)

Make the tolerance relative to the contour’s own size, not a fixed pixel count. A fixed eps that cleans a large room’s outline will erase a small cupboard entirely, and this is the single most common bug in this step.

The trade-off to state out loud: too small a tolerance leaves pixel noise in the geometry and produces CAD output nobody can edit; too large cuts corners off and changes measured areas. Validate by checking that simplified area stays within a tolerance of raster area.

Shapely for the operations that follow

OpenCV gives you contours. Shapely gives you predicates and set operations on them — the things a takeoff workflow actually needs.

python
from shapely.geometry import Polygon
from shapely.ops import unary_union

room = Polygon(shell=outer[:, 0, :], holes=[h[:, 0, :] for h in holes])

if not room.is_valid:                  # self-intersection, usually
    room = room.buffer(0)              # the standard repair

area_m2 = room.area * (m_per_px ** 2)
perimeter_m = room.length * m_per_px
merged = unary_union([room_a, room_b]) # dissolve a shared wall
overlap = room_a.intersection(room_b)  # should be near-empty; if not, bug

buffer(0) as a validity repair is the idiom worth knowing. A polygon that self-intersects — a bowtie, a boundary that touches itself — is invalid, and area on an invalid polygon is undefined rather than merely wrong. buffer(0) re-runs the geometry engine’s noding and usually returns a valid equivalent, though it can split one polygon into a MultiPolygon, which downstream code must expect.

Handle MultiPolygon everywhere. Any operation can return one: intersection of two L-shapes, buffer(0) on a pinched polygon, difference that splits a region in two. Code that assumes a single geometry crashes on real data eventually.

Snapping and regularisation

Vectorised architecture should look like architecture: walls axis-aligned or at consistent angles, corners meeting exactly, no 0.3-degree skews.

python
import numpy as np

def snap_angles(pts, tol_deg=5.0):
    """Force near-axis-aligned segments to exactly axis-aligned."""
    out = pts.copy().astype(float)
    for i in range(len(out) - 1):
        dx, dy = out[i + 1] - out[i]
        ang = np.degrees(np.arctan2(dy, dx)) % 90
        if ang < tol_deg or ang > 90 - tol_deg:
            if abs(dx) > abs(dy):
                out[i + 1][1] = out[i][1]     # horizontal
            else:
                out[i + 1][0] = out[i][0]     # vertical
    return out

Snap after rectification, never before — a photographed plan’s walls are not axis-aligned in image coordinates, and snapping there destroys the very skew the homography needs to measure. Order matters and interviewers ask about it.

Where it breaks in production

  • Holes ignored. RETR_EXTERNAL plus a courtyard gives an area that includes the courtyard. Silent, and wrong by a lot.
  • Fixed simplification tolerance. Erases small features, keeps noise on large ones.
  • Invalid polygons propagate. An invalid input to union gives an exception or a wrong result depending on version. Validate at the boundary.
  • Winding order. Some consumers require exteriors counter-clockwise and holes clockwise. shapely.geometry.polygon.orient normalises it.
  • Coordinate frame confusion. Image y grows downward, most CAD y grows upward. Flip once, in one place, and write down where.
  • Float accumulation. Summing thousands of small areas in float32 drifts. Use float64, and reconcile the total against the raster count as a check.

Interview angle 4

  • “How do you get square metres from a segmentation mask?” - find contours with hole retrieval, simplify with a tolerance relative to contour length, build a Shapely polygon with its holes, validate, then multiply by the squared scale factor from rectification. Say the hole part unprompted; it is the common error.
  • “Your room areas are all slightly too large.” - holes are being dropped. RETR_EXTERNAL discards inner boundaries, so columns and courtyards are counted as floor.
  • “What does buffer(0) do and why?” - repairs an invalid polygon, usually a self-intersection, by re-noding it. Necessary because area and set operations are undefined on invalid geometry. Follow-up: what can it return? A MultiPolygon - your code must handle that.
  • “When do you snap walls to axes?” - after rectification, never before. In the original photograph the walls genuinely are not axis-aligned, and snapping there destroys the perspective signal you need.