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

1"""Basic morphology operations that create new encodings.""" 

2 

3import numpy as np 

4 

5from .. import util 

6from ..constants import log_time 

7from . import encoding as enc 

8from . import ops 

9 

10try: 

11 from scipy import ndimage 

12except BaseException as E: 

13 # scipy is a soft dependency 

14 from ..exceptions import ExceptionWrapper 

15 

16 ndimage = ExceptionWrapper(E) 

17 

18 

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 

29 

30 

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}") 

38 

39 _assert_sparse_rank(sparse_indices, 3) 

40 return sparse_indices 

41 

42 

43def _assert_rank(value, rank): 

44 if len(value.shape) != rank: 

45 raise ValueError("Expected rank %d, got shape %s", rank, str(value.shape)) 

46 

47 

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 ) 

56 

57 

58@log_time 

59def fill_base(encoding): 

60 """ 

61 Given a sparse surface voxelization, fill in between columns. 

62 

63 Parameters 

64 -------------- 

65 encoding: Encoding object or sparse array with shape (?, 3) 

66 

67 Returns 

68 -------------- 

69 A new filled encoding object. 

70 """ 

71 return enc.SparseBinaryEncoding(ops.fill_base(_sparse_indices(encoding, rank=3))) 

72 

73 

74@log_time 

75def fill_orthographic(encoding): 

76 """ 

77 Fill the given encoding by orthographic projection method. 

78 

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. 

82 

83 Parameters 

84 -------------- 

85 encoding: Encoding object or dense rank-3 array. 

86 

87 Returns 

88 -------------- 

89 A new filled encoding object. 

90 """ 

91 return enc.DenseEncoding(ops.fill_orthographic(_dense(encoding, rank=3))) 

92 

93 

94@log_time 

95def fill_holes(encoding, **kwargs): 

96 """ 

97 Encoding wrapper around scipy.ndimage.morphology.binary_fill_holes. 

98 

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 

100 

101 Parameters 

102 -------------- 

103 encoding: Encoding object or dense rank-3 array. 

104 **kwargs: see scipy.ndimage.morphology.binary_fill_holes. 

105 

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 ) 

113 

114 

115fillers = util.FunctionRegistry( 

116 base=fill_base, 

117 orthographic=fill_orthographic, 

118 holes=fill_holes, 

119) 

120 

121 

122def fill(encoding, method="base", **kwargs): 

123 """ 

124 Fill the given encoding using the specified implementation. 

125 

126 See `fillers` for available implementations or to add your own, e.g. via 

127 `fillers['custom_key'] = custom_fn`. 

128 

129 `custom_fn` should have signature `(encoding, **kwargs) -> filled_encoding` 

130 and should not modify encoding. 

131 

132 Parameters 

133 -------------- 

134 encoding: Encoding object (left unchanged). 

135 method: method present in `fillers`. 

136 **kwargs: additional kwargs passed to the specified implementation. 

137 

138 Returns 

139 -------------- 

140 A new filled Encoding object. 

141 """ 

142 return fillers(method, encoding=encoding, **kwargs) 

143 

144 

145def binary_dilation(encoding, **kwargs): 

146 """ 

147 Encoding wrapper around scipy.ndimage.morphology.binary_dilation. 

148 

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)) 

152 

153 

154def binary_closing(encoding, **kwargs): 

155 """ 

156 Encoding wrapper around scipy.ndimage.morphology.binary_closing. 

157 

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)) 

161 

162 

163def surface(encoding, structure=None): 

164 """ 

165 Get elements on the surface of encoding. 

166 

167 A surface element is any one in encoding that is adjacent to an empty 

168 voxel. 

169 

170 Parameters 

171 -------------- 

172 encoding: Encoding or dense rank-3 array 

173 structure: adjacency structure. If None, square connectivity is used. 

174 

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)