Skip to content

Commit 67386c7

Browse files
feat: chunk_mesh (#44)
* wip: mesh chunking (for creating multiple LOD) * fix: running, though the algorithm is not perfect yet * fix: off by one error * fix: use C order * fix: rounding errors * fix: allow C or F order * feat: return grid associations * fix+perf: mesh.id, switch to all vectors * test: add basic tests * fix: getting closer to correctly fixing x direction faces * fix: able to fix single outliers 6-connected * refactor: rename to 6 connected * refactor: add_point and add_triangle to meshobject * refactor: add const to verts * fix: support 18 connected * wip: making progress on dealing with special cases * fix: a lot closer to working (cube looks ok) * fix: corners in 18-connected * fix: precision 3 * fix: sanity check that xaxis is not same as yaxis * fix: operation should be ceil not rounding * fix: remove epsilon subtraction * feat: add concatenate and consolidate to Mesh * fix: discard not-implemented 3d all different * refactor: use a single intersection function * wip: adding support for 26-connected cases * fix: rounding->ceil on grid size * fix: consolidate calling wrong name of is_empty * fix: handle last_face when there are zero verts * fix: remove debugging statements * feat: better error messages * fix: bug in 18-connected code * perf: faster triangle calculation * test: add basic chunking test * fix: correct face winding for 6-connected * perf: faster face id calculation for 6 connected * fix: face winding for one case in all_different * fix: improve windings for 18 connected Not perfect, but better? * chore: update test.py into something more useful * fix: improve windings for close * feat: automatic conversion to numpy * fix: safety checks before unsafe array access * fix: inverted windings * fix: more inverted windings * fix: generalize code to handle arbitrary meshes * fix: two situations for dividing a triangle * install: fix license metadata and supported python versions * chore: add namespace to zmesh files * fix: disallow zero or negative chunk sizes * fix: handle degenerate intersections * fix: handle degenerate cases * wip: attempting to fix the winding logic getting close... * fix: rejigger below * fix: incorrect initial offset for faces * wip: experimenting with a simplified implementation * fix: fix normals for simple triangle * fix: check if normals are ccw or cw * fix: possibly fixed normals * perf: only compute normal when needed * refactor: add hat and rename norm to len to Vec3 * refactor: remove premature optimizations * chore: remove dead code * fix: remove degenerate faces during consolidate * refactor: move remove_degenerate_faces into its own function * test: check for unreferenced vertices * feat: add remove_unreferenced_vertices * install: add fastremap to requirements * docs: add docstring to remove_degenerate_faces * feat: add removing disconnected vertices to consolidate * perf: remove unneeded copies * feat: add merge_close_vertices function * perf(remove_unreferenced_vertices): much more efficient remapping * perf(consolidate): remove unnecessary vectorization * perf: speed up normal remapping
1 parent 16a476d commit 67386c7

File tree

8 files changed

+844
-55
lines changed

8 files changed

+844
-55
lines changed

automated_test.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,4 +301,82 @@ def test_min_error_skip(reduction_factor):
301301
assert abs(factor - reduction_factor) < 1
302302

303303

304+
def test_chunk_shape():
305+
labels = np.zeros( (10, 10, 10), dtype=np.uint32)
306+
labels[1:-1, 1:-1, 1:-1] = 1
307+
308+
mesher = zmesh.Mesher( (1, 1, 1) )
309+
mesher.mesh(labels)
310+
mesh = mesher.get_mesh(1, normals=False, simplification_factor=0, max_simplification_error=100)
311+
312+
meshes = zmesh.chunk_mesh(
313+
mesh,
314+
[5.,5.,5.],
315+
)
316+
317+
assert len(meshes) == 8
318+
assert not any([
319+
m.is_empty() for m in meshes.values()
320+
])
321+
322+
def test_delete_unreference_vertices():
323+
vertices = [
324+
[0,0,0],
325+
[0,1,0],
326+
[1,0,0],
327+
[5,5,5],
328+
[7,7,7],
329+
[8,7,7],
330+
[7,8,8],
331+
]
332+
faces = [[0,1,2],[4,5,6]]
333+
334+
mesh = zmesh.Mesh(vertices=vertices, faces=faces, normals=None)
335+
res = mesh.remove_unreferenced_vertices()
336+
337+
ans_verts = vertices[:3] + vertices[4:]
338+
ans_faces = [[0,1,2],[3,4,5]]
339+
340+
assert res.vertices.shape == (6,3)
341+
assert res.faces.shape == (2,3)
342+
assert np.all(res.vertices == np.array(ans_verts))
343+
assert np.all(res.faces == np.array(ans_faces))
344+
345+
def test_chunk_mesh_triangle():
346+
vertices = [
347+
[0,0,0],
348+
[0,1,0],
349+
[1,0,0],
350+
]
351+
faces = [[0,1,2]]
352+
353+
mesh = zmesh.Mesh(vertices=vertices, faces=faces, normals=None)
354+
355+
meshes = zmesh.chunk_mesh(mesh, [.5,.5,.5])
356+
357+
meshes = [ m for m in meshes.values() if not m.is_empty() ]
358+
359+
assert len(meshes) == 3
360+
361+
m = zmesh.Mesh.concatenate(*meshes).consolidate()
362+
363+
assert m.vertices.shape[0] == 6
364+
365+
assert [0,0,0] in m.vertices
366+
assert [1,0,0] in m.vertices
367+
assert [0,1,0] in m.vertices
368+
assert [0,0.5,0] in m.vertices
369+
assert [0.5,0,0] in m.vertices
370+
assert [0.5,0.5,0] in m.vertices
371+
372+
373+
374+
375+
376+
377+
378+
379+
380+
381+
304382

dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
cython
2+
fastremap
23
pytest
34
Jinja2
45
tox

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description_file = README.md
77
author = William Silversmith (maintainer), Aleks Zlateski (original author)
88
author_email = [email protected]
99
home_page = https://github.com/seung-lab/zmesh/
10-
license = GPL-3.0+
10+
license = LGPL-3.0-or-later
1111
classifier =
1212
Intended Audience :: Developers
1313
Development Status :: 5 - Production/Stable

test.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import numpy as np
22
import zmesh
33

4-
labels = np.zeros( (50, 50, 50), dtype=np.uint32)
4+
labels = np.zeros( (20, 20, 20), dtype=np.uint32)
55
labels[1:-1, 1:-1, 1:-1] = 1
66

7-
mesher = zmesh.Mesher( (3.141, 1, 1) )
7+
mesher = zmesh.Mesher( (1, 1, 1) )
88
mesher.mesh(labels)
9-
mesh = mesher.get_mesh(1, normals=False, simplification_factor=100, max_simplification_error=100)
9+
mesh = mesher.get_mesh(1, normals=False, simplification_factor=0, max_simplification_error=100)
1010

11-
print(mesh)
1211

13-
with open('wow.obj', 'bw') as f:
14-
f.write(mesh.to_obj())
15-
16-
with open('wow.ply', 'bw') as f:
17-
f.write(mesh.to_ply())
12+
meshes = zmesh.chunk_mesh(
13+
mesh,
14+
[10.,10.,10.],
15+
)
16+
m = zmesh.Mesh.concatenate(*list(meshes.values()))
1817

18+
with open('cube.obj', 'bw') as f:
19+
f.write(m.to_obj())

zmesh/_zmesh.pyx

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# distutils: language = c++
33
import cython
44

5+
from typing import List, Tuple, Sequence
6+
57
from libc.stdint cimport uint64_t, uint32_t, uint16_t, uint8_t
68
from libcpp.vector cimport vector
79
from libcpp cimport bool
@@ -14,12 +16,25 @@ cimport numpy as cnp
1416
import numpy as np
1517
from zmesh.mesh import Mesh
1618

17-
cdef extern from "cMesher.hpp":
19+
cdef extern from "utility.hpp" namespace "zmesh::utility":
1820
cdef struct MeshObject:
1921
vector[float] points
2022
vector[float] normals
2123
vector[unsigned int] faces
2224

25+
cdef vector[MeshObject] chunk_mesh_accelerated(
26+
float* vertices, uint64_t num_vertices,
27+
unsigned int* faces, uint64_t num_faces,
28+
float cx, float cy, float cz
29+
) except +
30+
31+
cdef vector[MeshObject] chunk_mesh_accelerated(
32+
float* vertices, uint64_t num_vertices,
33+
unsigned int* faces, uint64_t num_faces,
34+
float cx, float cy, float cz
35+
) except +
36+
37+
cdef extern from "cMesher.hpp" namespace "zmesh":
2338
cdef cppclass CMesher[P,L,S]:
2439
CMesher(vector[float] voxel_res) except +
2540
void mesh(
@@ -51,6 +66,78 @@ cdef extern from "cMesher.hpp":
5166
void clear()
5267
P pack_coords(P x, P y, P z)
5368

69+
def chunk_mesh(
70+
mesh:Mesh,
71+
chunk_size:Sequence[float],
72+
) -> Dict[Tuple[int,int,int], Mesh]:
73+
74+
vert_order = 'C' if mesh.vertices.flags.c_contiguous else 'F'
75+
face_order = 'C' if mesh.faces.flags.c_contiguous else 'F'
76+
77+
cdef cnp.ndarray[float] vertices = mesh.vertices.reshape([mesh.vertices.size], order=vert_order)
78+
cdef cnp.ndarray[unsigned int] faces = mesh.faces.reshape([mesh.faces.size], order=face_order)
79+
80+
if vertices.size % 3 != 0:
81+
raise ValueError(f"Invalid vertex array. Must be a multiple of 3. Got: {vertices.size}")
82+
83+
if faces.size % 3 != 0:
84+
raise ValueError(f"Invalid faces array. Must be a multiple of 3. Got: {faces.size}")
85+
86+
cdef vector[MeshObject] objs = chunk_mesh_accelerated(
87+
<float*>&vertices[0], mesh.vertices.shape[0],
88+
<unsigned int*>&faces[0], mesh.faces.shape[0],
89+
chunk_size[0], chunk_size[1], chunk_size[2]
90+
)
91+
92+
def norm(msh):
93+
points = np.array(msh['points'], dtype=np.float32)
94+
Nv = points.size // 3
95+
Nf = len(msh['faces']) // 3
96+
97+
points = points.reshape(Nv, 3)
98+
faces = np.array(msh['faces'], dtype=np.uint32).reshape(Nf, 3)
99+
m = Mesh(points, faces, None)
100+
if hasattr(msh, 'id'):
101+
m.id = msh.id
102+
return m
103+
104+
minpt = np.min(mesh.vertices, axis=0)
105+
maxpt = np.max(mesh.vertices, axis=0)
106+
107+
grid_size = np.ceil((maxpt - minpt) / np.array(chunk_size)).astype(int)
108+
grid_size = np.maximum(grid_size, [1,1,1])
109+
110+
chunked_meshes = {}
111+
i = 0
112+
for gz in range(grid_size[2]):
113+
for gy in range(grid_size[1]):
114+
for gx in range(grid_size[0]):
115+
chunked_meshes[(gx,gy,gz)] = norm(objs[i])
116+
i += 1
117+
118+
return chunked_meshes
119+
120+
def _normalize_mesh(mesh, voxel_centered, physical, resolution):
121+
"""Convert a MeshObject into a zmesh.Mesh."""
122+
points = np.array(mesh['points'], dtype=np.float32)
123+
Nv = points.size // 3
124+
Nf = len(mesh['faces']) // 3
125+
126+
points = points.reshape(Nv, 3)
127+
if not physical:
128+
points *= resolution
129+
130+
if voxel_centered:
131+
points += resolution
132+
points /= 2.0
133+
faces = np.array(mesh['faces'], dtype=np.uint32).reshape(Nf, 3)
134+
135+
normals = None
136+
if mesh['normals']:
137+
normals = np.array(mesh['normals'], dtype=np.float32).reshape(Nv, 3)
138+
139+
return Mesh(points, faces, normals)
140+
54141
class Mesher:
55142
"""
56143
Represents a meshed volume.
@@ -150,7 +237,7 @@ class Mesher:
150237
transpose=True
151238
)
152239

153-
return self._normalize_simplified_mesh(mesh, voxel_centered, physical=True)
240+
return _normalize_mesh(mesh, voxel_centered, physical=True, resolution=self.voxel_res)
154241

155242
@cython.binding(True)
156243
def get(
@@ -183,30 +270,10 @@ class Mesher:
183270
transpose=False
184271
)
185272

186-
mesh = self._normalize_simplified_mesh(mesh, voxel_centered, physical=True)
273+
mesh = _normalize_mesh(mesh, voxel_centered, physical=True, resolution=self.voxel_res)
187274
mesh.id = int(label)
188275
return mesh
189276

190-
def _normalize_simplified_mesh(self, mesh, voxel_centered, physical):
191-
points = np.array(mesh['points'], dtype=np.float32)
192-
Nv = points.size // 3
193-
Nf = len(mesh['faces']) // 3
194-
195-
points = points.reshape(Nv, 3)
196-
if not physical:
197-
points *= self.voxel_res
198-
199-
if voxel_centered:
200-
points += self.voxel_res
201-
points /= 2.0
202-
faces = np.array(mesh['faces'], dtype=np.uint32).reshape(Nf, 3)
203-
204-
normals = None
205-
if mesh['normals']:
206-
normals = np.array(mesh['normals'], dtype=np.float32).reshape(Nv, 3)
207-
208-
return Mesh(points, faces, normals)
209-
210277
@cython.binding(True)
211278
def compute_normals(self, mesh):
212279
"""
@@ -305,7 +372,7 @@ class Mesher:
305372
for i in range(len(result.points)):
306373
result.points[i] += min_vertex
307374

308-
return self._normalize_simplified_mesh(result, voxel_centered, physical=False)
375+
return _normalize_mesh(result, voxel_centered, physical=False, resolution=self.voxel_res)
309376

310377
def clear(self):
311378
self._mesher.clear()

zmesh/cMesher.hpp

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
#include <zi/mesh/quadratic_simplifier.hpp>
88
#include <zi/vl/vec.hpp>
99

10-
struct MeshObject {
11-
std::vector<float> points;
12-
std::vector<float> normals;
13-
std::vector<unsigned int> faces;
14-
};
10+
#include "utility.hpp" // includes MeshObject def'n
11+
12+
namespace zmesh {
1513

1614
template <typename PositionType, typename LabelType, typename SimplifierType>
1715
class CMesher {
@@ -49,7 +47,7 @@ class CMesher {
4947
return marchingcubes_.pack_coords(x,y,z);
5048
}
5149

52-
MeshObject get_mesh(
50+
zmesh::utility::MeshObject get_mesh(
5351
LabelType segid,
5452
bool generate_normals,
5553
int simplification_factor,
@@ -60,7 +58,7 @@ class CMesher {
6058

6159
// MC produces no triangles if either
6260
// none or all voxels were labeled.
63-
MeshObject empty_obj;
61+
zmesh::utility::MeshObject empty_obj;
6462
if (marchingcubes_.count(segid) == 0) {
6563
return empty_obj;
6664
}
@@ -77,7 +75,7 @@ class CMesher {
7775
);
7876
}
7977

80-
MeshObject simplify(
78+
zmesh::utility::MeshObject simplify(
8179
const std::vector< zi::vl::vec< PositionType, 3> >& triangles,
8280
bool generate_normals,
8381
int simplification_factor,
@@ -89,7 +87,7 @@ class CMesher {
8987
bool transpose = true
9088
) {
9189

92-
MeshObject obj;
90+
zmesh::utility::MeshObject obj;
9391

9492
zi::mesh::int_mesh<PositionType, LabelType> im;
9593
im.add(triangles);
@@ -177,7 +175,7 @@ class CMesher {
177175
return obj;
178176
}
179177

180-
MeshObject simplify_points(
178+
zmesh::utility::MeshObject simplify_points(
181179
const uint64_t* points,
182180
const size_t Nv,
183181
bool generate_normals,
@@ -219,5 +217,6 @@ class CMesher {
219217
}
220218
};
221219

220+
};
222221

223222
#endif

0 commit comments

Comments
 (0)