Coverage for trimesh/voxel/transforms.py: 83%

72 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1import numpy as np 

2 

3from .. import caching, util 

4from .. import transformations as tr 

5 

6 

7class Transform: 

8 """ 

9 Class for caching metadata associated with 4x4 transformations. 

10 

11 The transformation matrix is used to define relevant properties 

12 for the voxels, including pitch and origin. 

13 """ 

14 

15 def __init__(self, matrix, datastore: caching.DataStore | None = None): 

16 """ 

17 Initialize with a transform. 

18 

19 Parameters 

20 ----------- 

21 matrix : (4, 4) float 

22 Homogeneous transformation matrix 

23 datastore 

24 If passed store the actual values in a reference to 

25 another datastore. 

26 """ 

27 matrix = np.asanyarray(matrix, dtype=np.float64) 

28 if matrix.shape != (4, 4) or not np.allclose(matrix[3, :], [0, 0, 0, 1]): 

29 raise ValueError("matrix is invalid!") 

30 

31 # store matrix as data 

32 if datastore is None: 

33 self._data = caching.DataStore() 

34 elif isinstance(datastore, caching.DataStore): 

35 self._data = datastore 

36 else: 

37 raise ValueError(f"{type(datastore)} != caching.DataStore") 

38 

39 self._data["transform_matrix"] = matrix 

40 # dump cache when matrix changes 

41 self._cache = caching.Cache(id_function=self._data.__hash__) 

42 

43 def __hash__(self): 

44 """ 

45 Get the hash of the current transformation matrix. 

46 

47 Returns 

48 ------------ 

49 hash : str 

50 Hash of transformation matrix 

51 """ 

52 return self._data.__hash__() 

53 

54 @property 

55 def translation(self): 

56 """ 

57 Get the translation component of the matrix 

58 

59 Returns 

60 ------------ 

61 translation : (3,) float 

62 Cartesian translation 

63 """ 

64 return self._data["transform_matrix"][:3, 3] 

65 

66 @property 

67 def matrix(self): 

68 """ 

69 Get the homogeneous transformation matrix. 

70 

71 Returns 

72 ------------- 

73 matrix : (4, 4) float 

74 Transformation matrix 

75 """ 

76 return self._data["transform_matrix"] 

77 

78 @matrix.setter 

79 def matrix(self, values): 

80 """ 

81 Set the homogeneous transformation matrix. 

82 

83 Parameters 

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

85 matrix : (4, 4) float 

86 Transformation matrix 

87 """ 

88 values = np.asanyarray(values, dtype=np.float64) 

89 if values.shape != (4, 4): 

90 raise ValueError("matrix must be (4, 4)!") 

91 self._data["transform_matrix"] = values 

92 

93 @caching.cache_decorator 

94 def scale(self): 

95 """ 

96 Get the scale factor of the current transformation. 

97 

98 Returns 

99 ------------- 

100 scale : (3,) float 

101 Scale factor from the matrix 

102 """ 

103 # get the current transformation 

104 matrix = self.matrix 

105 # get the (3,) diagonal of the rotation component 

106 scale = np.diag(matrix[:3, :3]) 

107 if not np.allclose(matrix[:3, :3], scale * np.eye(3), scale * 1e-6 + 1e-8): 

108 raise RuntimeError("transform features a shear or rotation") 

109 return scale 

110 

111 @caching.cache_decorator 

112 def pitch(self): 

113 scale = self.scale 

114 if not util.allclose(scale[0], scale[1:], np.max(np.abs(scale)) * 1e-6 + 1e-8): 

115 raise RuntimeError("transform features non-uniform scaling") 

116 return scale 

117 

118 @caching.cache_decorator 

119 def unit_volume(self): 

120 """Volume of a transformed unit cube.""" 

121 return np.linalg.det(self._data["transform_matrix"][:3, :3]) 

122 

123 def apply_transform(self, matrix): 

124 """Mutate the transform in-place and return self.""" 

125 self.matrix = np.matmul(matrix, self.matrix) 

126 return self 

127 

128 def apply_translation(self, translation): 

129 """Mutate the transform in-place and return self.""" 

130 self.matrix[:3, 3] += translation 

131 return self 

132 

133 def apply_scale(self, scale): 

134 """Mutate the transform in-place and return self.""" 

135 self.matrix[:3] *= scale 

136 return self 

137 

138 def transform_points(self, points): 

139 """ 

140 Apply the transformation to points (not in-place). 

141 

142 Parameters 

143 ---------- 

144 points: (n, 3) float 

145 Points in cartesian space 

146 

147 Returns 

148 ---------- 

149 transformed : (n, 3) float 

150 Points transformed by matrix 

151 """ 

152 if self.is_identity: 

153 return points.copy() 

154 return tr.transform_points(points.reshape(-1, 3), self.matrix).reshape( 

155 points.shape 

156 ) 

157 

158 def inverse_transform_points(self, points): 

159 """Apply the inverse transformation to points (not in-place).""" 

160 if self.is_identity: 

161 return points 

162 return tr.transform_points(points.reshape(-1, 3), self.inverse_matrix).reshape( 

163 points.shape 

164 ) 

165 

166 @caching.cache_decorator 

167 def inverse_matrix(self): 

168 inv = np.linalg.inv(self.matrix) 

169 inv.flags.writeable = False 

170 return inv 

171 

172 def copy(self): 

173 return Transform(matrix=self.matrix) 

174 

175 @caching.cache_decorator 

176 def is_identity(self): 

177 """ 

178 Flags this transformation being sufficiently close to eye(4). 

179 """ 

180 return util.allclose(self.matrix, np.eye(4), 1e-8)