or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-features.mdanalysis.mdcore-operations.mdfile-io.mdindex.mdmesh-processing.mdpoint-clouds.mdspatial-queries.mdvisualization.md

point-clouds.mddocs/

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

```