0
# Point Clouds and Alternative Representations
1
2
Point cloud processing, conversion between different geometric representations, and point-based analysis operations. Trimesh provides comprehensive support for working with point cloud data and converting between different geometric representations.
3
4
## Capabilities
5
6
### PointCloud Class
7
8
Main class for working with point cloud data.
9
10
```python { .api }
11
class PointCloud(Geometry3D):
12
"""Point cloud representation with analysis capabilities"""
13
14
def __init__(self, vertices, colors=None, **kwargs):
15
"""
16
Initialize point cloud.
17
18
Parameters:
19
- vertices: (n, 3) point coordinates
20
- colors: (n, 3) or (n, 4) point colors (optional)
21
- **kwargs: additional point cloud properties
22
"""
23
24
@property
25
def vertices(self) -> np.ndarray:
26
"""Point coordinates as (n, 3) array"""
27
28
@property
29
def colors(self) -> np.ndarray:
30
"""Point colors as (n, 3) or (n, 4) array"""
31
32
@colors.setter
33
def colors(self, colors: np.ndarray) -> None:
34
"""Set point colors"""
35
36
@property
37
def bounds(self) -> np.ndarray:
38
"""Bounding box of point cloud"""
39
40
@property
41
def extents(self) -> np.ndarray:
42
"""Size in each dimension"""
43
44
@property
45
def centroid(self) -> np.ndarray:
46
"""Geometric center of points"""
47
```
48
49
### Point Cloud Creation and Loading
50
51
Create point clouds from various sources.
52
53
```python { .api }
54
def sample_surface(self, count, **kwargs) -> np.ndarray:
55
"""
56
Sample points uniformly on mesh surface.
57
58
Parameters:
59
- count: int, number of points to sample
60
- **kwargs: sampling options
61
62
Returns:
63
(count, 3) sampled surface points
64
"""
65
66
def sample_surface_even(self, count, **kwargs) -> np.ndarray:
67
"""
68
Sample points with even distribution on surface.
69
70
Parameters:
71
- count: int, number of points to sample
72
- **kwargs: sampling options
73
74
Returns:
75
(count, 3) evenly distributed surface points
76
"""
77
78
def sample_volume(self, count, **kwargs) -> np.ndarray:
79
"""
80
Sample points inside mesh volume.
81
82
Parameters:
83
- count: int, number of points to sample
84
- **kwargs: volume sampling options
85
86
Returns:
87
(count, 3) points inside mesh volume
88
"""
89
90
def load_point_cloud(file_obj, **kwargs) -> PointCloud:
91
"""
92
Load point cloud from file.
93
94
Parameters:
95
- file_obj: file path or file-like object
96
- **kwargs: loading options
97
98
Returns:
99
PointCloud object
100
"""
101
```
102
103
### Point Cloud Analysis
104
105
Analyze point cloud properties and structure.
106
107
```python { .api }
108
def convex_hull(self) -> 'Trimesh':
109
"""
110
Compute convex hull of point cloud.
111
112
Returns:
113
Trimesh representing convex hull
114
"""
115
116
def bounding_box(self) -> 'Trimesh':
117
"""
118
Axis-aligned bounding box as mesh.
119
120
Returns:
121
Box mesh containing all points
122
"""
123
124
def bounding_box_oriented(self) -> 'Trimesh':
125
"""
126
Oriented bounding box as mesh.
127
128
Returns:
129
Oriented box mesh with minimal volume
130
"""
131
132
def principal_components(self) -> tuple:
133
"""
134
Principal component analysis of point cloud.
135
136
Returns:
137
tuple: (eigenvalues, eigenvectors, centroid)
138
- eigenvalues: (3,) principal component magnitudes
139
- eigenvectors: (3, 3) principal directions
140
- centroid: (3,) point cloud center
141
"""
142
```
143
144
### Point Cloud Clustering and Segmentation
145
146
Group and segment points based on various criteria.
147
148
```python { .api }
149
def k_means(self, k, **kwargs) -> tuple:
150
"""
151
K-means clustering of points.
152
153
Parameters:
154
- k: int, number of clusters
155
- **kwargs: clustering options
156
157
Returns:
158
tuple: (cluster_centers, point_labels)
159
- cluster_centers: (k, 3) cluster center coordinates
160
- point_labels: (n,) cluster assignment for each point
161
"""
162
163
def dbscan(self, eps=0.1, min_samples=10, **kwargs) -> np.ndarray:
164
"""
165
DBSCAN density-based clustering.
166
167
Parameters:
168
- eps: float, neighborhood radius
169
- min_samples: int, minimum points per cluster
170
- **kwargs: DBSCAN options
171
172
Returns:
173
(n,) cluster labels (-1 for noise points)
174
"""
175
176
def remove_outliers(self, nb_neighbors=20, std_ratio=2.0) -> 'PointCloud':
177
"""
178
Remove statistical outliers from point cloud.
179
180
Parameters:
181
- nb_neighbors: int, number of neighbors to analyze
182
- std_ratio: float, standard deviation threshold
183
184
Returns:
185
Filtered PointCloud with outliers removed
186
"""
187
```
188
189
### Point Cloud Processing
190
191
Filter, downsample, and process point cloud data.
192
193
```python { .api }
194
def voxel_downsample(self, voxel_size) -> 'PointCloud':
195
"""
196
Downsample using voxel grid.
197
198
Parameters:
199
- voxel_size: float, voxel size for downsampling
200
201
Returns:
202
Downsampled PointCloud
203
"""
204
205
def uniform_downsample(self, factor) -> 'PointCloud':
206
"""
207
Uniform downsampling by factor.
208
209
Parameters:
210
- factor: int, downsampling factor
211
212
Returns:
213
Downsampled PointCloud (every factor-th point)
214
"""
215
216
def filter_radius(self, radius, min_neighbors=1) -> 'PointCloud':
217
"""
218
Filter points based on local density.
219
220
Parameters:
221
- radius: float, neighborhood radius
222
- min_neighbors: int, minimum neighbors required
223
224
Returns:
225
Filtered PointCloud
226
"""
227
228
def smooth(self, iterations=1, factor=0.5) -> 'PointCloud':
229
"""
230
Smooth point positions using neighbor averaging.
231
232
Parameters:
233
- iterations: int, number of smoothing iterations
234
- factor: float, smoothing strength (0-1)
235
236
Returns:
237
Smoothed PointCloud
238
"""
239
```
240
241
### Normal Estimation
242
243
Estimate surface normals for point clouds.
244
245
```python { .api }
246
def estimate_normals(self, radius=None, k_neighbors=30) -> np.ndarray:
247
"""
248
Estimate surface normals at each point.
249
250
Parameters:
251
- radius: float, neighborhood radius (None for k-nearest)
252
- k_neighbors: int, number of neighbors for estimation
253
254
Returns:
255
(n, 3) estimated normal vectors
256
"""
257
258
def orient_normals(self, normals, point=None, camera_location=None) -> np.ndarray:
259
"""
260
Orient normals consistently.
261
262
Parameters:
263
- normals: (n, 3) normal vectors to orient
264
- point: (3,) reference point for orientation
265
- camera_location: (3,) camera position for orientation
266
267
Returns:
268
(n, 3) consistently oriented normals
269
"""
270
```
271
272
### Surface Reconstruction
273
274
Reconstruct mesh surfaces from point clouds.
275
276
```python { .api }
277
def poisson_reconstruction(self, normals=None, depth=8, **kwargs) -> 'Trimesh':
278
"""
279
Poisson surface reconstruction.
280
281
Parameters:
282
- normals: (n, 3) surface normals (estimated if None)
283
- depth: int, octree depth for reconstruction
284
- **kwargs: Poisson reconstruction options
285
286
Returns:
287
Reconstructed mesh surface
288
"""
289
290
def ball_pivoting_reconstruction(self, radii=None, **kwargs) -> 'Trimesh':
291
"""
292
Ball pivoting surface reconstruction.
293
294
Parameters:
295
- radii: array of ball radii to try
296
- **kwargs: ball pivoting options
297
298
Returns:
299
Reconstructed mesh surface
300
"""
301
302
def alpha_shape_reconstruction(self, alpha) -> 'Trimesh':
303
"""
304
Alpha shape surface reconstruction.
305
306
Parameters:
307
- alpha: float, alpha parameter
308
309
Returns:
310
Alpha shape mesh
311
"""
312
```
313
314
### Registration and Alignment
315
316
Align point clouds to each other or reference frames.
317
318
```python { .api }
319
def register_icp(self, target, **kwargs) -> tuple:
320
"""
321
Iterative Closest Point registration.
322
323
Parameters:
324
- target: PointCloud or Trimesh to register to
325
- **kwargs: ICP options
326
327
Returns:
328
tuple: (transform_matrix, rms_error, iterations)
329
"""
330
331
def register_colored_icp(self, target, **kwargs) -> tuple:
332
"""
333
Colored ICP using both geometry and color information.
334
335
Parameters:
336
- target: PointCloud with colors
337
- **kwargs: colored ICP options
338
339
Returns:
340
tuple: (transform_matrix, rms_error, iterations)
341
"""
342
343
def align_to_principal_axes(self) -> tuple:
344
"""
345
Align point cloud to principal component axes.
346
347
Returns:
348
tuple: (aligned_points, transform_matrix)
349
"""
350
```
351
352
### Point Cloud Comparison
353
354
Compare point clouds and compute metrics.
355
356
```python { .api }
357
def distance_to_points(self, other_points) -> np.ndarray:
358
"""
359
Distance from each point to nearest point in other set.
360
361
Parameters:
362
- other_points: (m, 3) comparison point coordinates
363
364
Returns:
365
(n,) distances to nearest points
366
"""
367
368
def hausdorff_distance(self, other) -> float:
369
"""
370
Hausdorff distance to another point cloud.
371
372
Parameters:
373
- other: PointCloud or (m, 3) points
374
375
Returns:
376
float, Hausdorff distance
377
"""
378
379
def chamfer_distance(self, other) -> float:
380
"""
381
Chamfer distance to another point cloud.
382
383
Parameters:
384
- other: PointCloud or (m, 3) points
385
386
Returns:
387
float, symmetric Chamfer distance
388
"""
389
```
390
391
## Usage Examples
392
393
### Point Cloud Creation and Sampling
394
395
```python
396
import trimesh
397
import numpy as np
398
import matplotlib.pyplot as plt
399
400
# Load mesh and sample surface points
401
mesh = trimesh.load('model.stl')
402
403
# Sample points on surface
404
surface_points = mesh.sample_surface(5000)
405
print(f"Sampled {len(surface_points)} surface points")
406
407
# Create point cloud
408
point_cloud = trimesh.PointCloud(surface_points)
409
print(f"Point cloud bounds: {point_cloud.bounds}")
410
print(f"Point cloud extents: {point_cloud.extents}")
411
print(f"Point cloud centroid: {point_cloud.centroid}")
412
413
# Even distribution sampling
414
even_points = mesh.sample_surface_even(1000)
415
even_cloud = trimesh.PointCloud(even_points)
416
417
# Volume sampling (points inside mesh)
418
if mesh.is_watertight:
419
volume_points = mesh.sample_volume(2000)
420
volume_cloud = trimesh.PointCloud(volume_points)
421
422
# Visualize different sampling methods
423
scene = trimesh.Scene([
424
point_cloud.apply_translation([-5, 0, 0]), # Random surface
425
even_cloud.apply_translation([0, 0, 0]), # Even surface
426
volume_cloud.apply_translation([5, 0, 0]) # Volume
427
])
428
scene.show()
429
```
430
431
### Point Cloud Analysis
432
433
```python
434
# Load point cloud from file
435
point_cloud = trimesh.load('scan.ply')
436
437
# Principal component analysis
438
eigenvalues, eigenvectors, centroid = point_cloud.principal_components()
439
print(f"Principal components: {eigenvalues}")
440
print(f"Centroid: {centroid}")
441
442
# Compute convex hull
443
hull = point_cloud.convex_hull()
444
print(f"Convex hull volume: {hull.volume:.4f}")
445
print(f"Points inside hull: {len(point_cloud.vertices)}")
446
447
# Oriented bounding box
448
obb = point_cloud.bounding_box_oriented()
449
print(f"OBB volume: {obb.volume:.4f}")
450
451
# Axis-aligned bounding box
452
aabb = point_cloud.bounding_box()
453
print(f"AABB volume: {aabb.volume:.4f}")
454
455
# Compare bounding methods
456
print(f"OBB vs AABB volume ratio: {obb.volume / aabb.volume:.3f}")
457
458
# Visualize analysis
459
scene = trimesh.Scene([
460
point_cloud,
461
hull.apply_translation([10, 0, 0]),
462
obb.apply_translation([20, 0, 0])
463
])
464
scene.show()
465
```
466
467
### Point Cloud Clustering
468
469
```python
470
# Generate test point cloud with multiple clusters
471
np.random.seed(42)
472
cluster_centers = np.array([[0, 0, 0], [5, 0, 0], [0, 5, 0], [5, 5, 0]])
473
n_points_per_cluster = 500
474
475
points = []
476
true_labels = []
477
for i, center in enumerate(cluster_centers):
478
cluster_points = np.random.normal(center, 0.5, (n_points_per_cluster, 3))
479
points.append(cluster_points)
480
true_labels.extend([i] * n_points_per_cluster)
481
482
all_points = np.vstack(points)
483
point_cloud = trimesh.PointCloud(all_points)
484
485
# K-means clustering
486
k = 4
487
cluster_centers_est, labels_kmeans = point_cloud.k_means(k)
488
print(f"K-means found {len(cluster_centers_est)} clusters")
489
490
# DBSCAN clustering
491
labels_dbscan = point_cloud.dbscan(eps=1.0, min_samples=10)
492
n_clusters_dbscan = len(set(labels_dbscan)) - (1 if -1 in labels_dbscan else 0)
493
n_noise = list(labels_dbscan).count(-1)
494
print(f"DBSCAN found {n_clusters_dbscan} clusters, {n_noise} noise points")
495
496
# Visualize clustering results
497
fig = plt.figure(figsize=(15, 5))
498
499
# Original clusters
500
ax1 = fig.add_subplot(131, projection='3d')
501
ax1.scatter(all_points[:, 0], all_points[:, 1], all_points[:, 2], c=true_labels, cmap='tab10')
502
ax1.set_title('True Clusters')
503
504
# K-means results
505
ax2 = fig.add_subplot(132, projection='3d')
506
ax2.scatter(all_points[:, 0], all_points[:, 1], all_points[:, 2], c=labels_kmeans, cmap='tab10')
507
ax2.scatter(cluster_centers_est[:, 0], cluster_centers_est[:, 1], cluster_centers_est[:, 2],
508
c='red', marker='x', s=100, label='Centers')
509
ax2.set_title('K-means Clustering')
510
511
# DBSCAN results
512
ax3 = fig.add_subplot(133, projection='3d')
513
unique_labels = set(labels_dbscan)
514
colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]
515
for k, col in zip(unique_labels, colors):
516
if k == -1:
517
col = [0, 0, 0, 1] # Black for noise
518
class_member_mask = (labels_dbscan == k)
519
xy = all_points[class_member_mask]
520
ax3.scatter(xy[:, 0], xy[:, 1], xy[:, 2], c=[col], s=20)
521
ax3.set_title('DBSCAN Clustering')
522
523
plt.tight_layout()
524
plt.show()
525
```
526
527
### Point Cloud Processing and Filtering
528
529
```python
530
# Load noisy point cloud
531
point_cloud = trimesh.load('noisy_scan.ply')
532
print(f"Original point cloud: {len(point_cloud.vertices)} points")
533
534
# Remove statistical outliers
535
filtered_cloud = point_cloud.remove_outliers(nb_neighbors=20, std_ratio=2.0)
536
print(f"After outlier removal: {len(filtered_cloud.vertices)} points")
537
538
# Voxel downsampling
539
voxel_size = 0.02
540
downsampled_cloud = filtered_cloud.voxel_downsample(voxel_size)
541
print(f"After voxel downsampling: {len(downsampled_cloud.vertices)} points")
542
543
# Radius filtering
544
filtered_dense = downsampled_cloud.filter_radius(radius=0.05, min_neighbors=5)
545
print(f"After radius filtering: {len(filtered_dense.vertices)} points")
546
547
# Smoothing
548
smoothed_cloud = filtered_dense.smooth(iterations=3, factor=0.3)
549
550
# Compare processing steps
551
clouds = [
552
('Original', point_cloud),
553
('Outliers Removed', filtered_cloud),
554
('Downsampled', downsampled_cloud),
555
('Radius Filtered', filtered_dense),
556
('Smoothed', smoothed_cloud)
557
]
558
559
scene = trimesh.Scene()
560
for i, (name, cloud) in enumerate(clouds):
561
transform = trimesh.transformations.translation_matrix([i * 10, 0, 0])
562
scene.add_geometry(cloud, transform=transform)
563
print(f"{name}: {len(cloud.vertices)} points")
564
565
scene.show()
566
```
567
568
### Normal Estimation and Surface Reconstruction
569
570
```python
571
# Load point cloud without normals
572
point_cloud = trimesh.load('scan_no_normals.ply')
573
574
# Estimate surface normals
575
normals = point_cloud.estimate_normals(k_neighbors=30)
576
print(f"Estimated normals for {len(normals)} points")
577
578
# Orient normals consistently (towards camera/viewpoint)
579
camera_location = point_cloud.centroid + [0, 0, 10] # Above the object
580
oriented_normals = point_cloud.orient_normals(normals, camera_location=camera_location)
581
582
# Poisson surface reconstruction
583
reconstructed_mesh = point_cloud.poisson_reconstruction(
584
normals=oriented_normals,
585
depth=9
586
)
587
588
if reconstructed_mesh is not None:
589
print(f"Reconstructed mesh: {len(reconstructed_mesh.faces)} faces")
590
print(f"Mesh volume: {reconstructed_mesh.volume:.4f}")
591
592
# Compare point cloud and reconstruction
593
scene = trimesh.Scene([
594
point_cloud.apply_translation([-5, 0, 0]),
595
reconstructed_mesh.apply_translation([5, 0, 0])
596
])
597
scene.show()
598
599
# Alternative reconstruction methods
600
try:
601
# Ball pivoting reconstruction
602
radii = [0.1, 0.2, 0.4] # Multiple radii
603
ball_pivot_mesh = point_cloud.ball_pivoting_reconstruction(radii=radii)
604
605
# Alpha shape reconstruction
606
alpha_mesh = point_cloud.alpha_shape_reconstruction(alpha=0.3)
607
608
# Compare reconstruction methods
609
meshes = []
610
if reconstructed_mesh is not None:
611
meshes.append(('Poisson', reconstructed_mesh))
612
if ball_pivot_mesh is not None:
613
meshes.append(('Ball Pivoting', ball_pivot_mesh))
614
if alpha_mesh is not None:
615
meshes.append(('Alpha Shape', alpha_mesh))
616
617
scene = trimesh.Scene()
618
for i, (name, mesh) in enumerate(meshes):
619
transform = trimesh.transformations.translation_matrix([i * 8, 0, 0])
620
scene.add_geometry(mesh, transform=transform)
621
print(f"{name}: {len(mesh.faces)} faces, volume: {mesh.volume:.4f}")
622
623
scene.show()
624
625
except Exception as e:
626
print(f"Some reconstruction methods failed: {e}")
627
```
628
629
### Point Cloud Registration
630
631
```python
632
# Create two point clouds from the same mesh with different transforms
633
mesh = trimesh.load('model.stl')
634
635
# Original point cloud
636
points1 = mesh.sample_surface(2000)
637
cloud1 = trimesh.PointCloud(points1)
638
639
# Transformed point cloud (with noise)
640
transform_true = trimesh.transformations.compose_matrix(
641
translate=[1, 0.5, 0.2],
642
angles=[0.1, 0.2, 0.05]
643
)
644
points2 = trimesh.transform_points(points1, transform_true)
645
# Add noise
646
points2 += np.random.normal(0, 0.01, points2.shape)
647
cloud2 = trimesh.PointCloud(points2)
648
649
print("Before registration:")
650
print(f"Cloud 1 centroid: {cloud1.centroid}")
651
print(f"Cloud 2 centroid: {cloud2.centroid}")
652
653
# ICP registration
654
transform_est, rms_error, iterations = cloud1.register_icp(cloud2)
655
656
print(f"\nICP Registration:")
657
print(f"RMS error: {rms_error:.6f}")
658
print(f"Iterations: {iterations}")
659
print(f"Estimated transform:\n{transform_est}")
660
661
# Apply estimated transform to align clouds
662
cloud2_aligned = cloud2.copy()
663
cloud2_aligned.apply_transform(transform_est)
664
665
print(f"\nAfter registration:")
666
print(f"Cloud 1 centroid: {cloud1.centroid}")
667
print(f"Cloud 2 aligned centroid: {cloud2_aligned.centroid}")
668
669
# Compute registration error
670
distances = cloud1.distance_to_points(cloud2_aligned.vertices)
671
print(f"Mean registration error: {distances.mean():.6f}")
672
print(f"Max registration error: {distances.max():.6f}")
673
674
# Visualize registration
675
scene = trimesh.Scene([
676
cloud1.apply_translation([-5, 0, 0]), # Original
677
cloud2.apply_translation([0, 0, 0]), # Transformed
678
cloud2_aligned.apply_translation([5, 0, 0]) # Aligned
679
])
680
scene.show()
681
```
682
683
### Point Cloud Comparison and Metrics
684
685
```python
686
# Load two point clouds for comparison
687
cloud1 = trimesh.load('scan1.ply')
688
cloud2 = trimesh.load('scan2.ply')
689
690
# Basic comparison metrics
691
hausdorff_dist = cloud1.hausdorff_distance(cloud2)
692
chamfer_dist = cloud1.chamfer_distance(cloud2)
693
694
print(f"Hausdorff distance: {hausdorff_dist:.6f}")
695
print(f"Chamfer distance: {chamfer_dist:.6f}")
696
697
# Point-to-point distances
698
distances_1_to_2 = cloud1.distance_to_points(cloud2.vertices)
699
distances_2_to_1 = cloud2.distance_to_points(cloud1.vertices)
700
701
print(f"\nDistance statistics (cloud 1 to cloud 2):")
702
print(f"Mean: {distances_1_to_2.mean():.6f}")
703
print(f"Std: {distances_1_to_2.std():.6f}")
704
print(f"Max: {distances_1_to_2.max():.6f}")
705
print(f"95th percentile: {np.percentile(distances_1_to_2, 95):.6f}")
706
707
# Visualize distance distribution
708
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
709
710
ax1.hist(distances_1_to_2, bins=50, alpha=0.7, label='Cloud 1 to Cloud 2')
711
ax1.hist(distances_2_to_1, bins=50, alpha=0.7, label='Cloud 2 to Cloud 1')
712
ax1.set_xlabel('Distance')
713
ax1.set_ylabel('Count')
714
ax1.set_title('Distance Distribution')
715
ax1.legend()
716
717
# Color-coded distance visualization
718
normalized_distances = distances_1_to_2 / distances_1_to_2.max()
719
colors = plt.cm.viridis(normalized_distances)
720
721
ax2 = fig.add_subplot(122, projection='3d')
722
ax2.scatter(cloud1.vertices[:, 0], cloud1.vertices[:, 1], cloud1.vertices[:, 2],
723
c=distances_1_to_2, cmap='viridis', s=20)
724
ax2.set_title('Distance-Colored Points')
725
726
plt.tight_layout()
727
plt.show()
728
729
# Export comparison results
730
comparison_data = {
731
'hausdorff_distance': float(hausdorff_dist),
732
'chamfer_distance': float(chamfer_dist),
733
'mean_distance_1_to_2': float(distances_1_to_2.mean()),
734
'mean_distance_2_to_1': float(distances_2_to_1.mean()),
735
'std_distance_1_to_2': float(distances_1_to_2.std()),
736
'max_distance_1_to_2': float(distances_1_to_2.max())
737
}
738
739
import json
740
with open('point_cloud_comparison.json', 'w') as f:
741
json.dump(comparison_data, f, indent=2)
742
743
print("Comparison results saved to point_cloud_comparison.json")
744
```