Skip to content

Commit 8d24891

Browse files
inhuszarclaude
andcommitted
Fix [0,0,0] "bullet hole" artefact in spherical barycentric resampling
barycentric_spherical_map() could leave a few target vertices unmapped, collapsing their interpolated value to exactly [0, 0, 0]. Two causes: 1. Search neighbourhood too small. Only the `neighborhood` (=10) nearest source-triangle centroids were searched per target point. Registration (e.g. mris_register) can distort triangle areas by >200x on the sphere, pushing a target point's true containing triangle past the nearest 10 centroids (observed rank 11-12), so it was never found. Default bumped to 30 (negligible cost) so the correct triangle is found and exact barycentric weights are used. 2. Fallback guard tested 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 "any points still unassigned". When the last iteration matched nothing, the fallback was skipped and those points kept face == -1 with zero weights. Downstream, `source.faces[-1]` negative-indexes the last face and the zero weights yield a sample of exactly [0, 0, 0]. Now guard on the actual `intersecting_faces == -1` mask so the fallback always runs. Effect: a vertex teleported to the origin, with its 1-ring neighbours stretched up to ~32 mm. Reproduced deterministically on dHCP white-matter surfaces resampled to a 245k-vertex canonical topology; 56/1121 subjects had >=1 such hole in the left white surface alone. With the fix, output is unchanged for all correctly-mapped vertices and the holes disappear. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c6cddec commit 8d24891

1 file changed

Lines changed: 7 additions & 7 deletions

File tree

surfa/mesh/sphere.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def spherical_to_cartesian(points):
113113
return np.stack([x, y, z], axis=1)
114114

115115

116-
def barycentric_spherical_map(source, target, neighborhood=10):
116+
def barycentric_spherical_map(source, target, neighborhood=30):
117117
"""
118118
Map the points of a target sphere to the barycentric coordinates of the nearest
119119
triangle on a source sphere.
@@ -125,7 +125,7 @@ def barycentric_spherical_map(source, target, neighborhood=10):
125125
target : Mesh
126126
Target sphere mesh.
127127
neighborhood : int, optional
128-
Max number of nearest triangles to consider for each target point.
128+
Max number of nearest triangles to consider for each target point.
129129
130130
Returns
131131
-------
@@ -214,12 +214,12 @@ def barycentric_spherical_map(source, target, neighborhood=10):
214214
intersecting_faces[remaining] = faces[hits]
215215
intersecting_barycenters[remaining] = barycentric[hits]
216216

217-
# if any target points were not assigned a face, just assign them the
218-
# center of the nearest face
219-
if np.count_nonzero(remaining) != 0:
220-
missing = intersecting_faces == -1
217+
# If any target points were not assigned a face,
218+
# fall back to the nearest face (barycentric centroid).
219+
missing = intersecting_faces == -1
220+
if np.count_nonzero(missing) != 0:
221221
intersecting_faces[missing] = nearest[0][missing]
222-
intersecting_barycenters[missing] = 0.33333333
222+
intersecting_barycenters[missing] = 1.0 / 3.0
223223

224224
return intersecting_faces, intersecting_barycenters
225225

0 commit comments

Comments
 (0)