Coverage for trimesh/poses.py: 95%

124 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +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 .util import diagonal_dot 

12 

13try: 

14 import networkx as nx 

15except BaseException as E: 

16 # create a dummy module which will raise the ImportError 

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

18 from .exceptions import ExceptionWrapper 

19 

20 nx = ExceptionWrapper(E) 

21 

22 

23def compute_stable_poses(mesh, center_mass=None, sigma=0.0, n_samples=1, threshold=0.0): 

24 """ 

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

26 

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

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

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

30 

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

32 a planar workspace and evaluates the probabilities of landing in 

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

34 

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

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

37 and a list of the probabilities for each pose. 

38 

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

40 most probable pose first. 

41 

42 Parameters 

43 ---------- 

44 mesh : trimesh.Trimesh 

45 The target mesh 

46 com : (3,) float 

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

48 assumes uniform density and watertightness and 

49 computes a center of mass explicitly 

50 sigma : float 

51 Rhe covariance for the multivariate gaussian used 

52 to sample center of mass locations 

53 n_samples : int 

54 The number of samples of the center of mass location 

55 threshold : float 

56 The probability value at which to threshold 

57 returned stable poses 

58 

59 Returns 

60 ------- 

61 transforms : (n, 4, 4) float 

62 The homogeneous matrices that transform the 

63 object to rest in a stable pose, with the 

64 new z-axis pointing upwards from the table 

65 and the object just touching the table. 

66 probs : (n,) float 

67 Probability in (0, 1) for each pose 

68 """ 

69 

70 # save convex hull mesh to avoid a cache check 

71 cvh = mesh.convex_hull 

72 

73 if center_mass is None: 

74 center_mass = mesh.center_mass 

75 

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

77 sample_coms = [] 

78 while len(sample_coms) < n_samples: 

79 remaining = n_samples - len(sample_coms) 

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

81 for c in coms: 

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

83 if np.all(dots < 0): 

84 sample_coms.append(c) 

85 

86 norms_to_probs = {} # Map from normal to probabilities 

87 

88 # For each sample, compute the stable poses 

89 for sample_com in sample_coms: 

90 # Create toppling digraph 

91 dg = _create_topple_graph(cvh, sample_com) 

92 

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

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

95 n_iters = 0 

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

97 new_nodes = [] 

98 for node in nodes: 

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

100 continue 

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

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

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

104 new_nodes.append(successor) 

105 nodes = new_nodes 

106 n_iters += 1 

107 

108 # Collect stable poses 

109 for node in dg.nodes(): 

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

111 normal = cvh.face_normals[node] 

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

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

114 if key in norms_to_probs: 

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

116 else: 

117 norms_to_probs[key] = { 

118 "prob": 1.0 / n_samples * prob, 

119 "normal": normal, 

120 } 

121 

122 transforms = [] 

123 probs = [] 

124 

125 # Filter stable poses 

126 for key in norms_to_probs: 

127 prob = norms_to_probs[key]["prob"] 

128 if prob > threshold: 

129 tf = np.eye(4) 

130 

131 # Compute a rotation matrix for this stable pose 

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

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

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

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

136 else: 

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

138 y = np.cross(z, x) 

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

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

141 

142 # Compute the necessary translation for this stable pose 

143 m = cvh.copy() 

144 m.apply_transform(tf) 

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

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

147 

148 transforms.append(tf) 

149 probs.append(prob) 

150 

151 # Sort the results 

152 transforms = np.array(transforms) 

153 probs = np.array(probs) 

154 inds = np.argsort(-probs) 

155 

156 return transforms[inds], probs[inds] 

157 

158 

159def _orient3dfast(plane, pd): 

160 """ 

161 Performs a fast 3D orientation test. 

162 

163 Parameters 

164 ---------- 

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

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

167 

168 Returns 

169 ------- 

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

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

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

173 given plane. 

174 """ 

175 pa, pb, pc = plane 

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

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

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

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

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

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

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

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

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

185 

186 return ( 

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

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

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

190 ) 

191 

192 

193def _compute_static_prob(tri, com): 

194 """ 

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

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

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

198 

199 Parameters 

200 ---------- 

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

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

203 

204 Returns 

205 ------- 

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

207 """ 

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

209 

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

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

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

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

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

215 

216 # Prevents weirdness with arctan 

217 try: 

218 return ( 

219 1.0 

220 / np.pi 

221 * np.arctan( 

222 np.sqrt( 

223 np.tan(s / 2) 

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

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

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

227 ) 

228 ) 

229 ) 

230 except BaseException: 

231 s = s + 1e-8 

232 return ( 

233 1.0 

234 / np.pi 

235 * np.arctan( 

236 np.sqrt( 

237 np.tan(s / 2) 

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

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

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

241 ) 

242 ) 

243 ) 

244 

245 

246def _create_topple_graph(cvh_mesh, com): 

247 """ 

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

249 center of mass. 

250 

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

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

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

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

255 lands on f_i initially. 

256 

257 This computation is described in detail in 

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

259 

260 Parameters 

261 ---------- 

262 cvh_mesh : trimesh.Trimesh 

263 Rhe convex hull of the target shape 

264 com : (3,) float 

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

266 

267 Returns 

268 ------- 

269 graph : networkx.DiGraph 

270 Graph representing static probabilities and toppling 

271 order for the convex hull 

272 """ 

273 adj_graph = nx.Graph() 

274 topple_graph = nx.DiGraph() 

275 

276 # Create face adjacency graph 

277 face_pairs = cvh_mesh.face_adjacency 

278 edges = cvh_mesh.face_adjacency_edges 

279 

280 graph_edges = [] 

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

282 verts = cvh_mesh.vertices[e] 

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

284 

285 adj_graph.add_edges_from(graph_edges) 

286 

287 # Compute static probabilities of landing on each face 

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

289 prob = _compute_static_prob(tri, com) 

290 topple_graph.add_node(i, prob=prob) 

291 

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

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

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

295 barys = points_to_barycentric(cvh_mesh.triangles, proj_coms) 

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

297 

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

299 for fi in unstable_face_indices: 

300 proj_com = proj_coms[fi] 

301 centroid = cvh_mesh.triangles_center[fi] 

302 norm = cvh_mesh.face_normals[fi] 

303 

304 for tfi in adj_graph[fi]: 

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

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

307 tmp = v2 

308 v2 = v1 

309 v1 = tmp 

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

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

312 if ( 

313 _orient3dfast(plane1, proj_com) >= 0 

314 and _orient3dfast(plane2, proj_com) >= 0 

315 ): 

316 break 

317 

318 topple_graph.add_edge(fi, tfi) 

319 

320 return topple_graph