metrics op• Data kinds: points × points → signal
• Call: import fullseye as fs; fs.ledger.m3c2_distance(a, b, cores, normals, radius, max_depth=None, min_points=4) (to call the implementation directly, import metrics3d; metrics3d.m3c2_distance(a, b, cores, normals, radius, max_depth=None, min_points=4); from the registry, ops3d.get("m3c2_distance"))
• Return value through the ledger: fullseye.ledger.m3c2_distance(...) returns only the declared out type signal (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.m3c2_distance.raw(...) or call metrics3d.m3c2_distance directly.
Measures the difference between point clouds from 2 epochs along the normal direction (M3C2). → the 2 values (distance, lod).
The nearest-neighbour distance (chamfer / C2C) is "the distance to the closest point", so on a tilted surface it counts even the shift along the surface as distance. In change detection of terrain and structures, that is the main cause of "false change detected merely by re-measuring" (poc_structure_4d_deterioration
measured: with zero deterioration, just re-measuring gives a C2C median of 21.07 mm, along the normal 0.55 mm).
M3C2 (Lague 2013) cuts both clouds, per core point, with a cylinder whose axis is the normal n (radius radius, length max_depth), projects each point onto n and returns the difference of the means. The side n points to is positive — the sign directly reads as "increased / decreased".
Args:
a: point cloud at time 1 (Na,3).
b: point cloud at time 2 (Nb,3).
cores: where to measure (M,3) (may be a subset of a, or a separately placed grid).
normals: normal per core (M,3) (need not be normalised. Its sign decides the sign of the result).
radius: cylinder radius (the averaging range around the core. Choose it larger than the surface roughness and smaller than
the structure you want to measure).
max_depth: half the cylinder length. None means no limit along the axis.
min_points: a core lacking this many points on either side returns nan (default 4).
Returns:
(distance, lod): both (M,) float64. distance is the signed difference
along the normal, lod (level of detection) is
1.96·sqrt(σa²/na + σb²/nb) — a difference not exceeding this value cannot be told apart from noise.
Cores with too few points get nan for both.
★Via the ledger (fullseye.ledger.m3c2_distance) only distance, the declared out type, is
returned. When lod is also needed, use fullseye.ledger.m3c2_distance.raw(...).
Raises:
ValueError: when the point clouds / cores are not (N,3), empty, the number of normals does not match
cores, radius <= 0, max_depth <= 0 or min_points < 1.
Limits (honest): (1) the caller supplies the normals — the default sign of estimate_point_normals is centroid-based, so unless the orientation is made consistent with viewpoint or orient_normals the sign flips from place to place. (2) lod looks only at noise and does not include the registration residual (Lague's original paper adds the registration error as a separate term).
(3) At edges where few points fall inside the cylinder the result is nan — it does not return 0 to make it
look like "no change".
Reference (public): D. Lague, N. Brodu, J. Leroux, "Accurate 3D comparison of complex topography with terrestrial laser scanner: application to the Rangitikei canyon (N-Z)", ISPRS Journal of Photogrammetry and Remote Sensing 82 (2013) 10-26.
• blender_interop — Using Blender alongside — build the shape, measure it with fullseye (pitfalls: axes, units, ground truth)
• measurement_uncertainty — Measurement uncertainty and calibration — what it takes to claim you are measuring
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• metrics_eval — py -3.11 examples_3d/metrics_eval.py
signal as input)metrics)chamfer_distance · hausdorff_distance · fscore · rmse_correspondence · normal_consistency · voxel_iou · pose_error
*Provenance: metrics3d.py — 3D operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.