Goal
Recover the position of every detected corner within the regular grid of a checkerboard calibration pattern. Input: a grayscale image and a set of detected corner locations . Output: an integer coordinate assigned to each corner that lies on the pattern grid, and a rejection flag for corners that do not. The method uses only the topology of a Delaunay triangulation of — it does not fit lines to pattern edges, tolerates curved edges from radial distortion, and handles partial occlusion of the pattern.
Algorithm
Let denote the image corner locations. Let denote the Delaunay triangulation of — the unique triangulation in which every triangle's circumcircle contains no point of . Let each triangle carry an interior colour , averaged over pixels at a small distance from the triangle edges. Let denote the quadrilateral mesh obtained by merging triangle pairs as defined below, and let denote the edge-degree of node in .
Two Delaunay triangles sharing an edge are merged into a quadrilateral when their interior means agree under a fixed tolerance :
Under the checkerboard pattern, every interior triangle has exactly one same-colour edge-neighbour; pairs that pass the test correspond to a black or white tile.
A quadrilateral is topologically legal when fewer than two of its four nodes carry edge-degree greater than in :
The grid has every interior node at degree and every boundary node at degree or ; any node of degree is a spurious corner from the scene background or pattern-interior noise.
A quadrilateral with opposite edges and is geometrically legal when both opposite-edge length ratios stay below :
The constant is deliberately conservative — an imaged tile stays within it until the camera points at the pattern at a grazing angle, at which point the upstream corner detection is already unreliable.
Given a seed quad with assigned node coordinates and a neighbour quad sharing edge , the four node coordinates of the neighbour are fixed by finding the slot of in the neighbour and assigning:
The assignment is purely topological — it uses only the shared-edge indices, not pixel geometry — so it survives radial distortion and perspective foreshortening.
Procedure
- Compute .
- For each triangle , compute by averaging over pixels at distance from the triangle edges.
- For each triangle , merge with its single edge-neighbour that satisfies . Emit a quadrilateral; discard triangles with no same-colour neighbour.
- Construct the quad mesh and compute for every node .
- Drop every that fails topological legality.
- Drop every remaining whose opposite-edge length ratio exceeds .
- Drop connected components of with a small number of quads.
- Pick any surviving quad as seed; assign its four nodes coordinates .
- Flood-fill the remaining quads: for each unlabelled quad adjacent to a labelled one through edge , apply coordinate propagation. Each quad is visited exactly once.
- Locate the three marker circles on the pattern, read off the origin and x-axis direction, and transform the labelled coordinates into the reference frame.
Implementation
The three legality tests and the coordinate propagation step translate directly from the definitions above.
type Node = (i32, i32);
struct Quad {
nodes: [NodeId; 4], // indices into the mesh's node array
coords: [Option<Node>; 4],
}
fn is_topologically_legal(q: &Quad, deg: &[u32]) -> bool {
q.nodes.iter().filter(|&&n| deg[n as usize] > 4).count() < 2
}
fn is_geometrically_legal(edge_len: [f32; 4], r_max: f32) -> bool {
let ratio = |a: f32, b: f32| a.max(b) / a.min(b);
ratio(edge_len[0], edge_len[2]) <= r_max
&& ratio(edge_len[1], edge_len[3]) <= r_max
}
fn propagate(next: &mut Quad, ns_slot: usize, ns_coord: Node) {
let (x, y) = ns_coord;
next.coords[ns_slot] = Some((x, y));
next.coords[(ns_slot + 1) % 4] = Some((x + 1, y));
next.coords[(ns_slot + 2) % 4] = Some((x + 1, y + 1));
next.coords[(ns_slot + 3) % 4] = Some((x, y + 1));
}
The flood-fill outer loop enqueues each quad once — every labelled quad pushes its three edge-neighbours; the queue is drained in time, and is linear in the corner count.
Remarks
- Complexity: in the number of corners. Delaunay triangulation is in the worst case and near-linear in practice; every subsequent stage — merge, filter, flood-fill — visits each triangle or quad a constant number of times.
- Topological tests are threshold-free and run before geometric tests. The degree test uses the integer grid property ; the aspect-ratio and connected-component tests rely on numerical thresholds and are applied only to what survives.
- Radial distortion does not affect labelling. Curved grid edges alter pixel geometry but not Delaunay adjacency, and coordinate propagation uses only adjacency.
- The detector fails at extreme viewing angles (roughly from the pattern plane) where Delaunay triangulation crosses tile boundaries instead of diagonalizing them. At such angles the upstream corner detector is also unreliable.
- Delaunay triangulation is not projective-invariant in general; the algorithm's practical robustness is empirical rather than a theorem. Extreme projective transforms can flip a diagonal and produce misaligned quads even when every corner is detected.
- The method is agnostic to the corner detector used in step 1. Any detector that produces subpixel corner locations on pattern intersections feeds the Delaunay stage identically.
When to choose Shu over Laureano
Laureano (2013) uses the same Delaunay-based topology approach as Shu (2009) but at a different graph granularity. Shu filters at the quad level after merging same-colour triangle pairs; Laureano filters at the triangle level directly, with a per-triangle uniform-interior + same-colour-neighbour rule. Both iterate to a fixed point.
| Shu (2009) | Laureano (2013) | |
|---|---|---|
| Graph unit | quad (merged triangle pair) | triangle (directly from Delaunay) |
| Per-vertex rule | + aspect ratio | same-colour neighbour, same-colour |
| Coordinate propagation | flood-fill on quad mesh | reflection across shared edges from a seed triangle pair |
| Subpixel refinement | upstream detector handles it | explicit Chen-Zhang Hessian solve at surviving vertices |
| Partial-pattern recovery | local (each connected quad component) | local (orphan-vertex pruning to fixed point) |
Choose Shu when (1) you have a clean upstream corner detector that produces well-separated saddle candidates and you want the simpler quad-mesh post-processing — the merge step removes the per-triangle bookkeeping; (2) the refinement is handled separately. Choose Laureano when you want the topology filter and the subpixel refinement to be part of the same pipeline — Laureano explicitly invokes the Chen-Zhang Hessian saddle solve only at vertices that have already passed the topology filter, which avoids wasting refinement compute on false positives. The triangle-level filter also tends to recover slightly more border vertices on partial patterns since it doesn't require a full quad to validate a vertex.
References
- C. Shu, A. Brunton, M. A. Fiala. A topological approach to finding grids in calibration patterns. Machine Vision and Applications, 2009. DOI: 10.1007/s00138-009-0202-2
- C. Harris, M. J. Stephens. A Combined Corner and Edge Detector. Alvey Vision Conference, 1988. DOI: 10.5244/c.2.23