Skip to content

Fix [0,0,0] "bullet hole" artefact in spherical barycentric resampling#77

Merged
jnolan14 merged 1 commit into
freesurfer:masterfrom
inhuszar:fix/barycentric-spherical-map-bullet-hole
May 22, 2026
Merged

Fix [0,0,0] "bullet hole" artefact in spherical barycentric resampling#77
jnolan14 merged 1 commit into
freesurfer:masterfrom
inhuszar:fix/barycentric-spherical-map-bullet-hole

Conversation

@inhuszar
Copy link
Copy Markdown
Contributor

Summary

barycentric_spherical_map() can leave a few target vertices unmapped, which
collapses their interpolated value to exactly [0, 0, 0]. When the map is
used to resample mesh coordinates (e.g. white/pial surfaces onto a canonical
topology), the affected vertices are teleported to the origin and their 1-ring
neighbours are stretched by tens of millimetres — a "bullet hole" in the surface.

Root cause (two compounding bugs)

  1. Search neighbourhood too small. Only the neighborhood (=10) nearest
    source-triangle centroids are searched per target point. Registration
    (e.g. mris_register) can distort triangle areas by >200× on the sphere,
    pushing a target point's true containing triangle past the nearest 10
    centroids (observed rank 11–12), so it is never found.
  2. Fallback guard tests the wrong mask. The "assign unmatched points to the
    nearest face" fallback was guarded by np.count_nonzero(remaining), where
    remaining is the leftover mask from the final loop iteration, not "are any
    points still unassigned". When the last iteration matched nothing, the
    fallback was skipped and those points kept face == -1 with zero weights.
  3. Consequence. SphericalResamplingBarycentric then does source.faces[faces]
    with faces == -1, so NumPy negative-indexes the last face, and the zero
    weights make sample() return exactly [0, 0, 0].

Fix

  • barycentric_spherical_map default neighborhood 10 → 30 (negligible cost; the
    correct containing triangle is found, giving exact barycentric weights).
  • Guard the fallback on the actual intersecting_faces == -1 mask so it always
    runs — no point can be left with a -1 face / zero weights.

barycentric_spherical_map() could leave some target vertices unmapped,
collapsing their interpolated value to exactly [0, 0, 0]. Two fixes:

- Increase the default centroid search neighbourhood from 10 to 30.
  Registration can distort triangle areas enough that a target point's
  true containing triangle falls outside the nearest 10 centroids.

- Guard the nearest-face fallback on the actual unmatched mask
  (intersecting_faces == -1) rather than the leftover loop mask, so the
  fallback always runs and no point is left with face == -1 and zero
  weights (which produced the [0, 0, 0] sample).
@inhuszar inhuszar force-pushed the fix/barycentric-spherical-map-bullet-hole branch from 8d24891 to bc523b6 Compare May 22, 2026 03:44
@jnolan14 jnolan14 merged commit b19e2dd into freesurfer:master May 22, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants