Expand description
Local lattice-orientation synthesis from point positions alone.
Both square strategies consume OrientedFeature<2> — each corner carries
two local grid directions. When the caller has no per-corner orientation
(Evidence::Positions: a dot grid, a circle grid, or a chessboard whose
corners carry no axis estimate), this module recovers those two directions
geometrically so the topological machinery runs
unchanged.
§The perspective problem
The grid is viewed in perspective. The two grid directions are therefore not orthogonal in the image, and the angle between them varies across the image (the grid-line families converge toward two vanishing points). Any method that assumes a fixed 90° between the axes, or a single global orientation, is wrong. The estimate must be local and must not constrain the inter-axis angle.
§What is perspective-invariant
A projective map preserves straight lines, so three collinear grid points
(i−1, j), (i, j), (i+1, j) stay collinear in the image. Hence a corner’s
+u and −u neighbour chords are exactly antipodal (180° apart) even
under arbitrary perspective. Folding chord orientation modulo π
therefore collapses each axis neighbour-pair onto a single direction
exactly — with no orthogonality assumption. The two grid directions show
up as two distinct clusters in [0, π), separated by whatever angle the
local perspective dictates.
A second fact makes the neighbour set reliable: for a grid cell the axis
step is shorter than the diagonal step (√(a² + b²) > max(a, b)), and mild
perspective scales both by roughly the same local factor, so a corner’s
four nearest neighbours are its four axis neighbours (±u, ±v). The
estimate uses those.
§Algorithm
- Per corner: fold the chord angles to its
knearest neighbours into[0, π). Generically these are two antipodal pairs collapsing to two directions. - Pool every corner’s folded nearest-edge angles into a global
distribution and pick its two dominant modes
(g0, g1). This is a robust, image-wide prior — used only to seed the per-corner estimate and as a fallback, never as the answer. - Per corner: run an undirected (mod-π) 2-means over the corner’s folded
chords, seeded at
(g0, g1). The two resulting centers are the corner’s two local grid directions — not constrained to be orthogonal, so they track the local perspective. An empty cluster falls back to its global seed.
§Precision contract
A corner whose synthesized axes are wrong is rejected downstream by the
seed / attach geometry gates (axis-alignment, edge-length, residual) — it
becomes a missing corner, never a mislabelled one. That is the correct
trade for the workspace precision contract: a missing corner is acceptable,
a wrong (i, j) label is not.
§Undirected circular statistics
Axis angles are undirected (period π): θ and θ + π are the same
direction. Every mean here accumulates (cos 2θ, sin 2θ) and halves the
atan2 result; raw (cos θ, sin θ) accumulation would break at the 0/π
seam.
Functions§
- synthesize_
oriented2 - Synthesize two local lattice axes for every point feature from neighbour
geometry, returning oriented-2 features that carry the same
source_indexand position plus the recovered axes. The two axes are not constrained to be orthogonal — they track the local projected grid directions. - synthesize_
oriented3 - Synthesize three local lattice axes for every point feature from neighbour
geometry — the hex analogue of
synthesize_oriented2. - synthesize_
oriented2_ from_ oriented1 - Synthesize the second local lattice axis for every single-axis feature, keeping the caller-supplied axis as the first.