Coverage for trimesh/voxel/morphology.py: 80%
59 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""Basic morphology operations that create new encodings."""
3import numpy as np
5from .. import util
6from ..constants import log_time
7from . import encoding as enc
8from . import ops
10try:
11 from scipy import ndimage
12except BaseException as E:
13 # scipy is a soft dependency
14 from ..exceptions import ExceptionWrapper
16 ndimage = ExceptionWrapper(E)
19def _dense(encoding, rank=None):
20 if isinstance(encoding, np.ndarray):
21 dense = encoding
22 elif isinstance(encoding, enc.Encoding):
23 dense = encoding.dense
24 else:
25 raise ValueError(f"encoding must be np.ndarray or Encoding, got {encoding!s}")
26 if rank:
27 _assert_rank(dense, rank)
28 return dense
31def _sparse_indices(encoding, rank=None):
32 if isinstance(encoding, np.ndarray):
33 sparse_indices = encoding
34 elif isinstance(encoding, enc.Encoding):
35 sparse_indices = encoding.sparse_indices
36 else:
37 raise ValueError(f"encoding must be np.ndarray or Encoding, got {encoding!s}")
39 _assert_sparse_rank(sparse_indices, 3)
40 return sparse_indices
43def _assert_rank(value, rank):
44 if len(value.shape) != rank:
45 raise ValueError("Expected rank %d, got shape %s", rank, str(value.shape))
48def _assert_sparse_rank(value, rank=None):
49 if len(value.shape) != 2:
50 raise ValueError(f"sparse_indices must be rank 2, got shape {value.shape!s}")
51 if rank is not None:
52 if value.shape[-1] != rank:
53 raise ValueError(
54 "sparse_indices.shape[1] must be %d, got %d", rank, value.shape[-1]
55 )
58@log_time
59def fill_base(encoding):
60 """
61 Given a sparse surface voxelization, fill in between columns.
63 Parameters
64 --------------
65 encoding: Encoding object or sparse array with shape (?, 3)
67 Returns
68 --------------
69 A new filled encoding object.
70 """
71 return enc.SparseBinaryEncoding(ops.fill_base(_sparse_indices(encoding, rank=3)))
74@log_time
75def fill_orthographic(encoding):
76 """
77 Fill the given encoding by orthographic projection method.
79 Any voxel in the dense representation with no free ray along the x, y, z
80 axes in each direction is assigned filled. This is likely faster than fill
81 holes, and is more stable with regards to small holes.
83 Parameters
84 --------------
85 encoding: Encoding object or dense rank-3 array.
87 Returns
88 --------------
89 A new filled encoding object.
90 """
91 return enc.DenseEncoding(ops.fill_orthographic(_dense(encoding, rank=3)))
94@log_time
95def fill_holes(encoding, **kwargs):
96 """
97 Encoding wrapper around scipy.ndimage.morphology.binary_fill_holes.
99 https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_fill_holes.html#scipy.ndimage.morphology.binary_fill_holes
101 Parameters
102 --------------
103 encoding: Encoding object or dense rank-3 array.
104 **kwargs: see scipy.ndimage.morphology.binary_fill_holes.
106 Returns
107 --------------
108 A new filled in encoding object.
109 """
110 return enc.DenseEncoding(
111 ndimage.binary_fill_holes(_dense(encoding, rank=3), **kwargs)
112 )
115fillers = util.FunctionRegistry(
116 base=fill_base,
117 orthographic=fill_orthographic,
118 holes=fill_holes,
119)
122def fill(encoding, method="base", **kwargs):
123 """
124 Fill the given encoding using the specified implementation.
126 See `fillers` for available implementations or to add your own, e.g. via
127 `fillers['custom_key'] = custom_fn`.
129 `custom_fn` should have signature `(encoding, **kwargs) -> filled_encoding`
130 and should not modify encoding.
132 Parameters
133 --------------
134 encoding: Encoding object (left unchanged).
135 method: method present in `fillers`.
136 **kwargs: additional kwargs passed to the specified implementation.
138 Returns
139 --------------
140 A new filled Encoding object.
141 """
142 return fillers(method, encoding=encoding, **kwargs)
145def binary_dilation(encoding, **kwargs):
146 """
147 Encoding wrapper around scipy.ndimage.morphology.binary_dilation.
149 https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_dilation.html#scipy.ndimage.morphology.binary_dilation
150 """
151 return enc.DenseEncoding(ndimage.binary_dilation(_dense(encoding, rank=3), **kwargs))
154def binary_closing(encoding, **kwargs):
155 """
156 Encoding wrapper around scipy.ndimage.morphology.binary_closing.
158 https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.morphology.binary_closing.html#scipy.ndimage.morphology.binary_closing
159 """
160 return enc.DenseEncoding(ndimage.binary_closing(_dense(encoding, rank=3), **kwargs))
163def surface(encoding, structure=None):
164 """
165 Get elements on the surface of encoding.
167 A surface element is any one in encoding that is adjacent to an empty
168 voxel.
170 Parameters
171 --------------
172 encoding: Encoding or dense rank-3 array
173 structure: adjacency structure. If None, square connectivity is used.
175 Returns
176 --------------
177 new surface Encoding.
178 """
179 dense = _dense(encoding, rank=3)
180 # padding/unpadding resolves issues with occupied voxels on the boundary
181 dense = np.pad(dense, np.ones((3, 2), dtype=int), mode="constant")
182 empty = np.logical_not(dense)
183 dilated = ndimage.binary_dilation(empty, structure=structure)
184 surface = np.logical_and(dense, dilated)[1:-1, 1:-1, 1:-1]
185 return enc.DenseEncoding(surface)