File: test_scenegraph.py

package info (click to toggle)
trimesh 4.5.1-3
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 33,416 kB
  • sloc: python: 35,596; makefile: 96; javascript: 85; sh: 38
file content (334 lines) | stat: -rw-r--r-- 12,258 bytes parent folder | download
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()