register_fpfh — 3D feature_register op

• Data kinds: points × points → pose

• Call: import fullseye as fs; fs.ledger.register_fpfh(src, dst, src_normals=None, dst_normals=None, voxel_size=None, normal_k=16, feature_k=60, n_bins=11, ransac_iters=8000, inlier_thr=None, edge_sim=0.9, mutual=True, ratio=0.95, seed=0, device='cpu') (to call the implementation directly, import feat_fpfh; feat_fpfh.register_fpfh(src, dst, src_normals=None, dst_normals=None, voxel_size=None, normal_k=16, feature_k=60, n_bins=11, ransac_iters=8000, inlier_thr=None, edge_sim=0.9, mutual=True, ratio=0.95, seed=0, device='cpu'); from the registry, ops3d.get("register_fpfh"))

Usage

Estimate the rigid alignment (R,t) without an initial guess using FPFH descriptors and RANSAC.

The 3D version of sparse feature matching, corresponding to Harris/SIFT in 2D. Both clouds are represented by FPFH descriptors, correspondences are set up by nearest neighbours in descriptor space (filtered by mutual nearest neighbour + Lowe ratio), and RANSAC solves a rigid pose robust to outliers. Unlike dense matching (NCC/Hough) or PCA, its value is that it yields the pose without an initial estimate even under large rotation (50-70°) +translation + partial overlap. The obtained (R,t) can be passed to ICP as the initial value (coarse init) to tighten to surface accuracy (ICP convention dst ≈ src @ R.T + t).

Pipeline:

1) voxel downsampling (brings the neighbourhoods of 2 independently sampled clouds to a common resolution; an essential preprocessing step that strongly affects the FPFH correspondence accuracy).

2) normal estimation (local PCA). If src_normals/dst_normals are passed, they are used.

3) FPFH descriptor (SPFH → distance-weighted combination).

4) descriptor NN matching (mutual + ratio).

5) RANSAC: 3-point sample → edge-length consistency prefilter → Kabsch → scoring by all-point fitness (the fraction of all src points that, once transformed, have their dst nearest neighbour within the threshold). Scoring only the correspondences (~100 points) tends to stick to wrong poses, so all-point fitness makes it robust.

6) refit on the correspondence inliers of the best pose.

Args:

src (N,3), dst (M,3): the 2 point clouds to register (numpy/torch).

src_normals, dst_normals ((N,3)/(M,3) or None): precomputed normals. None means internal estimation

(but when voxel_size>0 the coordinates change, so they are always re-estimated).

voxel_size (float or None): downsampling edge length. None automatically uses dst resolution ×2.5.

0/None disables it (raw point clouds as is; not recommended for independent sampling).

normal_k (int): number of neighbours for normal estimation.

feature_k (int): number of FPFH neighbours (larger makes the descriptor more stable and discriminative; around 60 recommended).

n_bins (int): histogram bins per 1 feature (descriptor dimension = 3*n_bins).

ransac_iters (int): number of RANSAC iterations.

inlier_thr (float or None): inlier distance threshold. None uses (post-downsampling) resolution ×3.

edge_sim (float): tolerance of the edge-length ratio of triples (0<edge_sim<=1; the closer to 1, the stricter).

mutual (bool): mutual nearest-neighbour filter. ratio (float or None): Lowe ratio.

seed (int): RANSAC random seed (change it on restart). device (str): device of the returned tensors.

Returns:

R (3,3) torch.Tensor (on device), t (3,) torch.Tensor,

info dict: {"n_corr" (number of correspondences), "inliers" (number of inliers), "inlier_ratio",

"fitness" (all-point fitness = the criterion for choosing among restarts), "rmse" (inlier RMSE),

"inlier_thr", "src_corr","dst_corr" (correspondence indices)}.

Notes (honest): FPFH is coarse registration; under hard conditions of independent sampling + noise + partial overlap a rotation error of a few degrees remains (measured: with 62° rotation, overlap ~64% and noise 0.5× resolution, after RANSAC the median is ~4.4°, ~90% are <8°, and the remaining ~10% sit at the 8-9° boundary. Tightening with ICP brings 100% to <8° with a median of ~0.5°). Once the noise is on the order of the point spacing (resolution) or larger, the normals and angular features break down and the descriptor loses its discriminative power (measured: the success rate drops at noise ≥1.0× resolution). RANSAC is randomised, so in practice restart it a few times and take the result with the largest info["fitness"].

Background guides (the physics and conventions behind this op)

• blas_threads_and_memory — Why matrix decompositions are slow — BLAS threads, caches, memory layout

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)

• feature_register — py -3.11 examples_3d/feature_register.py

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

fuse_to_voxel · pose_error · bundle_adjust · mean_reprojection_error · optimize_pose_graph · relative_pose · mean_edge_error · rotation_translation_error

Same category (feature_register)

harris3d_keypoints · iss_keypoints · compute_fpfh · shot_descriptor · register_spin · register_shot


*Provenance: feat_fpfh.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.