Coverage for trimesh/poses.py: 95%

126 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-07-31 23:55 +0000

1""" 

2poses.py 

3----------- 

4 

5Find stable orientations of meshes. 

6""" 

7 

8import numpy as np 

9 

10from .triangles import points_to_barycentric 

11from .typed import Seed 

12from .util import diagonal_dot, random_generator 

13 

14try: 

15 import networkx as nx 

16except BaseException as E: 

17 # create a dummy module which will raise the ImportError 

18 # or other exception only when someone tries to use networkx 

19 from .exceptions import ExceptionWrapper 

20 

21 nx = ExceptionWrapper(E) 

22 

23 

24def compute_stable_poses( 

25 mesh, center_mass=None, sigma=0.0, n_samples=1, threshold=0.0, seed: Seed = None 

26): 

27 """ 

28 Computes stable orientations of a mesh and their quasi-static probabilities. 

29 

30 This method samples the location of the center of mass from a multivariate 

31 gaussian with the mean at the center of mass, and a covariance 

32 equal to and identity matrix times sigma, over n_samples. 

33 

34 For each sample, it computes the stable resting poses of the mesh on a 

35 a planar workspace and evaluates the probabilities of landing in 

36 each pose if the object is dropped onto the table randomly. 

37 

38 This method returns the 4x4 homogeneous transform matrices that place 

39 the shape against the planar surface with the z-axis pointing upwards 

40 and a list of the probabilities for each pose. 

41 

42 The transforms and probabilities that are returned are sorted, with the 

43 most probable pose first. 

44 

45 Parameters 

46 ---------- 

47 mesh : trimesh.Trimesh 

48 The target mesh 

49 com : (3,) float 

50 Rhe object center of mass. If None, this method 

51 assumes uniform density and watertightness and 

52 computes a center of mass explicitly 

53 sigma : float 

54 Rhe covariance for the multivariate gaussian used 

55 to sample center of mass locations 

56 n_samples : int 

57 The number of samples of the center of mass location 

58 threshold : float 

59 The probability value at which to threshold 

60 returned stable poses 

61 

62 seed : None or int 

63 Seed for deterministic results, otherwise OS entropy. 

64 

65 Returns 

66 ------- 

67 transforms : (n, 4, 4) float 

68 The homogeneous matrices that transform the 

69 object to rest in a stable pose, with the 

70 new z-axis pointing upwards from the table 

71 and the object just touching the table. 

72 probs : (n,) float 

73 Probability in (0, 1) for each pose 

74 """ 

75 

76 # save convex hull mesh to avoid a cache check 

77 cvh = mesh.convex_hull 

78 

79 if center_mass is None: 

80 center_mass = mesh.center_mass 

81 

82 # Sample center of mass, rejecting points outside of conv hull 

83 random = random_generator(seed) 

84 sample_coms = [] 

85 while len(sample_coms) < n_samples: 

86 remaining = n_samples - len(sample_coms) 

87 coms = random.multivariate_normal(center_mass, sigma * np.eye(3), remaining) 

88 for c in coms: 

89 dots = diagonal_dot(c - cvh.triangles_center, cvh.face_normals) 

90 if np.all(dots < 0): 

91 sample_coms.append(c) 

92 

93 norms_to_probs = {} # Map from normal to probabilities 

94 

95 # For each sample, compute the stable poses 

96 for sample_com in sample_coms: 

97 # Create toppling digraph 

98 dg = _create_topple_graph(cvh, sample_com) 

99 

100 # Propagate probabilities to sink nodes with a breadth-first traversal 

101 nodes = [n for n in dg.nodes() if dg.in_degree(n) == 0] 

102 n_iters = 0 

103 while len(nodes) > 0 and n_iters <= len(mesh.faces): 

104 new_nodes = [] 

105 for node in nodes: 

106 if dg.out_degree(node) == 0: 

107 continue 

108 successor = next(iter(dg.successors(node))) 

109 dg.nodes[successor]["prob"] += dg.nodes[node]["prob"] 

110 dg.nodes[node]["prob"] = 0.0 

111 new_nodes.append(successor) 

112 nodes = new_nodes 

113 n_iters += 1 

114 

115 # Collect stable poses 

116 for node in dg.nodes(): 

117 if dg.nodes[node]["prob"] > 0.0: 

118 normal = cvh.face_normals[node] 

119 prob = dg.nodes[node]["prob"] 

120 key = tuple(np.around(normal, decimals=3)) 

121 if key in norms_to_probs: 

122 norms_to_probs[key]["prob"] += 1.0 / n_samples * prob 

123 else: 

124 norms_to_probs[key] = { 

125 "prob": 1.0 / n_samples * prob, 

126 "normal": normal, 

127 } 

128 

129 transforms = [] 

130 probs = [] 

131 

132 # Filter stable poses 

133 for key in norms_to_probs: 

134 prob = norms_to_probs[key]["prob"] 

135 if prob > threshold: 

136 tf = np.eye(4) 

137 

138 # Compute a rotation matrix for this stable pose 

139 z = -1.0 * norms_to_probs[key]["normal"] 

140 x = np.array([-z[1], z[0], 0]) 

141 if np.linalg.norm(x) == 0.0: 

142 x = np.array([1, 0, 0]) 

143 else: 

144 x = x / np.linalg.norm(x) 

145 y = np.cross(z, x) 

146 y = y / np.linalg.norm(y) 

147 tf[:3, :3] = np.array([x, y, z]) 

148 

149 # Compute the necessary translation for this stable pose 

150 m = cvh.copy() 

151 m.apply_transform(tf) 

152 z = -m.bounds[0][2] 

153 tf[:3, 3] = np.array([0, 0, z]) 

154 

155 transforms.append(tf) 

156 probs.append(prob) 

157 

158 # Sort the results 

159 transforms = np.array(transforms) 

160 probs = np.array(probs) 

161 inds = np.argsort(-probs) 

162 

163 return transforms[inds], probs[inds] 

164 

165 

166def _orient3dfast(plane, pd): 

167 """ 

168 Performs a fast 3D orientation test. 

169 

170 Parameters 

171 ---------- 

172 plane: (3,3) float, three points in space that define a plane 

173 pd: (3,) float, a single point 

174 

175 Returns 

176 ------- 

177 result: float, if greater than zero then pd is above the plane through 

178 the given three points, if less than zero then pd is below 

179 the given plane, and if equal to zero then pd is on the 

180 given plane. 

181 """ 

182 pa, pb, pc = plane 

183 adx = pa[0] - pd[0] 

184 bdx = pb[0] - pd[0] 

185 cdx = pc[0] - pd[0] 

186 ady = pa[1] - pd[1] 

187 bdy = pb[1] - pd[1] 

188 cdy = pc[1] - pd[1] 

189 adz = pa[2] - pd[2] 

190 bdz = pb[2] - pd[2] 

191 cdz = pc[2] - pd[2] 

192 

193 return ( 

194 adx * (bdy * cdz - bdz * cdy) 

195 + bdx * (cdy * adz - cdz * ady) 

196 + cdx * (ady * bdz - adz * bdy) 

197 ) 

198 

199 

200def _compute_static_prob(tri, com): 

201 """ 

202 For an object with the given center of mass, compute 

203 the probability that the given tri would be the first to hit the 

204 ground if the object were dropped with a pose chosen uniformly at random. 

205 

206 Parameters 

207 ---------- 

208 tri: (3,3) float, the vertices of a triangle 

209 cm: (3,) float, the center of mass of the object 

210 

211 Returns 

212 ------- 

213 prob: float, the probability in [0,1] for the given triangle 

214 """ 

215 sv = [(v - com) / np.linalg.norm(v - com) for v in tri] 

216 

217 # Use L'Huilier's Formula to compute spherical area 

218 a = np.arccos(min(1, max(-1, np.dot(sv[0], sv[1])))) 

219 b = np.arccos(min(1, max(-1, np.dot(sv[1], sv[2])))) 

220 c = np.arccos(min(1, max(-1, np.dot(sv[2], sv[0])))) 

