Skip to content

fix: seg2mesh writes closed x/y/z meshes, and mesh2seg fills them back exactly (v2.0) - #134

Open
uermel wants to merge 1 commit into
v2.0from
uermel/seg2mesh-roundtrip-v2
Open

uermel wants to merge 1 commit into
v2.0from
uermel/seg2mesh-roundtrip-v2

Conversation

@uermel

@uermel uermel commented Oct 26, 2026

Copy link
Copy Markdown
Member

Port of the main-line fix to v2.0 (fixes #132 there too). It's the same change; the commit cherry-picks cleanly next to v2.0's get_level_array tomogram lookup.

See the main PR for the details and the real-data verification:

  • seg2mesh: x/y/z vertices, padding so meshes close at the volume edge, --individual-meshes implemented;
  • mesh2seg: rays off the voxel grid, voxels decided by their centres, crossings by parity, rays from before the mesh.

Test plan

  • pytest tests with copick 2.0.0-alpha.6 / zarr 3.4.1: 124 passed, including the 15 new tests
  • pre-commit (black, ruff)

🤖 Generated with Claude Code

…k exactly

seg2mesh (volume_to_mesh) ran marching cubes on the (z, y, x) array and
stored the vertices in that order, so its meshes were mirrored (x and z
swapped, triangles facing inward) relative to the tomogram and to every
other copick mesh: seg2slab, validbox, picks2mesh and mesh2seg all use
(x, y, z) in Å with voxel index i at i * spacing. It also did not pad the
volume, so a segmentation touching the volume's edge gave an open mesh.
Vertices are now reversed to (x, y, z), which also turns the triangles
outward, and the volume is padded by a voxel of background. Fixes #132.

seg2mesh --individual-meshes was documented but never reached the
converter (one mesh was stored under a literal "{instance_id}" session).
It now stores one mesh per label (label_meshes, each label's mask cropped
to its bounding box), with {instance_id} replaced by the label.

mesh2seg (watertight) cast its rays through the voxel centres, exactly
where marching-cubes meshes have their vertices and edges, so crossings
were counted twice or missed: a correct seg2mesh mesh of 10521 run 35962's
valid-sample segmentation came back with IoU 0.65. Ray origins are now
offset by ~0.015 voxel across the ray. Three more fixes in the same code:
- a crossing was assigned to its nearest voxel (np.round), which counts
  the voxel before the surface in where a ray enters and leaves out the
  last one inside where it exits; a voxel is now inside when its centre
  is (the first centre after each crossing);
- two crossings in the same voxel were applied once (fancy-index
  assignment with repeated indices); crossings now toggle by parity;
- rays started one voxel before the volume, so a mesh reaching further
  lost its entry crossing; they now start before the mesh.

On 10521 run 35962 (boundary-clean valid-sample), seg2mesh then mesh2seg
now gives back the segmentation exactly (IoU 1.0, was 0.11). The
published pipeline mesh still round-trips, now with voxels decided by
their centres: +242,057 / -739 voxels (0.5%) next to its surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant