DeepSDFStruct 1.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DeepSDFStruct/SDF.py +1734 -0
- DeepSDFStruct/__init__.py +66 -0
- DeepSDFStruct/deep_sdf/__init__.py +69 -0
- DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
- DeepSDFStruct/deep_sdf/data.py +258 -0
- DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
- DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
- DeepSDFStruct/deep_sdf/models.py +197 -0
- DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
- DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
- DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
- DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
- DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
- DeepSDFStruct/deep_sdf/plotting.py +143 -0
- DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
- DeepSDFStruct/deep_sdf/training.py +840 -0
- DeepSDFStruct/deep_sdf/workspace.py +363 -0
- DeepSDFStruct/design_of_experiments.py +235 -0
- DeepSDFStruct/flexicubes/__init__.py +15 -0
- DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
- DeepSDFStruct/flexicubes/tables.py +2338 -0
- DeepSDFStruct/flexisquares/__init__.py +15 -0
- DeepSDFStruct/flexisquares/flexisquares.py +967 -0
- DeepSDFStruct/flexisquares/tables.py +63 -0
- DeepSDFStruct/lattice_structure.py +254 -0
- DeepSDFStruct/local_shapes.py +226 -0
- DeepSDFStruct/mesh.py +1184 -0
- DeepSDFStruct/optimization.py +357 -0
- DeepSDFStruct/parametrization.py +215 -0
- DeepSDFStruct/pretrained_models.py +128 -0
- DeepSDFStruct/sampling.py +670 -0
- DeepSDFStruct/sdf_operations.py +990 -0
- DeepSDFStruct/sdf_primitives.py +1416 -0
- DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
- DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
- DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
- DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
- DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
- DeepSDFStruct/torch_spline.py +446 -0
- DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
- DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/specs.json +46 -0
- DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
- DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
- DeepSDFStruct/utils.py +96 -0
- DeepSDFStruct/visualization/latent_viewer.py +211 -0
- DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
- benchmarks/generate_sdf_showcase.py +622 -0
- benchmarks/sdf_from_mesh_benchmark.py +52 -0
- benchmarks/sdf_from_mesh_comparison.md +33 -0
- benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
- deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
- deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
- deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
- deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
- deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
- deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
- deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
- deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
- docs/Makefile +20 -0
- docs/api_reference.rst +6 -0
- docs/conf.py +60 -0
- docs/index.rst +136 -0
- docs/make.bat +35 -0
- docs/qr_code_github.png +0 -0
- docs/qr_code_paper.png +0 -0
- docs/readme_images/example_output_01.png +0 -0
- docs/readme_images/example_output_02.png +0 -0
- docs/readme_images/example_output_03.png +0 -0
- docs/readme_images/example_output_04.png +0 -0
- tests/data/chairs/1005.obj +282 -0
- tests/data/chairs/1024.obj +222 -0
- tests/data/chairs/README.md +1 -0
- tests/data/circular_balls_test_case.stl +0 -0
- tests/data/cone.stl +1794 -0
- tests/data/example_disconnectd_mesh.inp +1784 -0
- tests/data/example_line_mesh.vtk +0 -0
- tests/data/stanford_bunny.stl +0 -0
- tests/data/sweep_test_case.stl +0 -0
- tests/test_DOE.py +49 -0
- tests/test_elongate_sdf.py +195 -0
- tests/test_flexisquares.py +187 -0
- tests/test_generate_dataset.py +76 -0
- tests/test_lattice_evaluation.py +137 -0
- tests/test_mesh_export.py +137 -0
- tests/test_mesh_functions.py +49 -0
- tests/test_networks.py +71 -0
- tests/test_pretrained_models.py +40 -0
- tests/test_reconstruction.py +101 -0
- tests/test_sdf_from_mesh.py +52 -0
- tests/test_sdf_functions.py +117 -0
- tests/test_sdf_primitives.py +668 -0
- tests/test_sdf_trimesh_comparison.py +482 -0
- tests/test_splinepy_unitcells.py +64 -0
- tests/test_structural_optimization.py +178 -0
- tests/test_torch_spline.py +216 -0
- tests/test_train_model.py +110 -0
- tests/test_training_data_ids.py +41 -0
- tests/tmp_outputs/temp_file.txt +0 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDF Sampling and Dataset Generation
|
|
3
|
+
====================================
|
|
4
|
+
|
|
5
|
+
This module provides tools for sampling points from SDF representations and
|
|
6
|
+
generating datasets for training neural networks (DeepSDF models). It supports
|
|
7
|
+
various sampling strategies to create well-distributed training data.
|
|
8
|
+
|
|
9
|
+
Key Features
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Sampling Strategies
|
|
13
|
+
- Uniform sampling in a bounding box
|
|
14
|
+
- Surface-focused sampling near the zero level set
|
|
15
|
+
- Importance sampling based on SDF gradients
|
|
16
|
+
- Sphere-based sampling patterns
|
|
17
|
+
- Combined strategies for balanced datasets
|
|
18
|
+
|
|
19
|
+
Dataset Generation
|
|
20
|
+
- Batch processing of multiple geometries
|
|
21
|
+
- Automatic data normalization and standardization
|
|
22
|
+
- Support for multiple geometry classes
|
|
23
|
+
- Metadata tracking (version, sampling parameters)
|
|
24
|
+
- Export to formats compatible with DeepSDF training
|
|
25
|
+
|
|
26
|
+
The module is designed to generate high-quality training data for implicit
|
|
27
|
+
neural representations, with careful attention to sampling near surfaces
|
|
28
|
+
where accurate reconstruction is most critical.
|
|
29
|
+
|
|
30
|
+
Classes
|
|
31
|
+
-------
|
|
32
|
+
SampledSDF
|
|
33
|
+
Container for sampled points and their SDF values, with utilities
|
|
34
|
+
for splitting by sign and visualization.
|
|
35
|
+
|
|
36
|
+
DataSetInfo
|
|
37
|
+
TypedDict for dataset metadata (name, classes, sampling strategy, etc.).
|
|
38
|
+
|
|
39
|
+
Functions
|
|
40
|
+
---------
|
|
41
|
+
generate_dataset
|
|
42
|
+
Batch process multiple geometries to create a complete dataset.
|
|
43
|
+
sample_sdf_*
|
|
44
|
+
Various sampling strategies for different use cases.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
from functools import partial
|
|
49
|
+
from itertools import starmap
|
|
50
|
+
import multiprocessing
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
import vtk
|
|
55
|
+
import numpy as np
|
|
56
|
+
import json
|
|
57
|
+
import pathlib
|
|
58
|
+
import typing
|
|
59
|
+
import gustaf as gus
|
|
60
|
+
import trimesh
|
|
61
|
+
from DeepSDFStruct.SDF import SDFfromMesh, SDFBase
|
|
62
|
+
from DeepSDFStruct.mesh import torchSurfMesh
|
|
63
|
+
import DeepSDFStruct
|
|
64
|
+
|
|
65
|
+
# from analysis.problems.homogenization import computeHomogenizedMaterialProperties
|
|
66
|
+
import splinepy
|
|
67
|
+
import torch
|
|
68
|
+
from collections import defaultdict
|
|
69
|
+
from tqdm import tqdm
|
|
70
|
+
import logging
|
|
71
|
+
import datetime
|
|
72
|
+
from importlib.metadata import version
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(DeepSDFStruct.__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DataSetInfo(typing.TypedDict):
|
|
78
|
+
"""Metadata for a generated SDF dataset.
|
|
79
|
+
|
|
80
|
+
Attributes
|
|
81
|
+
----------
|
|
82
|
+
dataset_name : str
|
|
83
|
+
Unique identifier for the dataset.
|
|
84
|
+
class_names : list of str
|
|
85
|
+
Names of geometry classes in the dataset.
|
|
86
|
+
sampling_strategy : str
|
|
87
|
+
Description of how points were sampled.
|
|
88
|
+
date_created : str
|
|
89
|
+
ISO format timestamp of dataset creation.
|
|
90
|
+
stds : list of float
|
|
91
|
+
Standard deviations used for normalization.
|
|
92
|
+
n_samples : int
|
|
93
|
+
Number of sample points per geometry.
|
|
94
|
+
add_surface_samples : bool
|
|
95
|
+
Whether surface points were included.
|
|
96
|
+
sdf_struct_version : str
|
|
97
|
+
Version of DeepSDFStruct used to create the dataset.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
dataset_name: str
|
|
101
|
+
class_names: list[str]
|
|
102
|
+
sampling_strategy: str
|
|
103
|
+
date_created: str
|
|
104
|
+
stds: list[float]
|
|
105
|
+
n_samples: int
|
|
106
|
+
add_surface_samples: bool
|
|
107
|
+
sdf_struct_version: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SphereParameters(typing.TypedDict):
|
|
111
|
+
"""Parameters defining a sampling sphere."""
|
|
112
|
+
|
|
113
|
+
cx: float
|
|
114
|
+
cy: float
|
|
115
|
+
cz: float
|
|
116
|
+
r: float
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class GeometryInstance:
|
|
121
|
+
name: str
|
|
122
|
+
geom: trimesh.Trimesh
|
|
123
|
+
C: np.ndarray | None
|
|
124
|
+
|
|
125
|
+
def __init__(self, name: str, geom: trimesh.Trimesh, C: np.ndarray | None):
|
|
126
|
+
self.name = name
|
|
127
|
+
self.geom = geom
|
|
128
|
+
self.C = C
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SampledSDF:
|
|
132
|
+
"""Container for sampled SDF points and their distance values.
|
|
133
|
+
|
|
134
|
+
This class stores point samples and their corresponding SDF values,
|
|
135
|
+
providing utilities for data manipulation, splitting, and visualization.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
samples : torch.Tensor
|
|
140
|
+
Point coordinates of shape (N, 3).
|
|
141
|
+
distances : torch.Tensor
|
|
142
|
+
SDF values at sample points of shape (N, 1).
|
|
143
|
+
|
|
144
|
+
Attributes
|
|
145
|
+
----------
|
|
146
|
+
samples : torch.Tensor
|
|
147
|
+
The sampled point coordinates.
|
|
148
|
+
distances : torch.Tensor
|
|
149
|
+
The SDF distance values.
|
|
150
|
+
|
|
151
|
+
Methods
|
|
152
|
+
-------
|
|
153
|
+
split_pos_neg()
|
|
154
|
+
Split into separate datasets for inside (negative) and outside
|
|
155
|
+
(positive) points.
|
|
156
|
+
create_gus_plottable()
|
|
157
|
+
Convert to gustaf Vertices for visualization.
|
|
158
|
+
stacked
|
|
159
|
+
Property returning concatenated samples and distances.
|
|
160
|
+
|
|
161
|
+
Examples
|
|
162
|
+
--------
|
|
163
|
+
>>> import torch
|
|
164
|
+
>>> from DeepSDFStruct.sampling import SampledSDF
|
|
165
|
+
>>>
|
|
166
|
+
>>> points = torch.rand(100, 3)
|
|
167
|
+
>>> distances = torch.randn(100, 1)
|
|
168
|
+
>>> sampled = SampledSDF(points, distances)
|
|
169
|
+
>>>
|
|
170
|
+
>>> # Split by sign
|
|
171
|
+
>>> inside, outside = sampled.split_pos_neg()
|
|
172
|
+
>>> print(f"Inside points: {inside.samples.shape[0]}")
|
|
173
|
+
>>> print(f"Outside points: {outside.samples.shape[0]}")
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
samples: torch.Tensor
|
|
177
|
+
distances: torch.Tensor
|
|
178
|
+
|
|
179
|
+
def split_pos_neg(self):
|
|
180
|
+
"""Split samples into inside (negative) and outside (positive) points.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
pos : SampledSDF
|
|
185
|
+
Samples with non-negative distances (outside or on surface).
|
|
186
|
+
neg : SampledSDF
|
|
187
|
+
Samples with negative distances (inside geometry).
|
|
188
|
+
"""
|
|
189
|
+
pos_mask = torch.where(self.distances >= 0.0)[0]
|
|
190
|
+
neg_mask = torch.where(self.distances < 0.0)[0]
|
|
191
|
+
pos = SampledSDF(
|
|
192
|
+
samples=self.samples[pos_mask], distances=self.distances[pos_mask]
|
|
193
|
+
)
|
|
194
|
+
neg = SampledSDF(
|
|
195
|
+
samples=self.samples[neg_mask], distances=self.distances[neg_mask]
|
|
196
|
+
)
|
|
197
|
+
return pos, neg
|
|
198
|
+
|
|
199
|
+
def create_gus_plottable(self):
|
|
200
|
+
"""Create a gustaf Vertices object for visualization.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
gustaf.Vertices
|
|
205
|
+
Vertices with distance values stored as vertex data.
|
|
206
|
+
"""
|
|
207
|
+
vp = gus.Vertices(vertices=self.samples)
|
|
208
|
+
vp.vertex_data["distance"] = self.distances
|
|
209
|
+
return vp
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def stacked(self):
|
|
213
|
+
"""Concatenate samples and distances into a single tensor.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
torch.Tensor
|
|
218
|
+
Tensor of shape (N, 4) with [x, y, z, distance] per row.
|
|
219
|
+
"""
|
|
220
|
+
return torch.hstack((self.samples, self.distances))
|
|
221
|
+
|
|
222
|
+
def __init__(self, samples, distances):
|
|
223
|
+
self.samples = samples
|
|
224
|
+
self.distances = distances
|
|
225
|
+
|
|
226
|
+
def __add__(self, other):
|
|
227
|
+
"""Concatenate two SampledSDF objects.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
other : SampledSDF
|
|
232
|
+
Another SampledSDF to concatenate.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
SampledSDF
|
|
237
|
+
Combined dataset with all samples from both objects.
|
|
238
|
+
"""
|
|
239
|
+
return SampledSDF(
|
|
240
|
+
samples=torch.vstack((self.samples, other.samples)),
|
|
241
|
+
distances=torch.vstack((self.distances, other.distances)),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _process_single_geometry_instance(
|
|
246
|
+
geom_instance: GeometryInstance,
|
|
247
|
+
file_name: str,
|
|
248
|
+
folder_name: pathlib.Path,
|
|
249
|
+
scale,
|
|
250
|
+
n_samples,
|
|
251
|
+
sampling_strategy,
|
|
252
|
+
add_surface_samples,
|
|
253
|
+
stds,
|
|
254
|
+
also_save_vtk,
|
|
255
|
+
also_save_mesh,
|
|
256
|
+
):
|
|
257
|
+
fname = folder_name / file_name
|
|
258
|
+
if not os.path.exists(folder_name):
|
|
259
|
+
os.makedirs(folder_name, exist_ok=True)
|
|
260
|
+
if os.path.isfile(fname):
|
|
261
|
+
logger.warning(f"File {fname} already exists")
|
|
262
|
+
return
|
|
263
|
+
mesh = None
|
|
264
|
+
geom = geom_instance.geom
|
|
265
|
+
if isinstance(geom, SDFBase):
|
|
266
|
+
sdf = geom
|
|
267
|
+
elif isinstance(geom, trimesh.Trimesh):
|
|
268
|
+
mesh = geom
|
|
269
|
+
sdf = SDFfromMesh(mesh, scale=scale)
|
|
270
|
+
if also_save_mesh:
|
|
271
|
+
mesh.export(fname.with_suffix(".stl"))
|
|
272
|
+
else:
|
|
273
|
+
raise NotImplementedError(
|
|
274
|
+
f"Geometry must be either trimesh or SDFBase, but not {type(geom)}."
|
|
275
|
+
)
|
|
276
|
+
sampled_sdf = random_sample_sdf(
|
|
277
|
+
sdf,
|
|
278
|
+
bounds=(-1, 1),
|
|
279
|
+
n_samples=int(n_samples),
|
|
280
|
+
sampling_strategy=sampling_strategy,
|
|
281
|
+
)
|
|
282
|
+
if add_surface_samples:
|
|
283
|
+
if not isinstance(mesh, trimesh.Trimesh):
|
|
284
|
+
logger.warning(
|
|
285
|
+
"Add surface samples was specified, but geometry"
|
|
286
|
+
f"is not given as a mesh but as {type(geom)}"
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
surf_samples = sample_mesh_surface(
|
|
290
|
+
sdf, mesh, int(n_samples // 2), stds, device="cpu", dtype=torch.float32
|
|
291
|
+
)
|
|
292
|
+
sampled_sdf += surf_samples
|
|
293
|
+
pos, neg = sampled_sdf.split_pos_neg()
|
|
294
|
+
if geom_instance.C is not None:
|
|
295
|
+
np.savez(fname, neg=neg.stacked, pos=pos.stacked, C=geom_instance.C)
|
|
296
|
+
else:
|
|
297
|
+
np.savez(fname, neg=neg.stacked, pos=pos.stacked)
|
|
298
|
+
if also_save_vtk:
|
|
299
|
+
save_points_to_vtp(fname.with_suffix(".vtp"), neg=neg.stacked, pos=pos.stacked)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class SDFSampler:
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
outdir,
|
|
306
|
+
splitdir,
|
|
307
|
+
dataset_name,
|
|
308
|
+
stds=[0.05, 0.025],
|
|
309
|
+
overwrite_existing=False,
|
|
310
|
+
) -> None:
|
|
311
|
+
self.outdir = outdir
|
|
312
|
+
self.splitdir = splitdir
|
|
313
|
+
self.dataset_name = dataset_name
|
|
314
|
+
self.geometries: dict[str, dict[str, GeometryInstance]] = {}
|
|
315
|
+
self.stds = stds
|
|
316
|
+
folder_name = pathlib.Path(outdir) / dataset_name
|
|
317
|
+
if os.path.exists(folder_name):
|
|
318
|
+
if not overwrite_existing:
|
|
319
|
+
raise IsADirectoryError(
|
|
320
|
+
f"Dataset {folder_name} already exists. "
|
|
321
|
+
"Set overwrite_existing to true to overwrite."
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
os.makedirs(folder_name)
|
|
325
|
+
|
|
326
|
+
def add_class(
|
|
327
|
+
self,
|
|
328
|
+
geom_list: list,
|
|
329
|
+
class_name: str,
|
|
330
|
+
n_faces=100,
|
|
331
|
+
homogenized_c: list[np.ndarray] | None = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Adds a geometry to the sampler object. Tries to transform inputs to
|
|
335
|
+
trimesh data. In case the geometry is a spline object, the n_faces
|
|
336
|
+
parameter determines the accuracy of the extracted mesh
|
|
337
|
+
"""
|
|
338
|
+
if homogenized_c is not None:
|
|
339
|
+
assert len(geom_list) == len(
|
|
340
|
+
homogenized_c
|
|
341
|
+
), f"Length of geometries ({len(geom_list)}) does not match length of homogenized elasticity tensors ({len(homogenized_c)})"
|
|
342
|
+
instances = {}
|
|
343
|
+
for i, geom in enumerate(geom_list):
|
|
344
|
+
instance_name = f"{class_name}_{i:05}"
|
|
345
|
+
if isinstance(geom, splinepy.Multipatch | splinepy.spline.Spline):
|
|
346
|
+
geom_gus: gus.Faces = geom.extract.faces(n_faces)
|
|
347
|
+
tris = gus.create.faces.to_simplex(geom_gus)
|
|
348
|
+
geom = trimesh.Trimesh(vertices=tris.vertices, faces=tris.faces)
|
|
349
|
+
|
|
350
|
+
elif isinstance(geom, torchSurfMesh):
|
|
351
|
+
geom = geom.to_trimesh()
|
|
352
|
+
|
|
353
|
+
if homogenized_c is not None:
|
|
354
|
+
C = homogenized_c[i]
|
|
355
|
+
else:
|
|
356
|
+
C = None
|
|
357
|
+
instances[instance_name] = GeometryInstance(instance_name, geom, C)
|
|
358
|
+
self.geometries[class_name] = instances
|
|
359
|
+
|
|
360
|
+
def process_geometries(
|
|
361
|
+
self,
|
|
362
|
+
sampling_strategy="uniform",
|
|
363
|
+
n_faces=100,
|
|
364
|
+
n_samples: int = 100000,
|
|
365
|
+
add_surface_samples=True,
|
|
366
|
+
also_save_vtk=False,
|
|
367
|
+
also_save_mesh=True,
|
|
368
|
+
scale=True,
|
|
369
|
+
n_workers=0,
|
|
370
|
+
):
|
|
371
|
+
tasks = []
|
|
372
|
+
|
|
373
|
+
for class_name, instance_list in self.geometries.items():
|
|
374
|
+
logger.info(
|
|
375
|
+
f"processing geometry list {class_name} with {len(instance_list)} items."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
folder_name = pathlib.Path(self.outdir) / self.dataset_name / class_name
|
|
379
|
+
|
|
380
|
+
func = partial(
|
|
381
|
+
_process_single_geometry_instance,
|
|
382
|
+
scale=scale,
|
|
383
|
+
n_samples=n_samples,
|
|
384
|
+
sampling_strategy=sampling_strategy,
|
|
385
|
+
add_surface_samples=add_surface_samples,
|
|
386
|
+
stds=self.stds,
|
|
387
|
+
also_save_vtk=also_save_vtk,
|
|
388
|
+
also_save_mesh=also_save_mesh,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
tasks = [
|
|
392
|
+
(geometry, f"{instance_id}.npz", folder_name)
|
|
393
|
+
for instance_id, geometry in instance_list.items()
|
|
394
|
+
]
|
|
395
|
+
if n_workers > 0:
|
|
396
|
+
logger.info(f"starting multiprocessing with {n_workers} workers")
|
|
397
|
+
with multiprocessing.Pool(processes=n_workers) as pool:
|
|
398
|
+
pool.starmap(func, tasks)
|
|
399
|
+
else:
|
|
400
|
+
logger.info("starting serial processing of geometries")
|
|
401
|
+
list(starmap(func, tasks))
|
|
402
|
+
|
|
403
|
+
logger.info(f"done processing geometry list {class_name}")
|
|
404
|
+
|
|
405
|
+
summary = DataSetInfo(
|
|
406
|
+
dataset_name=self.dataset_name,
|
|
407
|
+
class_names=list(self.geometries.keys()),
|
|
408
|
+
sampling_strategy=sampling_strategy,
|
|
409
|
+
date_created=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
410
|
+
stds=self.stds,
|
|
411
|
+
n_samples=n_samples,
|
|
412
|
+
add_surface_samples=add_surface_samples,
|
|
413
|
+
sdf_struct_version=version("DeepSDFStruct"),
|
|
414
|
+
)
|
|
415
|
+
with open(
|
|
416
|
+
str(pathlib.Path(self.outdir) / self.dataset_name / "summary.json"), "w"
|
|
417
|
+
) as f:
|
|
418
|
+
json.dump(summary, f, indent=4)
|
|
419
|
+
|
|
420
|
+
def get_meshs_from_folder(self, foldername, mesh_type) -> list:
|
|
421
|
+
"""
|
|
422
|
+
Reads all mesh files of a given type (extension) from a folder using meshio.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
foldername : str
|
|
427
|
+
Path to the folder containing the mesh files.
|
|
428
|
+
mesh_type : str
|
|
429
|
+
Mesh file extension (e.g., 'vtk', 'obj', 'stl', 'msh', 'xdmf').
|
|
430
|
+
|
|
431
|
+
Returns
|
|
432
|
+
-------
|
|
433
|
+
list[trimesh.Trimesh]
|
|
434
|
+
A list of trimesh.Trimesh objects loaded from the folder.
|
|
435
|
+
"""
|
|
436
|
+
meshes = []
|
|
437
|
+
|
|
438
|
+
# Normalize extension (remove dot if present)
|
|
439
|
+
mesh_type = mesh_type.lstrip(".")
|
|
440
|
+
|
|
441
|
+
# Iterate through all files in the folder
|
|
442
|
+
|
|
443
|
+
for filename in tqdm(os.listdir(foldername), desc="Loading meshs"):
|
|
444
|
+
if filename.lower().endswith("." + mesh_type.lower()):
|
|
445
|
+
filepath = os.path.join(foldername, filename)
|
|
446
|
+
try:
|
|
447
|
+
faces = gus.io.meshio.load(filepath)
|
|
448
|
+
trim = trimesh.Trimesh(faces.vertices, faces.elements)
|
|
449
|
+
meshes.append(trim)
|
|
450
|
+
logger.info(f"Loaded mesh: {filename}")
|
|
451
|
+
except ValueError as e:
|
|
452
|
+
logger.warning(f"Could not read {filename}: {e}")
|
|
453
|
+
|
|
454
|
+
if not meshes:
|
|
455
|
+
print(f"No .{mesh_type} meshes found in {foldername}.")
|
|
456
|
+
|
|
457
|
+
return meshes
|
|
458
|
+
|
|
459
|
+
def write_json(self, json_fname):
|
|
460
|
+
json_content = defaultdict(lambda: defaultdict(list))
|
|
461
|
+
for class_name, instance_list in self.geometries.items():
|
|
462
|
+
for instance_id, geometry in instance_list.items():
|
|
463
|
+
file_name = f"{instance_id}"
|
|
464
|
+
json_content[self.dataset_name][class_name].append(file_name)
|
|
465
|
+
# json_content = {
|
|
466
|
+
# data_info["dataset_name"]: {data_info["class_name"]: split_files}
|
|
467
|
+
# }
|
|
468
|
+
json_fname = pathlib.Path(f"{self.splitdir}/{json_fname}")
|
|
469
|
+
if not json_fname.parent.is_dir():
|
|
470
|
+
os.makedirs(json_fname.parent)
|
|
471
|
+
with open(json_fname, "w", encoding="utf-8") as f:
|
|
472
|
+
json.dump(json_content, f, indent=4)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def move(t_mesh, new_center):
|
|
476
|
+
t_mesh.vertices += new_center - t_mesh.bounding_box.centroid
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def noisy_sample(t_mesh, std, count):
|
|
480
|
+
return t_mesh.sample(int(count)) + torch.random.normal(
|
|
481
|
+
scale=std, size=(int(count), 3)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def random_points(count):
|
|
486
|
+
"""random points in a unit sphere centered at (0, 0, 0)"""
|
|
487
|
+
points = torch.random.uniform(-1, 1, (int(count * 3), 3))
|
|
488
|
+
points = points[torch.linalg.norm(points, axis=1) <= 1]
|
|
489
|
+
if points.shape[0] < count:
|
|
490
|
+
print("Too little random sampling points. Resampling.......")
|
|
491
|
+
random_points(count=count, boundary="unit_sphere")
|
|
492
|
+
elif points.shape[0] > count:
|
|
493
|
+
return points[torch.random.choice(points.shape[0], count)]
|
|
494
|
+
else:
|
|
495
|
+
return points
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def random_points_cube(count, box_size):
|
|
499
|
+
"""random points in a cube with size box_size centered at (0, 0, 0)"""
|
|
500
|
+
points = torch.random.uniform(-box_size / 2, box_size / 2, (int(count), 3))
|
|
501
|
+
return points
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def random_sample_sdf(
|
|
505
|
+
sdf,
|
|
506
|
+
bounds,
|
|
507
|
+
n_samples,
|
|
508
|
+
sampling_strategy="uniform",
|
|
509
|
+
device="cpu",
|
|
510
|
+
dtype=torch.float32,
|
|
511
|
+
):
|
|
512
|
+
supported_strategies = ("plane", "spherical_gaussian", "uniform")
|
|
513
|
+
bounds = torch.tensor(bounds, dtype=dtype, device=device)
|
|
514
|
+
if sampling_strategy == "plane":
|
|
515
|
+
samples = torch.random.uniform(
|
|
516
|
+
bounds[0], bounds[1], (n_samples, 2), device=device, dtype=dtype
|
|
517
|
+
)
|
|
518
|
+
samples = torch.hstack((samples, torch.zeros((n_samples, 1))))
|
|
519
|
+
elif sampling_strategy == "spherical_gaussian":
|
|
520
|
+
samples = torch.random.randn(n_samples, 3, device=device, dtype=dtype)
|
|
521
|
+
samples /= torch.linalg.norm(samples, axis=1).reshape(-1, 1)
|
|
522
|
+
# samples += torch.random.uniform(bounds[0], bounds[1], (n_samples, 3))
|
|
523
|
+
samples = samples + torch.random.normal(0, 0.01, (n_samples, 3))
|
|
524
|
+
elif sampling_strategy == "uniform":
|
|
525
|
+
samples = (
|
|
526
|
+
torch.rand((n_samples, 3), device=device, dtype=dtype)
|
|
527
|
+
* (bounds[1] - bounds[0])
|
|
528
|
+
+ bounds[0]
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
raise ValueError(
|
|
532
|
+
f"Unsupported sampling strategy '{sampling_strategy}'. "
|
|
533
|
+
f"Supported options are: {supported_strategies}."
|
|
534
|
+
)
|
|
535
|
+
distances = sdf(samples)
|
|
536
|
+
return SampledSDF(samples=samples, distances=distances)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def sample_mesh_surface(
|
|
540
|
+
sdf: SDFBase,
|
|
541
|
+
mesh: trimesh.Trimesh,
|
|
542
|
+
n_samples: int,
|
|
543
|
+
stds: list[float],
|
|
544
|
+
device="cpu",
|
|
545
|
+
dtype=torch.float32,
|
|
546
|
+
) -> SampledSDF:
|
|
547
|
+
"""
|
|
548
|
+
Sample noisy points around a mesh surface and evaluate them with a signed distance function (SDF).
|
|
549
|
+
|
|
550
|
+
This function uses trimesh.sample to generate surface samples
|
|
551
|
+
and perturbs them with Gaussian noise of varying standard deviations,
|
|
552
|
+
and queries the SDF at those points.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
sdf (SDFBase): A callable SDF object that takes 3D points and returns signed distances.
|
|
556
|
+
mesh (trimesh.Trimesh): A mesh object containing the vertices and faces.
|
|
557
|
+
n_samples (int): Number of mesh vertices to sample
|
|
558
|
+
stds (list[float]): Standard deviations for Gaussian noise added to sampled vertices.
|
|
559
|
+
- Typical values: [0.05, 0.0015].
|
|
560
|
+
- Larger values spread samples farther from the surface; smaller values keep them closer.
|
|
561
|
+
device (str, optional): Torch device to place tensors on (e.g., "cpu" or "cuda").
|
|
562
|
+
dtype (torch.dtype, optional): Data type for generated tensors (default: torch.float32).
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
SampledSDF: An object containing:
|
|
566
|
+
- samples (torch.Tensor): The perturbed sample points of shape (n_samples * len(stds), 3).
|
|
567
|
+
- distances (torch.Tensor): The corresponding SDF values at those sample points.
|
|
568
|
+
"""
|
|
569
|
+
samples = []
|
|
570
|
+
|
|
571
|
+
points, face_idx = trimesh.sample.sample_surface(mesh, n_samples)
|
|
572
|
+
|
|
573
|
+
surface_points = torch.tensor(points, dtype=dtype, device=device)
|
|
574
|
+
|
|
575
|
+
face_normals = torch.tensor(mesh.face_normals[face_idx], dtype=dtype, device=device)
|
|
576
|
+
|
|
577
|
+
for std in stds:
|
|
578
|
+
|
|
579
|
+
t = torch.randn(n_samples, 1, device=device, dtype=dtype) * std
|
|
580
|
+
|
|
581
|
+
noisy = surface_points + t * face_normals
|
|
582
|
+
samples.append(noisy)
|
|
583
|
+
|
|
584
|
+
queries = torch.vstack(samples)
|
|
585
|
+
distances = sdf(queries)
|
|
586
|
+
|
|
587
|
+
return SampledSDF(samples=queries, distances=distances)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def save_points_to_vtp(filename, neg, pos):
|
|
591
|
+
"""
|
|
592
|
+
Save pos/neg SDF sample points as a VTU point cloud using vtkPolyData.
|
|
593
|
+
Each point has an SDF scalar value.
|
|
594
|
+
"""
|
|
595
|
+
# Combine points
|
|
596
|
+
all_points = np.vstack((pos, neg))
|
|
597
|
+
coords = all_points[:, :3]
|
|
598
|
+
sdf_vals = all_points[:, 3]
|
|
599
|
+
|
|
600
|
+
# --- Create vtkPoints ---
|
|
601
|
+
vtk_points = vtk.vtkPoints()
|
|
602
|
+
for pt in coords:
|
|
603
|
+
vtk_points.InsertNextPoint(pt)
|
|
604
|
+
|
|
605
|
+
# --- Create PolyData ---
|
|
606
|
+
polydata = vtk.vtkPolyData()
|
|
607
|
+
polydata.SetPoints(vtk_points)
|
|
608
|
+
|
|
609
|
+
# Add vertex cells (required for points in PolyData)
|
|
610
|
+
verts = vtk.vtkCellArray()
|
|
611
|
+
for i in range(len(coords)):
|
|
612
|
+
verts.InsertNextCell(1)
|
|
613
|
+
verts.InsertCellPoint(i)
|
|
614
|
+
polydata.SetVerts(verts)
|
|
615
|
+
|
|
616
|
+
# --- Add SDF scalar values ---
|
|
617
|
+
vtk_array = vtk.vtkDoubleArray()
|
|
618
|
+
vtk_array.SetName("SDF")
|
|
619
|
+
vtk_array.SetNumberOfValues(len(sdf_vals))
|
|
620
|
+
for i, val in enumerate(sdf_vals):
|
|
621
|
+
vtk_array.SetValue(i, val)
|
|
622
|
+
polydata.GetPointData().SetScalars(vtk_array)
|
|
623
|
+
|
|
624
|
+
# --- Write to VTU ---
|
|
625
|
+
writer = vtk.vtkXMLPolyDataWriter()
|
|
626
|
+
writer.SetFileName(filename)
|
|
627
|
+
writer.SetInputData(polydata)
|
|
628
|
+
writer.Write()
|
|
629
|
+
|
|
630
|
+
logger.debug(f"Saved {len(coords)} points with SDF to '{filename}'")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def augment_by_FFD(
|
|
634
|
+
meshs: list[trimesh.Trimesh],
|
|
635
|
+
n_control_points: int = 5,
|
|
636
|
+
std_dev_fraction: float | None = 0.05,
|
|
637
|
+
n_transformations: int = 10,
|
|
638
|
+
save_meshs=False,
|
|
639
|
+
) -> list[trimesh.Trimesh]:
|
|
640
|
+
"""
|
|
641
|
+
Takes list of meshs and augments the meshs by applying a freeform deformation
|
|
642
|
+
"""
|
|
643
|
+
new_meshs = []
|
|
644
|
+
|
|
645
|
+
for i_mesh, mesh in enumerate(tqdm(meshs, desc="Augmenting meshs")):
|
|
646
|
+
bbox = mesh.bounds # shape (2, 3)
|
|
647
|
+
# Compute approximate spacing between control points along each axis
|
|
648
|
+
spacing = (bbox[1] - bbox[0]) / (n_control_points - 1)
|
|
649
|
+
# Use a fraction of spacing (e.g., 15%) as std_dev
|
|
650
|
+
std_dev_local = std_dev_fraction * spacing
|
|
651
|
+
|
|
652
|
+
for i_FFD in range(n_transformations):
|
|
653
|
+
ffd = splinepy.FFD()
|
|
654
|
+
ffd.mesh = gus.Faces(mesh.vertices, mesh.faces)
|
|
655
|
+
ffd.spline.insert_knots(0, np.linspace(0, 1, n_control_points)[1:-1])
|
|
656
|
+
ffd.spline.insert_knots(1, np.linspace(0, 1, n_control_points)[1:-1])
|
|
657
|
+
ffd.spline.insert_knots(2, np.linspace(0, 1, n_control_points)[1:-1])
|
|
658
|
+
ffd.spline.elevate_degrees([0, 1, 2])
|
|
659
|
+
ffd.spline.control_points += np.random.normal(
|
|
660
|
+
loc=0.0, scale=std_dev_local, size=ffd.spline.control_points.shape
|
|
661
|
+
)
|
|
662
|
+
new_meshs.append(trimesh.Trimesh(ffd.mesh.vertices, ffd.mesh.faces))
|
|
663
|
+
if save_meshs:
|
|
664
|
+
save_meshs = True
|
|
665
|
+
|
|
666
|
+
# Make sure the directory exists
|
|
667
|
+
os.makedirs("tmp", exist_ok=True)
|
|
668
|
+
gus.io.meshio.export(f"tmp/mesh_{i_mesh}_{i_FFD}.obj", ffd.mesh)
|
|
669
|
+
|
|
670
|
+
return new_meshs
|