m3c2_distance — 3D 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.

Usage

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.

Background guides (the physics and conventions behind this op)

• 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

References (sample data, literature)

• 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.

Runnable examples (verified samples that actually call this op)

• metrics_eval — py -3.11 examples_3d/metrics_eval.py

Ops the type connects to (they accept signal as input)

fuse_to_voxel

Same category (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.