221 s = (a + b + c) / 2.0 

222 

223 # Prevents weirdness with arctan 

224 try: 

225 return ( 

226 1.0 

227 / np.pi 

228 * np.arctan( 

229 np.sqrt( 

230 np.tan(s / 2) 

231 * np.tan((s - a) / 2) 

232 * np.tan((s - b) / 2) 

233 * np.tan((s - c) / 2) 

234 ) 

235 ) 

236 ) 

237 except BaseException: 

238 s = s + 1e-8 

239 return ( 

240 1.0 

241 / np.pi 

242 * np.arctan( 

243 np.sqrt( 

244 np.tan(s / 2) 

245 * np.tan((s - a) / 2) 

246 * np.tan((s - b) / 2) 

247 * np.tan((s - c) / 2) 

248 ) 

249 ) 

250 ) 

251 

252 

253def _create_topple_graph(cvh_mesh, com): 

254 """ 

255 Constructs a toppling digraph for the given convex hull mesh and 

256 center of mass. 

257 

258 Each node n_i in the digraph corresponds to a face f_i of the mesh and is 

259 labelled with the probability that the mesh will land on f_i if dropped 

260 randomly. Not all faces are stable, and node n_i has a directed edge to 

261 node n_j if the object will quasi-statically topple from f_i to f_j if it 

262 lands on f_i initially. 

263 

264 This computation is described in detail in 

265 http://goldberg.berkeley.edu/pubs/eps.pdf. 

266 

267 Parameters 

268 ---------- 

269 cvh_mesh : trimesh.Trimesh 

270 Rhe convex hull of the target shape 

271 com : (3,) float 

272 The 3D location of the target shape's center of mass 

273 

274 Returns 

275 ------- 

276 graph : networkx.DiGraph 

277 Graph representing static probabilities and toppling 

278 order for the convex hull 

279 """ 

280 adj_graph = nx.Graph() 

281 topple_graph = nx.DiGraph() 

282 

283 # Create face adjacency graph 

284 face_pairs = cvh_mesh.face_adjacency 

285 edges = cvh_mesh.face_adjacency_edges 

286 

287 graph_edges = [] 

288 for fp, e in zip(face_pairs, edges): 

289 verts = cvh_mesh.vertices[e] 

290 graph_edges.append([fp[0], fp[1], {"verts": verts}]) 

291 

292 adj_graph.add_edges_from(graph_edges) 

293 

294 # Compute static probabilities of landing on each face 

295 for i, tri in enumerate(cvh_mesh.triangles): 

296 prob = _compute_static_prob(tri, com) 

297 topple_graph.add_node(i, prob=prob) 

298 

299 # Compute COM projections onto planes of each triangle in cvh_mesh 

300 proj_dists = diagonal_dot(cvh_mesh.face_normals, com - cvh_mesh.triangles[:, 0]) 

301 proj_coms = com - proj_dists[:, None] * cvh_mesh.face_normals 

302 barys = points_to_barycentric(cvh_mesh.triangles, proj_coms) 

303 unstable_face_indices = np.where(np.any(barys < 0, axis=1))[0] 

304 

305 # For each unstable face, compute the face it topples to 

306 for fi in unstable_face_indices: 

307 proj_com = proj_coms[fi] 

308 centroid = cvh_mesh.triangles_center[fi] 

309 norm = cvh_mesh.face_normals[fi] 

310 

311 for tfi in adj_graph[fi]: 

312 v1, v2 = adj_graph[fi][tfi]["verts"] 

313 if np.dot(np.cross(v1 - centroid, v2 - centroid), norm) < 0: 

314 tmp = v2 

315 v2 = v1 

316 v1 = tmp 

317 plane1 = [centroid, v1, v1 + norm] 

318 plane2 = [centroid, v2 + norm, v2] 

319 if ( 

320 _orient3dfast(plane1, proj_com) >= 0 

321 and _orient3dfast(plane2, proj_com) >= 0 

322 ): 

323 break 

324 

325 topple_graph.add_edge(fi, tfi) 

326 

327 return topple_graph