scikit-image
352 строки · 12.6 Кб
1import base642
3import numpy as np4
5from . import _marching_cubes_lewiner_luts as mcluts6from . import _marching_cubes_lewiner_cy7
8
9def marching_cubes(10volume,11level=None,12*,13spacing=(1.0, 1.0, 1.0),14gradient_direction='descent',15step_size=1,16allow_degenerate=True,17method='lewiner',18mask=None,19):20"""Marching cubes algorithm to find surfaces in 3d volumetric data.21
22In contrast with Lorensen et al. approach [2]_, Lewiner et
23al. algorithm is faster, resolves ambiguities, and guarantees
24topologically correct results. Therefore, this algorithm generally
25a better choice.
26
27Parameters
28----------
29volume : (M, N, P) ndarray
30Input data volume to find isosurfaces. Will internally be
31converted to float32 if necessary.
32level : float, optional
33Contour value to search for isosurfaces in `volume`. If not
34given or None, the average of the min and max of vol is used.
35spacing : length-3 tuple of floats, optional
36Voxel spacing in spatial dimensions corresponding to numpy array
37indexing dimensions (M, N, P) as in `volume`.
38gradient_direction : string, optional
39Controls if the mesh was generated from an isosurface with gradient
40descent toward objects of interest (the default), or the opposite,
41considering the *left-hand* rule.
42The two options are:
43* descent : Object was greater than exterior
44* ascent : Exterior was greater than object
45step_size : int, optional
46Step size in voxels. Default 1. Larger steps yield faster but
47coarser results. The result will always be topologically correct
48though.
49allow_degenerate : bool, optional
50Whether to allow degenerate (i.e. zero-area) triangles in the
51end-result. Default True. If False, degenerate triangles are
52removed, at the cost of making the algorithm slower.
53method: {'lewiner', 'lorensen'}, optional
54Whether the method of Lewiner et al. or Lorensen et al. will be used.
55mask : (M, N, P) array, optional
56Boolean array. The marching cube algorithm will be computed only on
57True elements. This will save computational time when interfaces
58are located within certain region of the volume M, N, P-e.g. the top
59half of the cube-and also allow to compute finite surfaces-i.e. open
60surfaces that do not end at the border of the cube.
61
62Returns
63-------
64verts : (V, 3) array
65Spatial coordinates for V unique mesh vertices. Coordinate order
66matches input `volume` (M, N, P). If ``allow_degenerate`` is set to
67True, then the presence of degenerate triangles in the mesh can make
68this array have duplicate vertices.
69faces : (F, 3) array
70Define triangular faces via referencing vertex indices from ``verts``.
71This algorithm specifically outputs triangles, so each face has
72exactly three indices.
73normals : (V, 3) array
74The normal direction at each vertex, as calculated from the
75data.
76values : (V,) array
77Gives a measure for the maximum value of the data in the local region
78near each vertex. This can be used by visualization tools to apply
79a colormap to the mesh.
80
81See Also
82--------
83skimage.measure.mesh_surface_area
84skimage.measure.find_contours
85
86Notes
87-----
88The algorithm [1]_ is an improved version of Chernyaev's Marching
89Cubes 33 algorithm. It is an efficient algorithm that relies on
90heavy use of lookup tables to handle the many different cases,
91keeping the algorithm relatively easy. This implementation is
92written in Cython, ported from Lewiner's C++ implementation.
93
94To quantify the area of an isosurface generated by this algorithm, pass
95verts and faces to `skimage.measure.mesh_surface_area`.
96
97Regarding visualization of algorithm output, to contour a volume
98named `myvolume` about the level 0.0, using the ``mayavi`` package::
99
100>>>
101>> from mayavi import mlab
102>> verts, faces, _, _ = marching_cubes(myvolume, 0.0)
103>> mlab.triangular_mesh([vert[0] for vert in verts],
104[vert[1] for vert in verts],
105[vert[2] for vert in verts],
106faces)
107>> mlab.show()
108
109Similarly using the ``visvis`` package::
110
111>>>
112>> import visvis as vv
113>> verts, faces, normals, values = marching_cubes(myvolume, 0.0)
114>> vv.mesh(np.fliplr(verts), faces, normals, values)
115>> vv.use().Run()
116
117To reduce the number of triangles in the mesh for better performance,
118see this `example
119<https://docs.enthought.com/mayavi/mayavi/auto/example_julia_set_decimation.html#example-julia-set-decimation>`_
120using the ``mayavi`` package.
121
122References
123----------
124.. [1] Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan
125Tavares. Efficient implementation of Marching Cubes' cases with
126topological guarantees. Journal of Graphics Tools 8(2)
127pp. 1-15 (december 2003).
128:DOI:`10.1080/10867651.2003.10487582`
129.. [2] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
130Resolution 3D Surface Construction Algorithm. Computer Graphics
131(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
132:DOI:`10.1145/37401.37422`
133"""
134use_classic = False135if method == 'lorensen':136use_classic = True137elif method != 'lewiner':138raise ValueError("method should be either 'lewiner' or 'lorensen'")139return _marching_cubes_lewiner(140volume,141level,142spacing,143gradient_direction,144step_size,145allow_degenerate,146use_classic=use_classic,147mask=mask,148)149
150
151def _marching_cubes_lewiner(152volume,153level,154spacing,155gradient_direction,156step_size,157allow_degenerate,158use_classic,159mask,160):161"""Lewiner et al. algorithm for marching cubes. See162marching_cubes_lewiner for documentation.
163
164"""
165
166# Check volume and ensure its in the format that the alg needs167if not isinstance(volume, np.ndarray) or (volume.ndim != 3):168raise ValueError('Input volume should be a 3D numpy array.')169if volume.shape[0] < 2 or volume.shape[1] < 2 or volume.shape[2] < 2:170raise ValueError("Input array must be at least 2x2x2.")171volume = np.ascontiguousarray(volume, np.float32) # no copy if not necessary172
173# Check/convert other inputs:174# level175if level is None:176level = 0.5 * (volume.min() + volume.max())177else:178level = float(level)179if level < volume.min() or level > volume.max():180raise ValueError("Surface level must be within volume data range.")181# spacing182if len(spacing) != 3:183raise ValueError("`spacing` must consist of three floats.")184# step_size185step_size = int(step_size)186if step_size < 1:187raise ValueError('step_size must be at least one.')188# use_classic189use_classic = bool(use_classic)190
191# Get LutProvider class (reuse if possible)192L = _get_mc_luts()193
194# Check if a mask array is passed195if mask is not None:196if not mask.shape == volume.shape:197raise ValueError('volume and mask must have the same shape.')198
199# Apply algorithm200func = _marching_cubes_lewiner_cy.marching_cubes201vertices, faces, normals, values = func(202volume, level, L, step_size, use_classic, mask203)204
205if not len(vertices):206raise RuntimeError('No surface found at the given iso value.')207
208# Output in z-y-x order, as is common in skimage209vertices = np.fliplr(vertices)210normals = np.fliplr(normals)211
212# Finishing touches to output213faces.shape = -1, 3214if gradient_direction == 'descent':215# MC implementation is right-handed, but gradient_direction is216# left-handed217faces = np.fliplr(faces)218elif not gradient_direction == 'ascent':219raise ValueError(220f"Incorrect input {gradient_direction} in `gradient_direction`, "221"see docstring."222)223if not np.array_equal(spacing, (1, 1, 1)):224vertices = vertices * np.r_[spacing]225
226if allow_degenerate:227return vertices, faces, normals, values228else:229fun = _marching_cubes_lewiner_cy.remove_degenerate_faces230return fun(vertices.astype(np.float32), faces, normals, values)231
232
233def _to_array(args):234shape, text = args235byts = base64.decodebytes(text.encode('utf-8'))236ar = np.frombuffer(byts, dtype='int8')237ar.shape = shape238return ar239
240
241# Map an edge-index to two relative pixel positions. The edge index
242# represents a point that lies somewhere in between these pixels.
243# Linear interpolation should be used to determine where it is exactly.
244# 0
245# 3 1 -> 0x
246# 2 xx
247
248# fmt: off
249EDGETORELATIVEPOSX = np.array([ [0,1],[1,1],[1,0],[0,0], [0,1],[1,1],[1,0],[0,0], [0,0],[1,1],[1,1],[0,0] ], 'int8')250EDGETORELATIVEPOSY = np.array([ [0,0],[0,1],[1,1],[1,0], [0,0],[0,1],[1,1],[1,0], [0,0],[0,0],[1,1],[1,1] ], 'int8')251EDGETORELATIVEPOSZ = np.array([ [0,0],[0,0],[0,0],[0,0], [1,1],[1,1],[1,1],[1,1], [0,1],[0,1],[0,1],[0,1] ], 'int8')252# fmt: on
253
254
255def _get_mc_luts():256"""Kind of lazy obtaining of the luts."""257if not hasattr(mcluts, 'THE_LUTS'):258mcluts.THE_LUTS = _marching_cubes_lewiner_cy.LutProvider(259EDGETORELATIVEPOSX,260EDGETORELATIVEPOSY,261EDGETORELATIVEPOSZ,262_to_array(mcluts.CASESCLASSIC),263_to_array(mcluts.CASES),264_to_array(mcluts.TILING1),265_to_array(mcluts.TILING2),266_to_array(mcluts.TILING3_1),267_to_array(mcluts.TILING3_2),268_to_array(mcluts.TILING4_1),269_to_array(mcluts.TILING4_2),270_to_array(mcluts.TILING5),271_to_array(mcluts.TILING6_1_1),272_to_array(mcluts.TILING6_1_2),273_to_array(mcluts.TILING6_2),274_to_array(mcluts.TILING7_1),275_to_array(mcluts.TILING7_2),276_to_array(mcluts.TILING7_3),277_to_array(mcluts.TILING7_4_1),278_to_array(mcluts.TILING7_4_2),279_to_array(mcluts.TILING8),280_to_array(mcluts.TILING9),281_to_array(mcluts.TILING10_1_1),282_to_array(mcluts.TILING10_1_1_),283_to_array(mcluts.TILING10_1_2),284_to_array(mcluts.TILING10_2),285_to_array(mcluts.TILING10_2_),286_to_array(mcluts.TILING11),287_to_array(mcluts.TILING12_1_1),288_to_array(mcluts.TILING12_1_1_),289_to_array(mcluts.TILING12_1_2),290_to_array(mcluts.TILING12_2),291_to_array(mcluts.TILING12_2_),292_to_array(mcluts.TILING13_1),293_to_array(mcluts.TILING13_1_),294_to_array(mcluts.TILING13_2),295_to_array(mcluts.TILING13_2_),296_to_array(mcluts.TILING13_3),297_to_array(mcluts.TILING13_3_),298_to_array(mcluts.TILING13_4),299_to_array(mcluts.TILING13_5_1),300_to_array(mcluts.TILING13_5_2),301_to_array(mcluts.TILING14),302_to_array(mcluts.TEST3),303_to_array(mcluts.TEST4),304_to_array(mcluts.TEST6),305_to_array(mcluts.TEST7),306_to_array(mcluts.TEST10),307_to_array(mcluts.TEST12),308_to_array(mcluts.TEST13),309_to_array(mcluts.SUBCONFIG13),310)311
312return mcluts.THE_LUTS313
314
315def mesh_surface_area(verts, faces):316"""Compute surface area, given vertices and triangular faces.317
318Parameters
319----------
320verts : (V, 3) array of floats
321Array containing coordinates for V unique mesh vertices.
322faces : (F, 3) array of ints
323List of length-3 lists of integers, referencing vertex coordinates as
324provided in `verts`.
325
326Returns
327-------
328area : float
329Surface area of mesh. Units now [coordinate units] ** 2.
330
331Notes
332-----
333The arguments expected by this function are the first two outputs from
334`skimage.measure.marching_cubes`. For unit correct output, ensure correct
335`spacing` was passed to `skimage.measure.marching_cubes`.
336
337This algorithm works properly only if the ``faces`` provided are all
338triangles.
339
340See Also
341--------
342skimage.measure.marching_cubes
343
344"""
345# Fancy indexing to define two vector arrays from triangle vertices346actual_verts = verts[faces]347a = actual_verts[:, 0, :] - actual_verts[:, 1, :]348b = actual_verts[:, 0, :] - actual_verts[:, 2, :]349del actual_verts350
351# Area of triangle in 3D = 1/2 * Euclidean norm of cross product352return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2.0353