1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
|
try:
from . import generic as g
except BaseException:
import generic as g
from trimesh.scene.transforms import EnforcedForest
def random_chr():
return chr(ord("a") + int(round(g.random() * 25)))
class GraphTests(g.unittest.TestCase):
def test_forest(self):
graph = EnforcedForest()
for _i in range(5000):
graph.add_edge(random_chr(), random_chr())
def test_cache(self):
for _i in range(10):
scene = g.trimesh.Scene()
scene.add_geometry(g.trimesh.creation.box())
mod = [scene.graph.__hash__()]
scene.set_camera()
mod.append(scene.graph.__hash__())
assert mod[-1] != mod[-2]
assert not g.np.allclose(scene.camera_transform, g.np.eye(4))
scene.camera_transform = g.np.eye(4)
mod.append(scene.graph.__hash__())
assert mod[-1] != mod[-2]
assert g.np.allclose(scene.camera_transform, g.np.eye(4))
assert mod[-1] != mod[-2]
def test_successors(self):
s = g.get_mesh("CesiumMilkTruck.glb")
assert len(s.graph.nodes_geometry) == 5
# world should be root frame
assert s.graph.transforms.successors(s.graph.base_frame) == set(s.graph.nodes)
for n in s.graph.nodes:
# successors should always return subset of nodes
succ = s.graph.transforms.successors(n)
assert succ.issubset(s.graph.nodes)
# we self-include node in successors
assert n in succ
# test getting a subscene from successors
ss = s.subscene("3")
assert len(ss.geometry) == 1
assert len(ss.graph.nodes_geometry) == 1
assert isinstance(s.graph.to_networkx(), g.nx.DiGraph)
def test_nodes(self):
# get a scene graph
graph = g.get_mesh("cycloidal.3DXML").graph
# get any non-root node
node = next(iter(set(graph.nodes).difference([graph.base_frame])))
# remove that node
graph.transforms.remove_node(node)
# should have dumped the cache and removed the node
assert node not in graph.nodes
def test_remove_geometries(self):
# remove geometries from a scene graph
scene = g.get_mesh("cycloidal.3DXML")
# only keep geometry instances of these
keep = {"disc_cam_A", "disc_cam_B", "vxb-6800-2rs"}
assert len(scene.duplicate_nodes) == 12
# should remove instance references except `keep`
scene.graph.remove_geometries(set(scene.geometry.keys()).difference(keep))
# there should now be three groups of duplicate nodes
assert len(scene.duplicate_nodes) == len(keep)
def test_kwargs(self):
# test the function that converts various
# arguments into a homogeneous transformation
f = g.trimesh.scene.transforms.kwargs_to_matrix
# no arguments should be an identity matrix
assert g.np.allclose(f(), g.np.eye(4))
# a passed matrix should return immediately
fix = g.random((4, 4))
assert g.np.allclose(f(matrix=fix), fix)
quat = g.trimesh.unitize([1, 2, 3, 1])
trans = [1.0, 2.0, 3.0]
rot = g.trimesh.transformations.quaternion_matrix(quat)
# should be the same as passed to transformations
assert g.np.allclose(rot, f(quaternion=quat))
# try passing both quaternion and translation
combine = f(quaternion=quat, translation=trans)
# should be the same as passed and computed
assert g.np.allclose(combine[:3, :3], rot[:3, :3])
assert g.np.allclose(combine[:3, 3], trans)
def test_remove_node(self):
s = g.get_mesh("CesiumMilkTruck.glb")
assert len(s.graph.nodes_geometry) == 5
assert len(s.graph.nodes) == 9
assert len(s.graph.transforms.node_data) == 9
assert len(s.graph.transforms.edge_data) == 8
assert len(s.graph.transforms.parents) == 8
assert s.graph.transforms.remove_node("1")
assert len(s.graph.nodes_geometry) == 5
assert len(s.graph.nodes) == 8
assert len(s.graph.transforms.node_data) == 8
assert len(s.graph.transforms.edge_data) == 6
assert len(s.graph.transforms.parents) == 6
def test_subscene(self):
s = g.get_mesh("CesiumMilkTruck.glb")
assert len(s.graph.nodes) == 9
assert len(s.graph.transforms.node_data) == 9
assert len(s.graph.transforms.edge_data) == 8
ss = s.subscene("3")
assert ss.graph.base_frame == "3"
assert set(ss.graph.nodes) == {"3", "4"}
assert len(ss.graph.transforms.node_data) == 2
assert len(ss.graph.transforms.edge_data) == 1
assert list(ss.graph.transforms.edge_data.keys()) == [("3", "4")]
def test_scene_transform(self):
# get a scene graph
scene = g.get_mesh("cycloidal.3DXML")
# copy the original bounds of the scene's convex hull
b = scene.convex_hull.bounds.tolist()
# dump it into a single mesh
m = scene.to_mesh()
# mesh bounds should match exactly
assert g.np.allclose(m.bounds, b)
assert g.np.allclose(scene.convex_hull.bounds, b)
# get a random rotation matrix
T = g.trimesh.transformations.random_rotation_matrix()
# apply it to both the mesh and the scene
m.apply_transform(T)
scene.apply_transform(T)
# the mesh and scene should have the same bounds
assert g.np.allclose(m.convex_hull.bounds, scene.convex_hull.bounds)
# should have moved from original position
assert not g.np.allclose(m.convex_hull.bounds, b)
def test_simplify(self):
if not g.trimesh.util.has_module("fast_simplification"):
return
# get a scene graph
scene: g.trimesh.Scene = g.get_mesh("cycloidal.3DXML")
original = scene.to_mesh()
scene.simplify_quadric_decimation(percent=0.0, aggression=0)
assert len(scene.to_mesh().vertices) < len(original.vertices)
def test_reverse(self):
tf = g.trimesh.transformations
s = g.trimesh.scene.Scene()
s.add_geometry(
g.trimesh.creation.box(),
parent_node_name="world",
node_name="foo",
transform=tf.translation_matrix([0, 0, 1]),
)
s.add_geometry(
g.trimesh.creation.box(),
parent_node_name="foo",
node_name="foo2",
transform=tf.translation_matrix([0, 0, 1]),
)
assert len(s.graph.transforms.edge_data) == 2
a = s.graph.get(frame_from="world", frame_to="foo2")
assert len(s.graph.transforms.edge_data) == 2
# try going backward
i = s.graph.get(frame_from="foo2", frame_to="world")
# matrix should be inverted if you're going the other way
assert g.np.allclose(a[0], g.np.linalg.inv(i[0]))
# try getting foo2 with shorthand
b = s.graph.get(frame_to="foo2")
c = s.graph["foo2"]
# matrix should be inverted if you're going the other way
assert g.np.allclose(a[0], c[0])
assert g.np.allclose(b[0], c[0])
# get should not have edited edge data
assert len(s.graph.transforms.edge_data) == 2
def test_shortest_path(self):
# compare the EnforcedForest shortest path algo
# to the more general networkx.shortest_path algo
if g.sys.version_info < (3, 7):
# old networkx is a lot different
return
tf = g.trimesh.transformations
# start with a known good random tree
edges = [tuple(row) for row in g.data["random_tree"]]
tree = g.nx.from_edgelist(edges, create_using=g.nx.DiGraph)
r_choices = g.random((len(edges), 2))
r_matrices = g.random_transforms(len(edges))
edgelist = {}
for e, r_choice, r_mat in zip(edges, r_choices, r_matrices):
data = {}
if r_choice[0] > 0.5:
# if a matrix is omitted but an edge exists it is
# the same as passing an identity matrix
data["matrix"] = r_mat
if r_choice[1] > 0.4:
# a geometry is not required for a node
data["geometry"] = str(int(r_choice[1] * 1e8))
edgelist[e] = data
# now apply the random data to an EnforcedForest
forest = g.trimesh.scene.transforms.EnforcedForest()
for k, v in edgelist.items():
forest.add_edge(*k, **v)
# generate a lot of random queries
queries = g.np.random.choice(list(forest.nodes), 10000).reshape((-1, 2))
# filter out any self-queries as networkx doesn't handle them
queries = queries[g.np.ptp(queries, axis=1) > 0]
# now run our shortest path algorithm in a profiler
with g.Profiler() as P:
ours = [forest.shortest_path(*q) for q in queries]
# print this way to avoid a python2 syntax error
g.log.debug(P.output_text())
# check truth from networkx with an undirected graph
undir = tree.to_undirected()
with g.Profiler() as P:
truth = [g.nx.shortest_path(undir, *q) for q in queries]
g.log.debug(P.output_text())
# now compare our shortest path with networkx
for a, b, q in zip(truth, ours, queries):
if tuple(a) != tuple(b):
# raise the query that killed us
raise ValueError(q)
# now try creating this as a full scenegraph
sg = g.trimesh.scene.transforms.SceneGraph()
[
sg.update(frame_from=k[0], frame_to=k[1], **kwargs)
for k, kwargs in edgelist.items()
]
with g.Profiler() as P:
matgeom = [sg.get(frame_from=q[0], frame_to=q[1]) for q in queries]
g.log.debug(P.output_text())
# all of the matrices should be rigid transforms
assert all(tf.is_rigid(mat) for mat, _ in matgeom)
def test_scaling_order(self):
s = g.trimesh.creation.box().scene()
scaling = 1.0 / 3.0
c = s.scaled(scaling)
factor = c.geometry["geometry_0"].vertices / s.geometry["geometry_0"].vertices
assert g.np.allclose(factor, scaling)
# should be returning itself
r = s.apply_translation([10.5, 10.5, 10.5])
assert g.np.allclose(r.bounds, [[10, 10, 10], [11, 11, 11]])
assert g.np.allclose(s.bounds, [[10, 10, 10], [11, 11, 11]])
def test_translation_cache(self):
# scene with non-geometry nodes
c = g.get_mesh("cycloidal.3DXML")
s = c.scaled(1.0 / c.extents)
# get the pre-translation bounds
ori = s.bounds.copy()
# apply a translation
s.apply_translation([10, 10, 10])
assert g.np.allclose(s.bounds, ori + 10)
def test_translation_origin(self):
# check to see if we can translate to the origin
c = g.get_mesh("cycloidal.3DXML")
c.apply_transform(g.trimesh.transformations.random_rotation_matrix())
s = c.scaled(1.0 / c.extents)
# shouldn't be at the origin
assert not g.np.allclose(s.bounds[0], 0.0)
# should move to the origin
s.apply_translation(-s.bounds[0])
assert g.np.allclose(s.bounds[0], 0)
def test_reconstruct(self):
original = g.get_mesh("cycloidal.3DXML")
assert isinstance(original, g.trimesh.Scene)
# get the scene as "baked" meshes with no scene graph
dupe = g.trimesh.Scene(original.dump())
assert len(dupe.geometry) > len(original.geometry)
with g.Profiler() as P:
# reconstruct the instancing using `duplicate_nodes` and `procrustes`
rec = dupe.reconstruct_instances()
g.log.info(P.output_text())
assert len(rec.graph.nodes_geometry) == len(original.graph.nodes_geometry)
assert len(rec.geometry) == len(original.geometry)
assert g.np.allclose(rec.extents, original.extents, rtol=1e-8)
assert g.np.allclose(rec.center_mass, original.center_mass, rtol=1e-8)
if __name__ == "__main__":
g.trimesh.util.attach_to_log()
g.unittest.main()
|