sdf-sampler 0.1.0__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.
- sdf_sampler/__init__.py +78 -0
- sdf_sampler/algorithms/__init__.py +19 -0
- sdf_sampler/algorithms/flood_fill.py +233 -0
- sdf_sampler/algorithms/normal_idw.py +99 -0
- sdf_sampler/algorithms/normal_offset.py +111 -0
- sdf_sampler/algorithms/pocket.py +146 -0
- sdf_sampler/algorithms/voxel_grid.py +339 -0
- sdf_sampler/algorithms/voxel_regions.py +80 -0
- sdf_sampler/analyzer.py +299 -0
- sdf_sampler/config.py +171 -0
- sdf_sampler/io.py +178 -0
- sdf_sampler/models/__init__.py +49 -0
- sdf_sampler/models/analysis.py +85 -0
- sdf_sampler/models/constraints.py +192 -0
- sdf_sampler/models/samples.py +49 -0
- sdf_sampler/sampler.py +439 -0
- sdf_sampler/sampling/__init__.py +15 -0
- sdf_sampler/sampling/box.py +131 -0
- sdf_sampler/sampling/brush.py +63 -0
- sdf_sampler/sampling/ray_carve.py +134 -0
- sdf_sampler/sampling/sphere.py +57 -0
- sdf_sampler-0.1.0.dist-info/METADATA +226 -0
- sdf_sampler-0.1.0.dist-info/RECORD +25 -0
- sdf_sampler-0.1.0.dist-info/WHEEL +4 -0
- sdf_sampler-0.1.0.dist-info/licenses/LICENSE +21 -0
sdf_sampler/sampler.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# ABOUTME: SDFSampler class for generating training samples from constraints
|
|
2
|
+
# ABOUTME: Converts constraints to survi-compatible training data
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from scipy.spatial import KDTree
|
|
10
|
+
|
|
11
|
+
from sdf_sampler.config import SamplerConfig
|
|
12
|
+
from sdf_sampler.models.constraints import (
|
|
13
|
+
BoxConstraint,
|
|
14
|
+
BrushStrokeConstraint,
|
|
15
|
+
HalfspaceConstraint,
|
|
16
|
+
PocketConstraint,
|
|
17
|
+
RayCarveConstraint,
|
|
18
|
+
SamplePointConstraint,
|
|
19
|
+
SeedPropagationConstraint,
|
|
20
|
+
SignConvention,
|
|
21
|
+
SliceSelectionConstraint,
|
|
22
|
+
SphereConstraint,
|
|
23
|
+
)
|
|
24
|
+
from sdf_sampler.models.samples import SamplingStrategy, TrainingSample
|
|
25
|
+
from sdf_sampler.sampling.box import sample_box, sample_box_inverse_square
|
|
26
|
+
from sdf_sampler.sampling.brush import sample_brush_stroke
|
|
27
|
+
from sdf_sampler.sampling.ray_carve import sample_ray_carve
|
|
28
|
+
from sdf_sampler.sampling.sphere import sample_sphere
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SDFSampler:
|
|
32
|
+
"""Generate training samples from constraints for SDF learning.
|
|
33
|
+
|
|
34
|
+
Converts spatial constraints (boxes, spheres, etc.) into training samples
|
|
35
|
+
with position, signed distance, and optional normals.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> sampler = SDFSampler()
|
|
39
|
+
>>> samples = sampler.generate(
|
|
40
|
+
... xyz=points,
|
|
41
|
+
... normals=normals,
|
|
42
|
+
... constraints=result.constraints,
|
|
43
|
+
... )
|
|
44
|
+
>>> sampler.export_parquet(samples, "training.parquet")
|
|
45
|
+
|
|
46
|
+
Strategies:
|
|
47
|
+
- CONSTANT: Fixed samples per constraint
|
|
48
|
+
- DENSITY: Samples proportional to constraint volume
|
|
49
|
+
- INVERSE_SQUARE: More samples near surface, fewer far away
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: SamplerConfig | None = None):
|
|
53
|
+
"""Initialize the sampler.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config: Optional configuration. Uses defaults if not provided.
|
|
57
|
+
"""
|
|
58
|
+
self.config = config or SamplerConfig()
|
|
59
|
+
|
|
60
|
+
def generate(
|
|
61
|
+
self,
|
|
62
|
+
xyz: np.ndarray,
|
|
63
|
+
constraints: list[dict[str, Any]],
|
|
64
|
+
normals: np.ndarray | None = None,
|
|
65
|
+
total_samples: int | None = None,
|
|
66
|
+
strategy: str | SamplingStrategy = SamplingStrategy.INVERSE_SQUARE,
|
|
67
|
+
seed: int | None = None,
|
|
68
|
+
) -> list[TrainingSample]:
|
|
69
|
+
"""Generate training samples from constraints.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
xyz: Point cloud positions (N, 3) for distance computation
|
|
73
|
+
constraints: List of constraint dicts (from analyzer.analyze().constraints)
|
|
74
|
+
normals: Optional point normals (N, 3)
|
|
75
|
+
total_samples: Total samples to generate (default from config)
|
|
76
|
+
strategy: Sampling strategy (CONSTANT, DENSITY, or INVERSE_SQUARE)
|
|
77
|
+
seed: Random seed for reproducibility
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of TrainingSample objects
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> samples = sampler.generate(
|
|
84
|
+
... xyz=points,
|
|
85
|
+
... constraints=result.constraints,
|
|
86
|
+
... strategy="inverse_square",
|
|
87
|
+
... total_samples=50000,
|
|
88
|
+
... )
|
|
89
|
+
"""
|
|
90
|
+
xyz = np.asarray(xyz)
|
|
91
|
+
if normals is not None:
|
|
92
|
+
normals = np.asarray(normals)
|
|
93
|
+
|
|
94
|
+
if total_samples is None:
|
|
95
|
+
total_samples = self.config.total_samples
|
|
96
|
+
|
|
97
|
+
if isinstance(strategy, str):
|
|
98
|
+
strategy = SamplingStrategy(strategy)
|
|
99
|
+
|
|
100
|
+
rng = np.random.default_rng(seed if seed is not None else self.config.seed)
|
|
101
|
+
|
|
102
|
+
# Build KD-tree for inverse_square strategy
|
|
103
|
+
surface_tree = None
|
|
104
|
+
if strategy == SamplingStrategy.INVERSE_SQUARE:
|
|
105
|
+
surface_tree = KDTree(xyz)
|
|
106
|
+
|
|
107
|
+
samples: list[TrainingSample] = []
|
|
108
|
+
|
|
109
|
+
for constraint_dict in constraints:
|
|
110
|
+
# Convert dict to typed constraint
|
|
111
|
+
constraint = self._parse_constraint(constraint_dict)
|
|
112
|
+
if constraint is None:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
n_samples = self._compute_sample_count(constraint, strategy)
|
|
116
|
+
|
|
117
|
+
if isinstance(constraint, BoxConstraint):
|
|
118
|
+
if strategy == SamplingStrategy.INVERSE_SQUARE and surface_tree is not None:
|
|
119
|
+
samples.extend(
|
|
120
|
+
sample_box_inverse_square(
|
|
121
|
+
constraint,
|
|
122
|
+
rng,
|
|
123
|
+
self.config.near_band,
|
|
124
|
+
n_samples,
|
|
125
|
+
surface_tree,
|
|
126
|
+
self.config.inverse_square_falloff,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
samples.extend(
|
|
131
|
+
sample_box(constraint, rng, self.config.near_band, n_samples)
|
|
132
|
+
)
|
|
133
|
+
elif isinstance(constraint, SphereConstraint):
|
|
134
|
+
samples.extend(
|
|
135
|
+
sample_sphere(constraint, rng, self.config.near_band, n_samples)
|
|
136
|
+
)
|
|
137
|
+
elif isinstance(constraint, HalfspaceConstraint):
|
|
138
|
+
samples.extend(
|
|
139
|
+
self._sample_halfspace(constraint, xyz, rng, n_samples)
|
|
140
|
+
)
|
|
141
|
+
elif isinstance(constraint, BrushStrokeConstraint):
|
|
142
|
+
samples.extend(
|
|
143
|
+
sample_brush_stroke(constraint, rng, self.config.near_band, n_samples)
|
|
144
|
+
)
|
|
145
|
+
elif isinstance(constraint, SeedPropagationConstraint):
|
|
146
|
+
samples.extend(self._sample_propagated(constraint, xyz, normals))
|
|
147
|
+
elif isinstance(constraint, RayCarveConstraint):
|
|
148
|
+
samples.extend(sample_ray_carve(constraint, rng, n_samples))
|
|
149
|
+
elif isinstance(constraint, PocketConstraint):
|
|
150
|
+
samples.extend(self._sample_pocket(constraint, rng, n_samples))
|
|
151
|
+
elif isinstance(constraint, SliceSelectionConstraint):
|
|
152
|
+
samples.extend(self._sample_slice_selection(constraint, xyz, normals))
|
|
153
|
+
elif isinstance(constraint, SamplePointConstraint):
|
|
154
|
+
samples.extend(self._sample_sample_point(constraint))
|
|
155
|
+
|
|
156
|
+
return samples
|
|
157
|
+
|
|
158
|
+
def to_dataframe(self, samples: list[TrainingSample]) -> pd.DataFrame:
|
|
159
|
+
"""Convert samples to pandas DataFrame.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
samples: List of TrainingSample objects
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
DataFrame with columns: x, y, z, phi, nx, ny, nz, weight, source, is_surface, is_free
|
|
166
|
+
"""
|
|
167
|
+
return pd.DataFrame([s.to_dict() for s in samples])
|
|
168
|
+
|
|
169
|
+
def export_parquet(
|
|
170
|
+
self,
|
|
171
|
+
samples: list[TrainingSample],
|
|
172
|
+
path: str | Path,
|
|
173
|
+
) -> Path:
|
|
174
|
+
"""Export samples to Parquet file.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
samples: List of TrainingSample objects
|
|
178
|
+
path: Output file path
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Path to created file
|
|
182
|
+
"""
|
|
183
|
+
path = Path(path)
|
|
184
|
+
df = self.to_dataframe(samples)
|
|
185
|
+
df.to_parquet(path)
|
|
186
|
+
return path
|
|
187
|
+
|
|
188
|
+
def _parse_constraint(self, constraint_dict: dict[str, Any]) -> Any:
|
|
189
|
+
"""Parse a constraint dict into a typed constraint object."""
|
|
190
|
+
c_type = constraint_dict.get("type")
|
|
191
|
+
|
|
192
|
+
if c_type == "box":
|
|
193
|
+
return BoxConstraint(**constraint_dict)
|
|
194
|
+
elif c_type == "sphere":
|
|
195
|
+
return SphereConstraint(**constraint_dict)
|
|
196
|
+
elif c_type == "halfspace":
|
|
197
|
+
return HalfspaceConstraint(**constraint_dict)
|
|
198
|
+
elif c_type == "brush_stroke":
|
|
199
|
+
return BrushStrokeConstraint(**constraint_dict)
|
|
200
|
+
elif c_type == "seed_propagation":
|
|
201
|
+
return SeedPropagationConstraint(**constraint_dict)
|
|
202
|
+
elif c_type == "ray_carve":
|
|
203
|
+
return RayCarveConstraint(**constraint_dict)
|
|
204
|
+
elif c_type == "pocket":
|
|
205
|
+
return PocketConstraint(**constraint_dict)
|
|
206
|
+
elif c_type == "slice_selection":
|
|
207
|
+
return SliceSelectionConstraint(**constraint_dict)
|
|
208
|
+
elif c_type == "sample_point":
|
|
209
|
+
return SamplePointConstraint(**constraint_dict)
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _compute_sample_count(
|
|
214
|
+
self,
|
|
215
|
+
constraint: Any,
|
|
216
|
+
strategy: SamplingStrategy,
|
|
217
|
+
) -> int:
|
|
218
|
+
"""Compute number of samples for a constraint based on strategy."""
|
|
219
|
+
if strategy == SamplingStrategy.CONSTANT:
|
|
220
|
+
return self.config.samples_per_primitive
|
|
221
|
+
|
|
222
|
+
elif strategy == SamplingStrategy.DENSITY:
|
|
223
|
+
volume = self._compute_constraint_volume(constraint)
|
|
224
|
+
return max(10, int(volume * self.config.samples_per_cubic_meter))
|
|
225
|
+
|
|
226
|
+
elif strategy == SamplingStrategy.INVERSE_SQUARE:
|
|
227
|
+
return self.config.inverse_square_base_samples
|
|
228
|
+
|
|
229
|
+
return self.config.samples_per_primitive
|
|
230
|
+
|
|
231
|
+
def _compute_constraint_volume(self, constraint: Any) -> float:
|
|
232
|
+
"""Compute approximate volume of a constraint in cubic meters."""
|
|
233
|
+
if isinstance(constraint, BoxConstraint):
|
|
234
|
+
half = np.array(constraint.half_extents)
|
|
235
|
+
return float(np.prod(half * 2))
|
|
236
|
+
|
|
237
|
+
elif isinstance(constraint, SphereConstraint):
|
|
238
|
+
return (4 / 3) * np.pi * (constraint.radius**3)
|
|
239
|
+
|
|
240
|
+
elif isinstance(constraint, PocketConstraint):
|
|
241
|
+
voxel_size = 0.01
|
|
242
|
+
return constraint.voxel_count * (voxel_size**3)
|
|
243
|
+
|
|
244
|
+
elif isinstance(constraint, BrushStrokeConstraint):
|
|
245
|
+
n_points = len(constraint.stroke_points)
|
|
246
|
+
sphere_vol = (4 / 3) * np.pi * (constraint.radius**3)
|
|
247
|
+
return n_points * sphere_vol * 0.5
|
|
248
|
+
|
|
249
|
+
return 0.001
|
|
250
|
+
|
|
251
|
+
def _sample_halfspace(
|
|
252
|
+
self,
|
|
253
|
+
constraint: HalfspaceConstraint,
|
|
254
|
+
xyz: np.ndarray,
|
|
255
|
+
rng: np.random.Generator,
|
|
256
|
+
n_samples: int,
|
|
257
|
+
) -> list[TrainingSample]:
|
|
258
|
+
"""Generate samples from a halfspace constraint."""
|
|
259
|
+
samples = []
|
|
260
|
+
point = np.array(constraint.point)
|
|
261
|
+
normal = np.array(constraint.normal)
|
|
262
|
+
normal /= np.linalg.norm(normal)
|
|
263
|
+
|
|
264
|
+
bounds_low = xyz.min(axis=0)
|
|
265
|
+
bounds_high = xyz.max(axis=0)
|
|
266
|
+
|
|
267
|
+
for _ in range(n_samples):
|
|
268
|
+
sample_point = rng.uniform(bounds_low, bounds_high)
|
|
269
|
+
dist = np.dot(sample_point - point, normal)
|
|
270
|
+
|
|
271
|
+
if constraint.sign == SignConvention.EMPTY:
|
|
272
|
+
phi = abs(dist) + self.config.near_band
|
|
273
|
+
else:
|
|
274
|
+
phi = -(abs(dist) + self.config.near_band)
|
|
275
|
+
|
|
276
|
+
samples.append(
|
|
277
|
+
TrainingSample(
|
|
278
|
+
x=float(sample_point[0]),
|
|
279
|
+
y=float(sample_point[1]),
|
|
280
|
+
z=float(sample_point[2]),
|
|
281
|
+
phi=phi,
|
|
282
|
+
nx=float(normal[0]),
|
|
283
|
+
ny=float(normal[1]),
|
|
284
|
+
nz=float(normal[2]),
|
|
285
|
+
weight=constraint.weight,
|
|
286
|
+
source=f"halfspace_{constraint.sign.value}",
|
|
287
|
+
is_surface=False,
|
|
288
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return samples
|
|
293
|
+
|
|
294
|
+
def _sample_propagated(
|
|
295
|
+
self,
|
|
296
|
+
constraint: SeedPropagationConstraint,
|
|
297
|
+
xyz: np.ndarray,
|
|
298
|
+
normals: np.ndarray | None,
|
|
299
|
+
) -> list[TrainingSample]:
|
|
300
|
+
"""Generate samples from propagated seed."""
|
|
301
|
+
samples = []
|
|
302
|
+
|
|
303
|
+
for i, idx in enumerate(constraint.propagated_indices):
|
|
304
|
+
if idx >= len(xyz):
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
point = xyz[idx]
|
|
308
|
+
normal = normals[idx] if normals is not None else [0, 0, 1]
|
|
309
|
+
confidence = constraint.confidences[i] if i < len(constraint.confidences) else 1.0
|
|
310
|
+
|
|
311
|
+
phi = (
|
|
312
|
+
0.0
|
|
313
|
+
if constraint.sign == SignConvention.SURFACE
|
|
314
|
+
else (-0.01 if constraint.sign == SignConvention.SOLID else 0.01)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
samples.append(
|
|
318
|
+
TrainingSample(
|
|
319
|
+
x=float(point[0]),
|
|
320
|
+
y=float(point[1]),
|
|
321
|
+
z=float(point[2]),
|
|
322
|
+
phi=phi,
|
|
323
|
+
nx=float(normal[0]),
|
|
324
|
+
ny=float(normal[1]),
|
|
325
|
+
nz=float(normal[2]),
|
|
326
|
+
weight=constraint.weight * confidence,
|
|
327
|
+
source=f"propagated_{constraint.sign.value}",
|
|
328
|
+
is_surface=constraint.sign == SignConvention.SURFACE,
|
|
329
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return samples
|
|
334
|
+
|
|
335
|
+
def _sample_pocket(
|
|
336
|
+
self,
|
|
337
|
+
constraint: PocketConstraint,
|
|
338
|
+
rng: np.random.Generator,
|
|
339
|
+
n_samples: int,
|
|
340
|
+
) -> list[TrainingSample]:
|
|
341
|
+
"""Generate samples from a pocket constraint.
|
|
342
|
+
|
|
343
|
+
Note: Without access to voxel data, we sample uniformly within bounds.
|
|
344
|
+
For full pocket sampling, use the original SDF Labeler backend.
|
|
345
|
+
"""
|
|
346
|
+
samples = []
|
|
347
|
+
|
|
348
|
+
if constraint.sign == SignConvention.SOLID:
|
|
349
|
+
phi = -0.05
|
|
350
|
+
else:
|
|
351
|
+
phi = 0.05
|
|
352
|
+
|
|
353
|
+
bounds_low = np.array(constraint.bounds_low)
|
|
354
|
+
bounds_high = np.array(constraint.bounds_high)
|
|
355
|
+
|
|
356
|
+
for _ in range(n_samples):
|
|
357
|
+
point = rng.uniform(bounds_low, bounds_high)
|
|
358
|
+
|
|
359
|
+
samples.append(
|
|
360
|
+
TrainingSample(
|
|
361
|
+
x=float(point[0]),
|
|
362
|
+
y=float(point[1]),
|
|
363
|
+
z=float(point[2]),
|
|
364
|
+
phi=phi,
|
|
365
|
+
nx=0.0,
|
|
366
|
+
ny=0.0,
|
|
367
|
+
nz=0.0,
|
|
368
|
+
weight=constraint.weight,
|
|
369
|
+
source=f"pocket_{constraint.sign.value}",
|
|
370
|
+
is_surface=False,
|
|
371
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return samples
|
|
376
|
+
|
|
377
|
+
def _sample_slice_selection(
|
|
378
|
+
self,
|
|
379
|
+
constraint: SliceSelectionConstraint,
|
|
380
|
+
xyz: np.ndarray,
|
|
381
|
+
normals: np.ndarray | None,
|
|
382
|
+
) -> list[TrainingSample]:
|
|
383
|
+
"""Generate samples from slice selection constraint."""
|
|
384
|
+
samples = []
|
|
385
|
+
|
|
386
|
+
for idx in constraint.point_indices:
|
|
387
|
+
if idx >= len(xyz):
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
point = xyz[idx]
|
|
391
|
+
normal = normals[idx] if normals is not None else [0, 0, 1]
|
|
392
|
+
|
|
393
|
+
if constraint.sign == SignConvention.SURFACE:
|
|
394
|
+
phi = 0.0
|
|
395
|
+
elif constraint.sign == SignConvention.SOLID:
|
|
396
|
+
phi = -0.01
|
|
397
|
+
else:
|
|
398
|
+
phi = 0.01
|
|
399
|
+
|
|
400
|
+
samples.append(
|
|
401
|
+
TrainingSample(
|
|
402
|
+
x=float(point[0]),
|
|
403
|
+
y=float(point[1]),
|
|
404
|
+
z=float(point[2]),
|
|
405
|
+
phi=phi,
|
|
406
|
+
nx=float(normal[0]),
|
|
407
|
+
ny=float(normal[1]),
|
|
408
|
+
nz=float(normal[2]),
|
|
409
|
+
weight=constraint.weight,
|
|
410
|
+
source=f"slice_{constraint.sign.value}",
|
|
411
|
+
is_surface=constraint.sign == SignConvention.SURFACE,
|
|
412
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return samples
|
|
417
|
+
|
|
418
|
+
def _sample_sample_point(
|
|
419
|
+
self,
|
|
420
|
+
constraint: SamplePointConstraint,
|
|
421
|
+
) -> list[TrainingSample]:
|
|
422
|
+
"""Convert a sample_point constraint directly to a training sample."""
|
|
423
|
+
phi = constraint.distance
|
|
424
|
+
|
|
425
|
+
return [
|
|
426
|
+
TrainingSample(
|
|
427
|
+
x=float(constraint.position[0]),
|
|
428
|
+
y=float(constraint.position[1]),
|
|
429
|
+
z=float(constraint.position[2]),
|
|
430
|
+
phi=phi,
|
|
431
|
+
nx=0.0,
|
|
432
|
+
ny=0.0,
|
|
433
|
+
nz=0.0,
|
|
434
|
+
weight=constraint.weight,
|
|
435
|
+
source=f"idw_{constraint.sign.value}",
|
|
436
|
+
is_surface=constraint.sign == SignConvention.SURFACE,
|
|
437
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
438
|
+
)
|
|
439
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ABOUTME: Sampling module exports
|
|
2
|
+
# ABOUTME: Provides sampling functions for different constraint types
|
|
3
|
+
|
|
4
|
+
from sdf_sampler.sampling.box import sample_box, sample_box_inverse_square
|
|
5
|
+
from sdf_sampler.sampling.brush import sample_brush_stroke
|
|
6
|
+
from sdf_sampler.sampling.ray_carve import sample_ray_carve
|
|
7
|
+
from sdf_sampler.sampling.sphere import sample_sphere
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"sample_box",
|
|
11
|
+
"sample_box_inverse_square",
|
|
12
|
+
"sample_sphere",
|
|
13
|
+
"sample_brush_stroke",
|
|
14
|
+
"sample_ray_carve",
|
|
15
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# ABOUTME: Box constraint sampling functions
|
|
2
|
+
# ABOUTME: Generates training samples from axis-aligned box constraints
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from sdf_sampler.models.constraints import BoxConstraint, SignConvention
|
|
9
|
+
from sdf_sampler.models.samples import TrainingSample
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sample_box(
|
|
13
|
+
constraint: BoxConstraint,
|
|
14
|
+
rng: np.random.Generator,
|
|
15
|
+
near_band: float,
|
|
16
|
+
n_samples: int,
|
|
17
|
+
) -> list[TrainingSample]:
|
|
18
|
+
"""Generate samples from a box constraint.
|
|
19
|
+
|
|
20
|
+
Samples points near the box surfaces with appropriate SDF values.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
constraint: Box constraint to sample
|
|
24
|
+
rng: Random number generator
|
|
25
|
+
near_band: Near-band width for offset
|
|
26
|
+
n_samples: Number of samples to generate
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of TrainingSample objects
|
|
30
|
+
"""
|
|
31
|
+
samples = []
|
|
32
|
+
center = np.array(constraint.center)
|
|
33
|
+
half = np.array(constraint.half_extents)
|
|
34
|
+
|
|
35
|
+
for _ in range(n_samples):
|
|
36
|
+
# Random point near box surface
|
|
37
|
+
face = rng.integers(0, 6)
|
|
38
|
+
point = center + rng.uniform(-1, 1, 3) * half
|
|
39
|
+
|
|
40
|
+
# Clamp to face
|
|
41
|
+
axis = face // 2
|
|
42
|
+
sign = 1 if face % 2 else -1
|
|
43
|
+
point[axis] = center[axis] + sign * half[axis]
|
|
44
|
+
|
|
45
|
+
# Offset based on sign convention
|
|
46
|
+
offset = near_band if constraint.sign == SignConvention.EMPTY else -near_band
|
|
47
|
+
normal = np.zeros(3)
|
|
48
|
+
normal[axis] = sign
|
|
49
|
+
point = point + offset * normal
|
|
50
|
+
|
|
51
|
+
phi = offset
|
|
52
|
+
|
|
53
|
+
samples.append(
|
|
54
|
+
TrainingSample(
|
|
55
|
+
x=float(point[0]),
|
|
56
|
+
y=float(point[1]),
|
|
57
|
+
z=float(point[2]),
|
|
58
|
+
phi=phi,
|
|
59
|
+
nx=float(normal[0]),
|
|
60
|
+
ny=float(normal[1]),
|
|
61
|
+
nz=float(normal[2]),
|
|
62
|
+
weight=constraint.weight,
|
|
63
|
+
source=f"box_{constraint.sign.value}",
|
|
64
|
+
is_surface=False,
|
|
65
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return samples
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sample_box_inverse_square(
|
|
73
|
+
constraint: BoxConstraint,
|
|
74
|
+
rng: np.random.Generator,
|
|
75
|
+
near_band: float,
|
|
76
|
+
n_samples: int,
|
|
77
|
+
surface_tree: Any,
|
|
78
|
+
falloff: float = 2.0,
|
|
79
|
+
) -> list[TrainingSample]:
|
|
80
|
+
"""Generate samples from a box with inverse-square density distribution.
|
|
81
|
+
|
|
82
|
+
Samples more points near the surface (point cloud) and fewer far away.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
constraint: Box constraint to sample
|
|
86
|
+
rng: Random number generator
|
|
87
|
+
near_band: Near-band width for offset
|
|
88
|
+
n_samples: Number of samples to generate
|
|
89
|
+
surface_tree: KDTree of surface points for distance computation
|
|
90
|
+
falloff: Falloff exponent (higher = faster falloff)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of TrainingSample objects
|
|
94
|
+
"""
|
|
95
|
+
samples = []
|
|
96
|
+
center = np.array(constraint.center)
|
|
97
|
+
half = np.array(constraint.half_extents)
|
|
98
|
+
|
|
99
|
+
n_candidates = n_samples * 10
|
|
100
|
+
|
|
101
|
+
for _ in range(n_candidates):
|
|
102
|
+
if len(samples) >= n_samples:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
point = center + rng.uniform(-1, 1, 3) * half
|
|
106
|
+
dist_to_surface, _ = surface_tree.query(point, k=1)
|
|
107
|
+
|
|
108
|
+
min_dist = max(dist_to_surface, near_band * 0.1)
|
|
109
|
+
weight = (near_band / min_dist) ** falloff
|
|
110
|
+
|
|
111
|
+
if rng.random() < min(1.0, weight):
|
|
112
|
+
offset = near_band if constraint.sign == SignConvention.EMPTY else -near_band
|
|
113
|
+
phi = offset
|
|
114
|
+
|
|
115
|
+
samples.append(
|
|
116
|
+
TrainingSample(
|
|
117
|
+
x=float(point[0]),
|
|
118
|
+
y=float(point[1]),
|
|
119
|
+
z=float(point[2]),
|
|
120
|
+
phi=phi,
|
|
121
|
+
nx=0.0,
|
|
122
|
+
ny=0.0,
|
|
123
|
+
nz=0.0,
|
|
124
|
+
weight=constraint.weight,
|
|
125
|
+
source=f"box_{constraint.sign.value}_inv_sq",
|
|
126
|
+
is_surface=False,
|
|
127
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return samples
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ABOUTME: Brush stroke constraint sampling functions
|
|
2
|
+
# ABOUTME: Generates training samples from volumetric brush strokes
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from sdf_sampler.models.constraints import BrushStrokeConstraint, SignConvention
|
|
7
|
+
from sdf_sampler.models.samples import TrainingSample
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sample_brush_stroke(
|
|
11
|
+
constraint: BrushStrokeConstraint,
|
|
12
|
+
rng: np.random.Generator,
|
|
13
|
+
near_band: float,
|
|
14
|
+
n_samples_per_point: int,
|
|
15
|
+
) -> list[TrainingSample]:
|
|
16
|
+
"""Generate samples from brush stroke volume.
|
|
17
|
+
|
|
18
|
+
Samples uniformly within the tube-like stroke region.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
constraint: Brush stroke constraint to sample
|
|
22
|
+
rng: Random number generator
|
|
23
|
+
near_band: Near-band width for phi calculation
|
|
24
|
+
n_samples_per_point: Number of samples per stroke point
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of TrainingSample objects
|
|
28
|
+
"""
|
|
29
|
+
samples = []
|
|
30
|
+
stroke_points = np.array(constraint.stroke_points)
|
|
31
|
+
radius = constraint.radius
|
|
32
|
+
|
|
33
|
+
if constraint.sign == SignConvention.SURFACE:
|
|
34
|
+
phi = 0.0
|
|
35
|
+
elif constraint.sign == SignConvention.SOLID:
|
|
36
|
+
phi = -near_band
|
|
37
|
+
else:
|
|
38
|
+
phi = near_band
|
|
39
|
+
|
|
40
|
+
for center in stroke_points:
|
|
41
|
+
for _ in range(n_samples_per_point):
|
|
42
|
+
direction = rng.standard_normal(3)
|
|
43
|
+
direction /= np.linalg.norm(direction)
|
|
44
|
+
distance = rng.uniform(0, radius)
|
|
45
|
+
point = center + distance * direction
|
|
46
|
+
|
|
47
|
+
samples.append(
|
|
48
|
+
TrainingSample(
|
|
49
|
+
x=float(point[0]),
|
|
50
|
+
y=float(point[1]),
|
|
51
|
+
z=float(point[2]),
|
|
52
|
+
phi=phi,
|
|
53
|
+
nx=0.0,
|
|
54
|
+
ny=0.0,
|
|
55
|
+
nz=0.0,
|
|
56
|
+
weight=constraint.weight,
|
|
57
|
+
source=f"brush_{constraint.sign.value}",
|
|
58
|
+
is_surface=constraint.sign == SignConvention.SURFACE,
|
|
59
|
+
is_free=constraint.sign == SignConvention.EMPTY,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return samples
|