File: test_points.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 (299 lines) | stat: -rw-r--r-- 11,111 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
try:
    from . import generic as g
except BaseException:
    import generic as g


class PointsTest(g.unittest.TestCase):
    def test_pointcloud(self):
        """
        Test PointCloud object
        """
        shape = (100, 3)
        # random points
        points = g.random(shape)
        # make sure randomness never gives duplicates by offsetting
        points += g.np.arange(shape[0]).reshape((-1, 1))

        # make some duplicate vertices
        points[:10] = points[0]

        # create a pointcloud object
        cloud = g.trimesh.points.PointCloud(points)

        initial_hash = hash(cloud)

        assert cloud.convex_hull.volume > 0.0

        # check shapes of data
        assert cloud.vertices.shape == shape
        assert cloud.shape == shape
        assert cloud.extents.shape == (3,)
        assert cloud.bounds.shape == (2, 3)

        assert hash(cloud) == initial_hash

        # set some random colors
        cloud.colors = g.random((shape[0], 4))
        # remove the duplicates we created
        cloud.merge_vertices()

        # new shape post- merge
        new_shape = (shape[0] - 9, shape[1])

        # make sure vertices and colors are new shape
        assert cloud.vertices.shape == new_shape
        assert len(cloud.colors) == new_shape[0]
        assert hash(cloud) != initial_hash

        # AABB volume should be same as points
        assert g.np.isclose(
            cloud.bounding_box.volume, g.np.prod(g.np.ptp(points, axis=0))
        )

        # will populate all bounding primitives
        assert cloud.bounding_primitive.volume > 0.0
        # ... except AABB (it uses OBB)
        assert cloud.bounding_box.volume > 0.0

        # check getitem and setitem
        cloud[0] = [10, 10, 10]
        assert g.np.allclose(cloud[0], [10, 10, 10])
        # cloud should have copied
        assert not g.np.allclose(points[0], [10, 10, 10])

        # check to see if copy works
        assert g.np.allclose(cloud.vertices, cloud.copy().vertices)
        assert hash(cloud.visual) == hash(cloud.copy().visual)

    def test_empty(self):
        p = g.trimesh.PointCloud(None)
        assert p.is_empty
        assert p.copy().is_empty

        p.vertices = [[0, 1, 2]]
        assert not p.is_empty

    def test_vertex_only(self):
        """
        Test to make sure we can instantiate a mesh with just
        vertices and no faces for some unknowable reason
        """

        v = g.random((1000, 3))
        v[g.np.floor(g.random(90) * len(v)).astype(int)] = v[0]

        mesh = g.trimesh.Trimesh(v)

        assert len(mesh.vertices) < 950
        assert len(mesh.vertices) > 900

    def test_plane(self):
        # make sure plane fitting works for 2D points in space
        for _i in range(10):
            # create a random rotation
            matrix = g.trimesh.transformations.random_rotation_matrix()
            # create some random points in spacd
            p = g.random((1000, 3))
            # make them all lie on the XY plane so we know
            # the correct normal to check against
            p[:, 2] = 0
            # transform them into random frame
            p = g.trimesh.transform_points(p, matrix)
            # we made the Z values zero before transforming
            # so the true normal should be Z then rotated
            truth = g.trimesh.transform_points([[0, 0, 1]], matrix, translate=False)[0]
            # run the plane fit
            _C, N = g.trimesh.points.plane_fit(p)
            # sign of normal is arbitrary on fit so check both
            assert g.np.allclose(truth, N) or g.np.allclose(truth, -N)
        # make sure plane fit works with multiple point sets at once
        nb_points_sets = 20
        for _i in range(10):
            # create a random rotation
            matrices = [
                g.trimesh.transformations.random_rotation_matrix()
                for _ in range(nb_points_sets)
            ]
            # create some random points in spacd
            p = g.random((nb_points_sets, 1000, 3))
            # make them all lie on the XY plane so we know
            # the correct normal to check against
            p[..., 2] = 0
            # transform them into random frame
            for j, matrix in enumerate(matrices):
                p[j, ...] = g.trimesh.transform_points(p[j, ...], matrix)
            # p = g.trimesh.transform_points(p, matrix)
            # we made the Z values zero before transforming
            # so the true normal should be Z then rotated
            truths = g.np.zeros((len(p), 3))
            for j, matrix in enumerate(matrices):
                truths[j, :] = g.trimesh.transform_points(
                    [[0, 0, 1]], matrix, translate=False
                )[0]
            # run the plane fit
            _C, N = g.trimesh.points.plane_fit(p)

            # sign of normal is arbitrary on fit so check both
            cosines = g.np.einsum("ij,ij->i", N, truths)
            assert g.np.allclose(g.np.abs(cosines), g.np.ones_like(cosines))

    def test_kmeans(self, cluster_count=5, points_per_cluster=100):
        """
        Test K-means clustering
        """
        clustered = []

        # get random-ish points in a -0.5:+0.5 interval
        points = g.random((points_per_cluster * cluster_count, 3)) - 0.5
        for index, group in enumerate(g.np.array_split(points, cluster_count)):
            # move the cluster along the XYZ vector
            clustered.append((group / 1000) + (index * 100))
        clustered = g.np.vstack(clustered)

        # run k- means clustering on our nicely separated data
        centroids, klabel = g.trimesh.points.k_means(points=clustered, k=cluster_count)

        # reshape to make sure all groups have the same index
        variance = g.np.ptp(klabel.reshape((cluster_count, points_per_cluster)), axis=1)

        assert len(centroids) == cluster_count
        assert (variance == 0).all()

    def test_tsp(self):
        """
        Test our solution for visiting every point in order.
        """
        for dimension in [2, 3]:
            for count in [2, 10, 100]:
                for _i in range(10):
                    points = g.random((count, dimension))

                    # find a path that visits every point quickly
                    idx, dist = g.trimesh.points.tsp(points, start=0)

                    # indexes should visit every point exactly once
                    assert set(idx) == set(range(len(points)))
                    assert len(idx) == len(points)
                    assert len(dist) == len(points) - 1

                    # shouldn't be any negative indexes
                    assert (idx >= 0).all()

                    # make sure distances returned are correct
                    dist_check = g.np.linalg.norm(g.np.diff(points[idx], axis=0), axis=1)
                    assert g.np.allclose(dist_check, dist)

    def test_xyz(self):
        """
        Test XYZ file loading
        """
        # test a small file from cloudcompare
        p = g.get_mesh("points_cloudcompare.xyz")
        assert p.vertices.shape == (101, 3)
        assert p.colors.shape == (101, 4)

        # test a small file from agisoft
        p = g.get_mesh("points_agisoft.xyz")
        assert p.vertices.shape == (100, 3)
        assert p.colors.shape == (100, 4)

        # test exports
        e = p.export(file_type="xyz")
        p = g.trimesh.load(g.trimesh.util.wrap_as_stream(e), file_type="xyz")
        assert p.vertices.shape == (100, 3)
        assert p.colors.shape == (100, 4)

    def test_obb(self):
        p = g.get_mesh("points_agisoft.xyz")
        original = p.bounds.copy()
        matrix = p.apply_obb()
        assert matrix.shape == (4, 4)
        assert not g.np.allclose(p.bounds, original)

    def test_ply(self):
        p = g.get_mesh("points_agisoft.xyz")
        assert isinstance(p, g.trimesh.PointCloud)
        assert len(p.vertices) > 0

        # initial color CRC
        initial = hash(p.visual)
        # set to random colors
        p.colors = g.random((len(p.vertices), 4))
        # visual CRC should have changed
        assert hash(p.visual) != initial

        # test exporting a pointcloud to a PLY file
        r = g.roundtrip(p.export(file_type="ply"), file_type="ply")
        assert r.vertices.shape == p.vertices.shape
        # make sure colors survived the round trip
        assert g.np.allclose(r.colors, p.colors)

    def test_glb(self):
        p = g.get_mesh("points_agisoft.xyz")
        assert isinstance(p, g.trimesh.PointCloud)
        assert len(p.vertices) > 0
        # test exporting a pointcloud to a GLTF
        # TODO : WE SHOULD IMPLEMENT THE IMPORTER TOO
        r = p.export(file_type="gltf")
        assert len(g.json.loads(r["model.gltf"].decode("utf-8"))["meshes"]) == 1

    def test_remove_close(self):
        # create 100 unique points
        p = g.np.arange(300).reshape((100, 3))
        # should return the original 100 points
        culled, mask = g.trimesh.points.remove_close(g.np.vstack((p, p)), radius=0.1)
        assert culled.shape == (100, 3)
        assert mask.shape == (200,)

    def test_add_operator(self):
        points_1 = g.random((10, 3))
        points_2 = g.random((20, 3))
        colors_1 = [[123, 123, 123, 255]] * len(points_1)
        colors_2 = [[255, 0, 123, 255]] * len(points_2)

        # Test: Both cloud's colors are preserved
        cloud_1 = g.trimesh.points.PointCloud(points_1, colors=colors_1)
        cloud_2 = g.trimesh.points.PointCloud(points_2, colors=colors_2)

        cloud_sum = cloud_1 + cloud_2
        assert g.np.allclose(
            cloud_sum.colors, g.np.vstack((cloud_1.colors, cloud_2.colors))
        )

        # Next test: Only second cloud has colors
        cloud_1 = g.trimesh.points.PointCloud(points_1)
        cloud_2 = g.trimesh.points.PointCloud(points_2, colors=colors_2)

        cloud_sum = cloud_1 + cloud_2
        assert g.np.allclose(cloud_sum.colors[len(cloud_1.vertices) :], cloud_2.colors)

        # Next test: Only first cloud has colors
        cloud_1 = g.trimesh.points.PointCloud(points_1, colors=colors_1)
        cloud_2 = g.trimesh.points.PointCloud(points_2)

        cloud_sum = cloud_1 + cloud_2
        assert g.np.allclose(cloud_sum.colors[: len(cloud_1.vertices)], cloud_1.colors)

    def test_radial_sort(self):
        theta = g.np.linspace(0.0, g.np.pi * 2.0, 1000)
        points = g.np.column_stack(
            (g.np.cos(theta), g.np.sin(theta), g.np.zeros(len(theta)))
        )
        points *= g.random(len(theta)).reshape((-1, 1))

        # apply a random order to the points
        order = g.np.random.permutation(g.np.arange(len(points)))

        # get the sorted version of these points
        # when we pass them the randomly ordered points
        sort = g.trimesh.points.radial_sort(
            points[order], origin=[0, 0, 0], normal=[0, 0, 1]
        )
        # should have re-established original order
        assert g.np.allclose(points, sort)


if __name__ == "__main__":
    g.trimesh.util.attach_to_log()
    g.unittest.main()