Skip to content

Commit ae2f0ca

Browse files
Migrate mesh module
1 parent 4c51921 commit ae2f0ca

4 files changed

Lines changed: 125 additions & 24 deletions

File tree

elf/mesh/mesh.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional, Tuple
22

3-
import nifty
3+
import bioimage_cpp as bic
44
import numpy as np
55
from skimage.measure import marching_cubes as marching_cubes_impl
66

@@ -52,24 +52,4 @@ def smooth_mesh(
5252
The vertices after smoothing.
5353
The normals after smoothing.
5454
"""
55-
n_verts = len(verts)
56-
g = nifty.graph.undirectedGraph(n_verts)
57-
58-
edges = np.concatenate([faces[:, :2], faces[:, 1:], faces[:, ::2]], axis=0)
59-
g.insertEdges(edges)
60-
61-
current_verts = verts
62-
current_normals = normals
63-
new_verts = np.zeros_like(verts, dtype=verts.dtype)
64-
new_normals = np.zeros_like(normals, dtype=normals.dtype)
65-
66-
# Implement this directly in nifty for speed up?
67-
for it in range(iterations):
68-
for vert in range(n_verts):
69-
nbrs = np.array([vert] + [nbr[0] for nbr in g.nodeAdjacency(vert)], dtype="int")
70-
new_verts[vert] = np.mean(current_verts[nbrs], axis=0)
71-
new_normals[vert] = np.mean(current_normals[nbrs], axis=0)
72-
current_verts = new_verts
73-
current_normals = new_normals
74-
75-
return new_verts, new_normals
55+
return bic.utils.smooth_mesh(verts, normals, faces, iterations)

elf/mesh/mesh_to_segmentation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Optional, Tuple
44

55
import numpy as np
6-
import vigra
6+
from bioimage_cpp.segmentation import label as cc_label
77
from tqdm import tqdm
88

99
from elf.parallel import label
@@ -86,7 +86,7 @@ def mesh_to_segmentation(
8686
seg[coords] = 0
8787

8888
if block_shape is None:
89-
seg_out = vigra.analysis.labelVolumeWithBackground(seg) == 2
89+
seg_out = cc_label(seg, background=0, connectivity=1) == 2
9090
else:
9191
seg_out = np.zeros_like(seg)
9292
seg_out = label(seg, seg_out, with_background=True, block_shape=block_shape) == 2

test/mesh/test_mesh.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,69 @@ def test_marching_cubes_smoothing(self):
2222
self.assertGreater(len(faces), 0)
2323
self.assertGreater(len(normals), 0)
2424

25+
def test_marching_cubes_resolution(self):
26+
from elf.mesh import marching_cubes
27+
shape = (32,) * 3
28+
rng = np.random.default_rng(0)
29+
seg = (rng.random(shape) > 0.8).astype("uint32")
30+
31+
verts_unit, _, _ = marching_cubes(seg, resolution=(1.0, 1.0, 1.0))
32+
resolution = (2.0, 1.0, 0.5)
33+
verts_scaled, _, _ = marching_cubes(seg, resolution=resolution)
34+
35+
self.assertEqual(verts_unit.shape, verts_scaled.shape)
36+
expected = verts_unit * np.array(resolution)
37+
self.assertTrue(np.allclose(verts_scaled, expected))
38+
39+
def test_smooth_mesh_tetrahedron(self):
40+
from elf.mesh.mesh import smooth_mesh
41+
42+
verts = np.array(
43+
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
44+
dtype=np.float64,
45+
)
46+
normals = verts + 0.5
47+
faces = np.array(
48+
[[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]], dtype=np.int64
49+
)
50+
51+
out_verts, out_normals = smooth_mesh(verts, normals, faces, iterations=1)
52+
53+
self.assertEqual(out_verts.shape, verts.shape)
54+
self.assertEqual(out_normals.shape, normals.shape)
55+
self.assertEqual(out_verts.dtype, verts.dtype)
56+
self.assertEqual(out_normals.dtype, normals.dtype)
57+
58+
# Every vertex of a tetrahedron is connected to every other vertex,
59+
# so one Jacobi-Laplacian iteration collapses each vertex onto the
60+
# mean of all four vertices (the centroid).
61+
centroid_verts = verts.mean(axis=0)
62+
centroid_normals = normals.mean(axis=0)
63+
for i in range(len(verts)):
64+
self.assertTrue(np.allclose(out_verts[i], centroid_verts))
65+
self.assertTrue(np.allclose(out_normals[i], centroid_normals))
66+
67+
def test_smooth_mesh_no_op(self):
68+
from elf.mesh.mesh import smooth_mesh
69+
70+
verts = np.array(
71+
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
72+
dtype=np.float64,
73+
)
74+
normals = verts + 0.5
75+
faces = np.array(
76+
[[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]], dtype=np.int64
77+
)
78+
79+
out_verts, out_normals = smooth_mesh(verts, normals, faces, iterations=0)
80+
81+
self.assertEqual(out_verts.shape, verts.shape)
82+
self.assertEqual(out_normals.shape, normals.shape)
83+
self.assertEqual(out_verts.dtype, verts.dtype)
84+
self.assertEqual(out_normals.dtype, normals.dtype)
85+
self.assertTrue(np.array_equal(out_verts, verts))
86+
self.assertTrue(np.array_equal(out_normals, normals))
87+
2588

2689
if __name__ == '__main__':
2790
unittest.main()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
import tempfile
3+
import unittest
4+
5+
import numpy as np
6+
7+
try:
8+
import madcad # noqa: F401
9+
except ImportError:
10+
madcad = None
11+
12+
13+
def _make_sphere(shape, radius):
14+
z, y, x = np.indices(shape)
15+
center = np.array(shape) // 2
16+
dist = np.sqrt(
17+
(z - center[0]) ** 2 + (y - center[1]) ** 2 + (x - center[2]) ** 2
18+
)
19+
return (dist <= radius).astype("uint8")
20+
21+
22+
@unittest.skipIf(madcad is None, "Needs madcad")
23+
class TestMeshToSegmentation(unittest.TestCase):
24+
def _roundtrip(self, block_shape):
25+
from elf.mesh import marching_cubes
26+
from elf.mesh.io import write_obj
27+
from elf.mesh.mesh_to_segmentation import mesh_to_segmentation
28+
29+
shape = (32, 32, 32)
30+
seg = _make_sphere(shape, radius=10)
31+
verts, faces, _ = marching_cubes(seg)
32+
33+
with tempfile.NamedTemporaryFile(suffix=".obj", delete=False) as f:
34+
obj_path = f.name
35+
try:
36+
write_obj(obj_path, verts, faces)
37+
recovered = mesh_to_segmentation(
38+
obj_path, shape=shape, block_shape=block_shape
39+
)
40+
finally:
41+
if os.path.exists(obj_path):
42+
os.remove(obj_path)
43+
44+
# The recovered mask should agree with the original sphere on the
45+
# interior up to a thin surface tolerance.
46+
diff = recovered.astype(bool) ^ seg.astype(bool)
47+
# Number of disagreements should be small relative to the sphere volume.
48+
self.assertLess(diff.sum(), seg.sum() * 0.5)
49+
50+
def test_mesh_to_segmentation_serial(self):
51+
self._roundtrip(block_shape=None)
52+
53+
def test_mesh_to_segmentation_blockwise(self):
54+
self._roundtrip(block_shape=(16, 16, 16))
55+
56+
57+
if __name__ == "__main__":
58+
unittest.main()

0 commit comments

Comments
 (0)