sdf-sampler 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (38) hide show
  1. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/.gitignore +1 -0
  2. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/CHANGELOG.md +13 -0
  3. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/PKG-INFO +150 -39
  4. sdf_sampler-0.2.0/README.md +293 -0
  5. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/pyproject.toml +5 -2
  6. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/__init__.py +1 -1
  7. sdf_sampler-0.2.0/src/sdf_sampler/__main__.py +17 -0
  8. sdf_sampler-0.2.0/src/sdf_sampler/cli.py +457 -0
  9. sdf_sampler-0.1.0/.github_token.env +0 -1
  10. sdf_sampler-0.1.0/README.md +0 -182
  11. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/LICENSE +0 -0
  12. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/__init__.py +0 -0
  13. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/flood_fill.py +0 -0
  14. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/normal_idw.py +0 -0
  15. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/normal_offset.py +0 -0
  16. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/pocket.py +0 -0
  17. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/voxel_grid.py +0 -0
  18. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/voxel_regions.py +0 -0
  19. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/analyzer.py +0 -0
  20. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/config.py +0 -0
  21. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/io.py +0 -0
  22. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/__init__.py +0 -0
  23. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/analysis.py +0 -0
  24. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/constraints.py +0 -0
  25. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/samples.py +0 -0
  26. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampler.py +0 -0
  27. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/__init__.py +0 -0
  28. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/box.py +0 -0
  29. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/brush.py +0 -0
  30. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/ray_carve.py +0 -0
  31. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/sphere.py +0 -0
  32. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/__init__.py +0 -0
  33. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_analyzer.py +0 -0
  34. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_equivalence.py +0 -0
  35. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_integration.py +0 -0
  36. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_models.py +0 -0
  37. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_sampler.py +0 -0
  38. {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/uv.lock +0 -0
@@ -79,3 +79,4 @@ dmypy.json
79
79
 
80
80
  # Secrets
81
81
  .pypi_token.env
82
+ *.env
@@ -5,6 +5,19 @@ All notable changes to sdf-sampler will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2025-01-29
9
+
10
+ ### Added
11
+
12
+ - **Command-Line Interface** for batch processing
13
+ - `sdf-sampler pipeline` - Full workflow (analyze + sample + export)
14
+ - `sdf-sampler analyze` - Detect SOLID/EMPTY regions
15
+ - `sdf-sampler sample` - Generate training samples from constraints
16
+ - `sdf-sampler info` - Inspect point clouds, constraints, and sample files
17
+ - Support for `python -m sdf_sampler` invocation
18
+ - Console script entry point (`sdf-sampler` command)
19
+ - Comprehensive README with SDK and CLI documentation
20
+
8
21
  ## [0.1.0] - 2025-01-29
9
22
 
10
23
  ### Added
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdf-sampler
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation
5
- Project-URL: Repository, https://github.com/chiark/sdf-sampler
5
+ Project-URL: Repository, https://github.com/Chiark-Collective/sdf-sampler
6
6
  Author-email: Liam <liam@example.com>
7
7
  License: MIT
8
8
  License-File: LICENSE
@@ -60,7 +60,88 @@ For additional I/O format support (PLY, LAS/LAZ):
60
60
  pip install sdf-sampler[io]
61
61
  ```
62
62
 
63
- ## Quick Start
63
+ ## Command-Line Interface
64
+
65
+ sdf-sampler provides a CLI for common workflows:
66
+
67
+ ```bash
68
+ # Run as module
69
+ python -m sdf_sampler --help
70
+
71
+ # Or use the installed command
72
+ sdf-sampler --help
73
+ ```
74
+
75
+ ### Commands
76
+
77
+ #### `pipeline` - Full workflow (recommended)
78
+
79
+ Run the complete pipeline: analyze point cloud → generate samples → export.
80
+
81
+ ```bash
82
+ # Basic usage
83
+ sdf-sampler pipeline scan.ply -o training_data.parquet
84
+
85
+ # With options
86
+ sdf-sampler pipeline scan.ply \
87
+ -o training_data.parquet \
88
+ -n 50000 \
89
+ -s inverse_square \
90
+ --save-constraints constraints.json \
91
+ -v
92
+ ```
93
+
94
+ Options:
95
+ - `-o, --output`: Output parquet file (default: `<input>_samples.parquet`)
96
+ - `-n, --total-samples`: Number of samples to generate (default: 10000)
97
+ - `-s, --strategy`: Sampling strategy: `constant`, `density`, `inverse_square` (default: `inverse_square`)
98
+ - `-a, --algorithms`: Specific algorithms to run (default: all)
99
+ - `--save-constraints`: Also save constraints to JSON
100
+ - `--seed`: Random seed for reproducibility
101
+ - `-v, --verbose`: Verbose output
102
+
103
+ #### `analyze` - Detect regions
104
+
105
+ Analyze a point cloud to detect SOLID/EMPTY regions.
106
+
107
+ ```bash
108
+ sdf-sampler analyze scan.ply -o constraints.json -v
109
+ ```
110
+
111
+ Options:
112
+ - `-o, --output`: Output JSON file (default: `<input>_constraints.json`)
113
+ - `-a, --algorithms`: Algorithms to run (see below)
114
+ - `--no-hull-filter`: Disable hull filtering
115
+ - `-v, --verbose`: Verbose output
116
+
117
+ #### `sample` - Generate training samples
118
+
119
+ Generate training samples from a constraints file.
120
+
121
+ ```bash
122
+ sdf-sampler sample scan.ply constraints.json -o samples.parquet -n 50000
123
+ ```
124
+
125
+ Options:
126
+ - `-o, --output`: Output parquet file
127
+ - `-n, --total-samples`: Number of samples (default: 10000)
128
+ - `-s, --strategy`: Sampling strategy (default: `inverse_square`)
129
+ - `--seed`: Random seed
130
+ - `-v, --verbose`: Verbose output
131
+
132
+ #### `info` - Inspect files
133
+
134
+ Show information about point clouds, constraints, or sample files.
135
+
136
+ ```bash
137
+ sdf-sampler info scan.ply
138
+ sdf-sampler info constraints.json
139
+ sdf-sampler info samples.parquet
140
+ ```
141
+
142
+ ## Python SDK
143
+
144
+ ### Quick Start
64
145
 
65
146
  ```python
66
147
  from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
@@ -86,28 +167,13 @@ samples = sampler.generate(
86
167
  sampler.export_parquet(samples, "training_data.parquet")
87
168
  ```
88
169
 
89
- ## Features
90
-
91
- ### Auto-Analysis Algorithms
92
-
93
- - **flood_fill**: Detects EMPTY (outside) regions by ray propagation from sky
94
- - **voxel_regions**: Detects SOLID (underground) regions
95
- - **normal_offset**: Generates paired SOLID/EMPTY boxes along surface normals
96
- - **normal_idw**: Inverse distance weighted sampling along normals
97
- - **pocket**: Detects interior cavities
98
-
99
- ### Sampling Strategies
100
-
101
- - **CONSTANT**: Fixed number of samples per constraint
102
- - **DENSITY**: Samples proportional to constraint volume
103
- - **INVERSE_SQUARE**: More samples near surface, fewer far away (recommended)
104
-
105
- ## API Reference
106
-
107
170
  ### SDFAnalyzer
108
171
 
172
+ Analyzes point clouds to detect SOLID and EMPTY regions.
173
+
109
174
  ```python
110
- from sdf_sampler import SDFAnalyzer, AnalyzerConfig
175
+ from sdf_sampler import SDFAnalyzer
176
+ from sdf_sampler.config import AnalyzerConfig, AutoAnalysisOptions
111
177
 
112
178
  # With default config
113
179
  analyzer = SDFAnalyzer()
@@ -136,10 +202,23 @@ print(f"EMPTY: {result.summary.empty_constraints}")
136
202
  constraints = result.constraints
137
203
  ```
138
204
 
205
+ #### Analysis Algorithms
206
+
207
+ | Algorithm | Description | Output |
208
+ |-----------|-------------|--------|
209
+ | `flood_fill` | Detects EMPTY (outside) regions by ray propagation from sky | Box or SamplePoint constraints |
210
+ | `voxel_regions` | Detects SOLID (underground) regions | Box or SamplePoint constraints |
211
+ | `normal_offset` | Generates paired SOLID/EMPTY boxes along surface normals | Box constraints |
212
+ | `normal_idw` | Inverse distance weighted sampling along normals | SamplePoint constraints |
213
+ | `pocket` | Detects interior cavities | Pocket constraints |
214
+
139
215
  ### SDFSampler
140
216
 
217
+ Generates training samples from constraints.
218
+
141
219
  ```python
142
- from sdf_sampler import SDFSampler, SamplerConfig
220
+ from sdf_sampler import SDFSampler
221
+ from sdf_sampler.config import SamplerConfig
143
222
 
144
223
  # With default config
145
224
  sampler = SDFSampler()
@@ -167,6 +246,14 @@ sampler.export_parquet(samples, "output.parquet")
167
246
  df = sampler.to_dataframe(samples)
168
247
  ```
169
248
 
249
+ #### Sampling Strategies
250
+
251
+ | Strategy | Description |
252
+ |----------|-------------|
253
+ | `constant` | Fixed number of samples per constraint |
254
+ | `density` | Samples proportional to constraint volume |
255
+ | `inverse_square` | More samples near surface, fewer far away (recommended) |
256
+
170
257
  ### Constraint Types
171
258
 
172
259
  The analyzer generates various constraint types:
@@ -180,6 +267,22 @@ Each constraint has:
180
267
  - `sign`: "solid" (negative SDF) or "empty" (positive SDF)
181
268
  - `weight`: Sample weight (default 1.0)
182
269
 
270
+ ### I/O Helpers
271
+
272
+ ```python
273
+ from sdf_sampler import load_point_cloud, export_parquet
274
+
275
+ # Load various formats
276
+ xyz, normals = load_point_cloud("scan.ply") # PLY (requires trimesh)
277
+ xyz, normals = load_point_cloud("scan.las") # LAS/LAZ (requires laspy)
278
+ xyz, normals = load_point_cloud("scan.csv") # CSV with x,y,z columns
279
+ xyz, normals = load_point_cloud("scan.npz") # NumPy archive
280
+ xyz, normals = load_point_cloud("scan.parquet") # Parquet
281
+
282
+ # Export samples
283
+ export_parquet(samples, "output.parquet")
284
+ ```
285
+
183
286
  ## Output Format
184
287
 
185
288
  The exported parquet file contains columns:
@@ -194,32 +297,40 @@ The exported parquet file contains columns:
194
297
  | is_surface | bool | Whether sample is on surface |
195
298
  | is_free | bool | Whether sample is in free space (EMPTY) |
196
299
 
197
- ## Configuration Options
300
+ ## Configuration Reference
198
301
 
199
302
  ### AnalyzerConfig
200
303
 
201
304
  | Option | Default | Description |
202
305
  |--------|---------|-------------|
203
- | min_gap_size | 0.10 | Minimum gap size for flood fill (meters) |
204
- | max_grid_dim | 200 | Maximum voxel grid dimension |
205
- | cone_angle | 15.0 | Ray propagation cone half-angle (degrees) |
206
- | normal_offset_pairs | 40 | Number of box pairs for normal_offset |
207
- | idw_sample_count | 1000 | Total IDW samples |
208
- | idw_max_distance | 0.5 | Maximum IDW distance (meters) |
209
- | hull_filter_enabled | True | Filter outside X-Y alpha shape |
210
- | hull_alpha | 1.0 | Alpha shape parameter |
306
+ | `min_gap_size` | 0.10 | Minimum gap size for flood fill (meters) |
307
+ | `max_grid_dim` | 200 | Maximum voxel grid dimension |
308
+ | `cone_angle` | 15.0 | Ray propagation cone half-angle (degrees) |
309
+ | `normal_offset_pairs` | 40 | Number of box pairs for normal_offset |
310
+ | `idw_sample_count` | 1000 | Total IDW samples |
311
+ | `idw_max_distance` | 0.5 | Maximum IDW distance (meters) |
312
+ | `hull_filter_enabled` | True | Filter outside X-Y alpha shape |
313
+ | `hull_alpha` | 1.0 | Alpha shape parameter |
211
314
 
212
315
  ### SamplerConfig
213
316
 
214
317
  | Option | Default | Description |
215
318
  |--------|---------|-------------|
216
- | total_samples | 10000 | Default total samples |
217
- | samples_per_primitive | 100 | Samples per constraint (CONSTANT) |
218
- | samples_per_cubic_meter | 10000 | Sample density (DENSITY) |
219
- | inverse_square_base_samples | 100 | Base samples (INVERSE_SQUARE) |
220
- | inverse_square_falloff | 2.0 | Falloff exponent |
221
- | near_band | 0.02 | Near-band width |
222
- | seed | 0 | Random seed |
319
+ | `total_samples` | 10000 | Default total samples |
320
+ | `samples_per_primitive` | 100 | Samples per constraint (CONSTANT) |
321
+ | `samples_per_cubic_meter` | 10000 | Sample density (DENSITY) |
322
+ | `inverse_square_base_samples` | 100 | Base samples (INVERSE_SQUARE) |
323
+ | `inverse_square_falloff` | 2.0 | Falloff exponent |
324
+ | `near_band` | 0.02 | Near-band width |
325
+ | `seed` | 0 | Random seed |
326
+
327
+ ## Integration with Ubik
328
+
329
+ sdf-sampler is the core analysis engine for [Ubik](https://github.com/Chiark-Collective/ubik), an interactive web application for SDF labeling. Use sdf-sampler directly for:
330
+
331
+ - Automated batch processing pipelines
332
+ - Integration into ML training workflows
333
+ - Custom analysis scripts
223
334
 
224
335
  ## License
225
336
 
@@ -0,0 +1,293 @@
1
+ # sdf-sampler
2
+
3
+ Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation.
4
+
5
+ A lightweight, standalone Python package for generating SDF training hints from point clouds. Automatically detects SOLID (inside) and EMPTY (outside) regions and generates training samples suitable for SDF regression models.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install sdf-sampler
11
+ ```
12
+
13
+ For additional I/O format support (PLY, LAS/LAZ):
14
+
15
+ ```bash
16
+ pip install sdf-sampler[io]
17
+ ```
18
+
19
+ ## Command-Line Interface
20
+
21
+ sdf-sampler provides a CLI for common workflows:
22
+
23
+ ```bash
24
+ # Run as module
25
+ python -m sdf_sampler --help
26
+
27
+ # Or use the installed command
28
+ sdf-sampler --help
29
+ ```
30
+
31
+ ### Commands
32
+
33
+ #### `pipeline` - Full workflow (recommended)
34
+
35
+ Run the complete pipeline: analyze point cloud → generate samples → export.
36
+
37
+ ```bash
38
+ # Basic usage
39
+ sdf-sampler pipeline scan.ply -o training_data.parquet
40
+
41
+ # With options
42
+ sdf-sampler pipeline scan.ply \
43
+ -o training_data.parquet \
44
+ -n 50000 \
45
+ -s inverse_square \
46
+ --save-constraints constraints.json \
47
+ -v
48
+ ```
49
+
50
+ Options:
51
+ - `-o, --output`: Output parquet file (default: `<input>_samples.parquet`)
52
+ - `-n, --total-samples`: Number of samples to generate (default: 10000)
53
+ - `-s, --strategy`: Sampling strategy: `constant`, `density`, `inverse_square` (default: `inverse_square`)
54
+ - `-a, --algorithms`: Specific algorithms to run (default: all)
55
+ - `--save-constraints`: Also save constraints to JSON
56
+ - `--seed`: Random seed for reproducibility
57
+ - `-v, --verbose`: Verbose output
58
+
59
+ #### `analyze` - Detect regions
60
+
61
+ Analyze a point cloud to detect SOLID/EMPTY regions.
62
+
63
+ ```bash
64
+ sdf-sampler analyze scan.ply -o constraints.json -v
65
+ ```
66
+
67
+ Options:
68
+ - `-o, --output`: Output JSON file (default: `<input>_constraints.json`)
69
+ - `-a, --algorithms`: Algorithms to run (see below)
70
+ - `--no-hull-filter`: Disable hull filtering
71
+ - `-v, --verbose`: Verbose output
72
+
73
+ #### `sample` - Generate training samples
74
+
75
+ Generate training samples from a constraints file.
76
+
77
+ ```bash
78
+ sdf-sampler sample scan.ply constraints.json -o samples.parquet -n 50000
79
+ ```
80
+
81
+ Options:
82
+ - `-o, --output`: Output parquet file
83
+ - `-n, --total-samples`: Number of samples (default: 10000)
84
+ - `-s, --strategy`: Sampling strategy (default: `inverse_square`)
85
+ - `--seed`: Random seed
86
+ - `-v, --verbose`: Verbose output
87
+
88
+ #### `info` - Inspect files
89
+
90
+ Show information about point clouds, constraints, or sample files.
91
+
92
+ ```bash
93
+ sdf-sampler info scan.ply
94
+ sdf-sampler info constraints.json
95
+ sdf-sampler info samples.parquet
96
+ ```
97
+
98
+ ## Python SDK
99
+
100
+ ### Quick Start
101
+
102
+ ```python
103
+ from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
104
+
105
+ # 1. Load point cloud (supports PLY, LAS, CSV, NPZ, Parquet)
106
+ xyz, normals = load_point_cloud("scan.ply")
107
+
108
+ # 2. Auto-analyze to detect EMPTY/SOLID regions
109
+ analyzer = SDFAnalyzer()
110
+ result = analyzer.analyze(xyz=xyz, normals=normals)
111
+ print(f"Generated {len(result.constraints)} constraints")
112
+
113
+ # 3. Generate training samples
114
+ sampler = SDFSampler()
115
+ samples = sampler.generate(
116
+ xyz=xyz,
117
+ constraints=result.constraints,
118
+ strategy="inverse_square",
119
+ total_samples=50000,
120
+ )
121
+
122
+ # 4. Export to parquet
123
+ sampler.export_parquet(samples, "training_data.parquet")
124
+ ```
125
+
126
+ ### SDFAnalyzer
127
+
128
+ Analyzes point clouds to detect SOLID and EMPTY regions.
129
+
130
+ ```python
131
+ from sdf_sampler import SDFAnalyzer
132
+ from sdf_sampler.config import AnalyzerConfig, AutoAnalysisOptions
133
+
134
+ # With default config
135
+ analyzer = SDFAnalyzer()
136
+
137
+ # With custom config
138
+ analyzer = SDFAnalyzer(config=AnalyzerConfig(
139
+ min_gap_size=0.10, # Minimum gap for flood fill
140
+ max_grid_dim=200, # Maximum voxel grid dimension
141
+ cone_angle=15.0, # Ray propagation cone angle
142
+ hull_filter_enabled=True, # Filter outside X-Y hull
143
+ ))
144
+
145
+ # Run analysis
146
+ result = analyzer.analyze(
147
+ xyz=xyz, # (N, 3) point positions
148
+ normals=normals, # (N, 3) point normals (optional)
149
+ algorithms=["flood_fill", "voxel_regions"], # Which algorithms to run
150
+ )
151
+
152
+ # Access results
153
+ print(f"Total constraints: {result.summary.total_constraints}")
154
+ print(f"SOLID: {result.summary.solid_constraints}")
155
+ print(f"EMPTY: {result.summary.empty_constraints}")
156
+
157
+ # Get constraint dicts for sampling
158
+ constraints = result.constraints
159
+ ```
160
+
161
+ #### Analysis Algorithms
162
+
163
+ | Algorithm | Description | Output |
164
+ |-----------|-------------|--------|
165
+ | `flood_fill` | Detects EMPTY (outside) regions by ray propagation from sky | Box or SamplePoint constraints |
166
+ | `voxel_regions` | Detects SOLID (underground) regions | Box or SamplePoint constraints |
167
+ | `normal_offset` | Generates paired SOLID/EMPTY boxes along surface normals | Box constraints |
168
+ | `normal_idw` | Inverse distance weighted sampling along normals | SamplePoint constraints |
169
+ | `pocket` | Detects interior cavities | Pocket constraints |
170
+
171
+ ### SDFSampler
172
+
173
+ Generates training samples from constraints.
174
+
175
+ ```python
176
+ from sdf_sampler import SDFSampler
177
+ from sdf_sampler.config import SamplerConfig
178
+
179
+ # With default config
180
+ sampler = SDFSampler()
181
+
182
+ # With custom config
183
+ sampler = SDFSampler(config=SamplerConfig(
184
+ total_samples=10000,
185
+ inverse_square_base_samples=100,
186
+ inverse_square_falloff=2.0,
187
+ near_band=0.02,
188
+ ))
189
+
190
+ # Generate samples
191
+ samples = sampler.generate(
192
+ xyz=xyz, # Point cloud for distance computation
193
+ constraints=constraints, # From analyzer.analyze().constraints
194
+ strategy="inverse_square", # Sampling strategy
195
+ seed=42, # For reproducibility
196
+ )
197
+
198
+ # Export
199
+ sampler.export_parquet(samples, "output.parquet")
200
+
201
+ # Or get DataFrame
202
+ df = sampler.to_dataframe(samples)
203
+ ```
204
+
205
+ #### Sampling Strategies
206
+
207
+ | Strategy | Description |
208
+ |----------|-------------|
209
+ | `constant` | Fixed number of samples per constraint |
210
+ | `density` | Samples proportional to constraint volume |
211
+ | `inverse_square` | More samples near surface, fewer far away (recommended) |
212
+
213
+ ### Constraint Types
214
+
215
+ The analyzer generates various constraint types:
216
+
217
+ - **BoxConstraint**: Axis-aligned bounding box
218
+ - **SphereConstraint**: Spherical region
219
+ - **SamplePointConstraint**: Direct point with signed distance
220
+ - **PocketConstraint**: Detected cavity region
221
+
222
+ Each constraint has:
223
+ - `sign`: "solid" (negative SDF) or "empty" (positive SDF)
224
+ - `weight`: Sample weight (default 1.0)
225
+
226
+ ### I/O Helpers
227
+
228
+ ```python
229
+ from sdf_sampler import load_point_cloud, export_parquet
230
+
231
+ # Load various formats
232
+ xyz, normals = load_point_cloud("scan.ply") # PLY (requires trimesh)
233
+ xyz, normals = load_point_cloud("scan.las") # LAS/LAZ (requires laspy)
234
+ xyz, normals = load_point_cloud("scan.csv") # CSV with x,y,z columns
235
+ xyz, normals = load_point_cloud("scan.npz") # NumPy archive
236
+ xyz, normals = load_point_cloud("scan.parquet") # Parquet
237
+
238
+ # Export samples
239
+ export_parquet(samples, "output.parquet")
240
+ ```
241
+
242
+ ## Output Format
243
+
244
+ The exported parquet file contains columns:
245
+
246
+ | Column | Type | Description |
247
+ |--------|------|-------------|
248
+ | x, y, z | float | 3D position |
249
+ | phi | float | Signed distance (negative=solid, positive=empty) |
250
+ | nx, ny, nz | float | Normal vector (if available) |
251
+ | weight | float | Sample weight |
252
+ | source | string | Sample origin (e.g., "box_solid", "flood_fill_empty") |
253
+ | is_surface | bool | Whether sample is on surface |
254
+ | is_free | bool | Whether sample is in free space (EMPTY) |
255
+
256
+ ## Configuration Reference
257
+
258
+ ### AnalyzerConfig
259
+
260
+ | Option | Default | Description |
261
+ |--------|---------|-------------|
262
+ | `min_gap_size` | 0.10 | Minimum gap size for flood fill (meters) |
263
+ | `max_grid_dim` | 200 | Maximum voxel grid dimension |
264
+ | `cone_angle` | 15.0 | Ray propagation cone half-angle (degrees) |
265
+ | `normal_offset_pairs` | 40 | Number of box pairs for normal_offset |
266
+ | `idw_sample_count` | 1000 | Total IDW samples |
267
+ | `idw_max_distance` | 0.5 | Maximum IDW distance (meters) |
268
+ | `hull_filter_enabled` | True | Filter outside X-Y alpha shape |
269
+ | `hull_alpha` | 1.0 | Alpha shape parameter |
270
+
271
+ ### SamplerConfig
272
+
273
+ | Option | Default | Description |
274
+ |--------|---------|-------------|
275
+ | `total_samples` | 10000 | Default total samples |
276
+ | `samples_per_primitive` | 100 | Samples per constraint (CONSTANT) |
277
+ | `samples_per_cubic_meter` | 10000 | Sample density (DENSITY) |
278
+ | `inverse_square_base_samples` | 100 | Base samples (INVERSE_SQUARE) |
279
+ | `inverse_square_falloff` | 2.0 | Falloff exponent |
280
+ | `near_band` | 0.02 | Near-band width |
281
+ | `seed` | 0 | Random seed |
282
+
283
+ ## Integration with Ubik
284
+
285
+ sdf-sampler is the core analysis engine for [Ubik](https://github.com/Chiark-Collective/ubik), an interactive web application for SDF labeling. Use sdf-sampler directly for:
286
+
287
+ - Automated batch processing pipelines
288
+ - Integration into ML training workflows
289
+ - Custom analysis scripts
290
+
291
+ ## License
292
+
293
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdf-sampler"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -42,8 +42,11 @@ dev = [
42
42
  ]
43
43
  all = ["sdf-sampler[io,dev]"]
44
44
 
45
+ [project.scripts]
46
+ sdf-sampler = "sdf_sampler.cli:main"
47
+
45
48
  [project.urls]
46
- Repository = "https://github.com/chiark/sdf-sampler"
49
+ Repository = "https://github.com/Chiark-Collective/sdf-sampler"
47
50
 
48
51
  [build-system]
49
52
  requires = ["hatchling"]
@@ -47,7 +47,7 @@ from sdf_sampler.models import (
47
47
  )
48
48
  from sdf_sampler.sampler import SDFSampler
49
49
 
50
- __version__ = "0.1.0"
50
+ __version__ = "0.2.0"
51
51
 
52
52
  __all__ = [
53
53
  # Main classes
@@ -0,0 +1,17 @@
1
+ # ABOUTME: Entry point for running sdf-sampler as a module
2
+ # ABOUTME: Enables `python -m sdf_sampler` invocation
3
+
4
+ """
5
+ Run sdf-sampler as a module.
6
+
7
+ Usage:
8
+ python -m sdf_sampler --help
9
+ python -m sdf_sampler analyze input.ply -o constraints.json
10
+ python -m sdf_sampler sample input.ply constraints.json -o samples.parquet
11
+ python -m sdf_sampler pipeline input.ply -o samples.parquet
12
+ """
13
+
14
+ from sdf_sampler.cli import main
15
+
16
+ if __name__ == "__main__":
17
+ raise SystemExit(main())
@@ -0,0 +1,457 @@
1
+ # ABOUTME: Command-line interface for sdf-sampler
2
+ # ABOUTME: Provides analyze, sample, and pipeline commands
3
+
4
+ """
5
+ CLI for sdf-sampler.
6
+
7
+ Usage:
8
+ python -m sdf_sampler analyze input.ply -o constraints.json
9
+ python -m sdf_sampler sample input.ply constraints.json -o samples.parquet
10
+ python -m sdf_sampler pipeline input.ply -o samples.parquet
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+
20
+
21
+ def main(argv: list[str] | None = None) -> int:
22
+ """Main CLI entry point."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="sdf-sampler",
25
+ description="Auto-analysis and sampling of point clouds for SDF training data",
26
+ )
27
+ parser.add_argument(
28
+ "--version", action="store_true", help="Show version and exit"
29
+ )
30
+
31
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
32
+
33
+ # analyze command
34
+ analyze_parser = subparsers.add_parser(
35
+ "analyze",
36
+ help="Analyze point cloud to detect SOLID/EMPTY regions",
37
+ )
38
+ analyze_parser.add_argument(
39
+ "input",
40
+ type=Path,
41
+ help="Input point cloud file (PLY, LAS, NPZ, CSV, Parquet)",
42
+ )
43
+ analyze_parser.add_argument(
44
+ "-o", "--output",
45
+ type=Path,
46
+ default=None,
47
+ help="Output constraints JSON file (default: <input>_constraints.json)",
48
+ )
49
+ analyze_parser.add_argument(
50
+ "-a", "--algorithms",
51
+ type=str,
52
+ nargs="+",
53
+ default=None,
54
+ help="Algorithms to run (flood_fill, voxel_regions, normal_offset, normal_idw, pocket)",
55
+ )
56
+ analyze_parser.add_argument(
57
+ "--no-hull-filter",
58
+ action="store_true",
59
+ help="Disable hull filtering",
60
+ )
61
+ analyze_parser.add_argument(
62
+ "-v", "--verbose",
63
+ action="store_true",
64
+ help="Verbose output",
65
+ )
66
+
67
+ # sample command
68
+ sample_parser = subparsers.add_parser(
69
+ "sample",
70
+ help="Generate training samples from constraints",
71
+ )
72
+ sample_parser.add_argument(
73
+ "input",
74
+ type=Path,
75
+ help="Input point cloud file",
76
+ )
77
+ sample_parser.add_argument(
78
+ "constraints",
79
+ type=Path,
80
+ help="Constraints JSON file (from analyze command)",
81
+ )
82
+ sample_parser.add_argument(
83
+ "-o", "--output",
84
+ type=Path,
85
+ default=None,
86
+ help="Output parquet file (default: <input>_samples.parquet)",
87
+ )
88
+ sample_parser.add_argument(
89
+ "-n", "--total-samples",
90
+ type=int,
91
+ default=10000,
92
+ help="Total number of samples to generate (default: 10000)",
93
+ )
94
+ sample_parser.add_argument(
95
+ "-s", "--strategy",
96
+ type=str,
97
+ choices=["constant", "density", "inverse_square"],
98
+ default="inverse_square",
99
+ help="Sampling strategy (default: inverse_square)",
100
+ )
101
+ sample_parser.add_argument(
102
+ "--seed",
103
+ type=int,
104
+ default=None,
105
+ help="Random seed for reproducibility",
106
+ )
107
+ sample_parser.add_argument(
108
+ "-v", "--verbose",
109
+ action="store_true",
110
+ help="Verbose output",
111
+ )
112
+
113
+ # pipeline command
114
+ pipeline_parser = subparsers.add_parser(
115
+ "pipeline",
116
+ help="Full pipeline: analyze + sample + export",
117
+ )
118
+ pipeline_parser.add_argument(
119
+ "input",
120
+ type=Path,
121
+ help="Input point cloud file",
122
+ )
123
+ pipeline_parser.add_argument(
124
+ "-o", "--output",
125
+ type=Path,
126
+ default=None,
127
+ help="Output parquet file (default: <input>_samples.parquet)",
128
+ )
129
+ pipeline_parser.add_argument(
130
+ "-a", "--algorithms",
131
+ type=str,
132
+ nargs="+",
133
+ default=None,
134
+ help="Algorithms to run",
135
+ )
136
+ pipeline_parser.add_argument(
137
+ "-n", "--total-samples",
138
+ type=int,
139
+ default=10000,
140
+ help="Total number of samples to generate (default: 10000)",
141
+ )
142
+ pipeline_parser.add_argument(
143
+ "-s", "--strategy",
144
+ type=str,
145
+ choices=["constant", "density", "inverse_square"],
146
+ default="inverse_square",
147
+ help="Sampling strategy (default: inverse_square)",
148
+ )
149
+ pipeline_parser.add_argument(
150
+ "--seed",
151
+ type=int,
152
+ default=None,
153
+ help="Random seed for reproducibility",
154
+ )
155
+ pipeline_parser.add_argument(
156
+ "--save-constraints",
157
+ type=Path,
158
+ default=None,
159
+ help="Also save constraints to JSON file",
160
+ )
161
+ pipeline_parser.add_argument(
162
+ "-v", "--verbose",
163
+ action="store_true",
164
+ help="Verbose output",
165
+ )
166
+
167
+ # info command
168
+ info_parser = subparsers.add_parser(
169
+ "info",
170
+ help="Show information about a point cloud or constraints file",
171
+ )
172
+ info_parser.add_argument(
173
+ "input",
174
+ type=Path,
175
+ help="Input file (point cloud or constraints JSON)",
176
+ )
177
+
178
+ args = parser.parse_args(argv)
179
+
180
+ if args.version:
181
+ from sdf_sampler import __version__
182
+ print(f"sdf-sampler {__version__}")
183
+ return 0
184
+
185
+ if args.command is None:
186
+ parser.print_help()
187
+ return 0
188
+
189
+ if args.command == "analyze":
190
+ return cmd_analyze(args)
191
+ elif args.command == "sample":
192
+ return cmd_sample(args)
193
+ elif args.command == "pipeline":
194
+ return cmd_pipeline(args)
195
+ elif args.command == "info":
196
+ return cmd_info(args)
197
+
198
+ return 0
199
+
200
+
201
+ def cmd_analyze(args: argparse.Namespace) -> int:
202
+ """Run analyze command."""
203
+ from sdf_sampler import SDFAnalyzer, load_point_cloud
204
+ from sdf_sampler.config import AutoAnalysisOptions
205
+
206
+ if not args.input.exists():
207
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
208
+ return 1
209
+
210
+ output = args.output or args.input.with_suffix(".constraints.json")
211
+
212
+ if args.verbose:
213
+ print(f"Loading point cloud: {args.input}")
214
+
215
+ try:
216
+ xyz, normals = load_point_cloud(str(args.input))
217
+ except Exception as e:
218
+ print(f"Error loading point cloud: {e}", file=sys.stderr)
219
+ return 1
220
+
221
+ if args.verbose:
222
+ print(f" Points: {len(xyz):,}")
223
+ print(f" Normals: {'yes' if normals is not None else 'no'}")
224
+
225
+ options = AutoAnalysisOptions(
226
+ hull_filter_enabled=not args.no_hull_filter,
227
+ )
228
+
229
+ if args.verbose:
230
+ algos = args.algorithms or ["all"]
231
+ print(f"Running analysis: {', '.join(algos)}")
232
+
233
+ analyzer = SDFAnalyzer()
234
+ result = analyzer.analyze(
235
+ xyz=xyz,
236
+ normals=normals,
237
+ algorithms=args.algorithms,
238
+ options=options,
239
+ )
240
+
241
+ if args.verbose:
242
+ print(f"Generated {len(result.constraints)} constraints")
243
+ print(f" SOLID: {result.summary.solid_constraints}")
244
+ print(f" EMPTY: {result.summary.empty_constraints}")
245
+
246
+ # Save constraints
247
+ with open(output, "w") as f:
248
+ json.dump(result.constraints, f, indent=2, default=_json_serializer)
249
+
250
+ print(f"Saved constraints to: {output}")
251
+ return 0
252
+
253
+
254
+ def cmd_sample(args: argparse.Namespace) -> int:
255
+ """Run sample command."""
256
+ from sdf_sampler import SDFSampler, load_point_cloud
257
+
258
+ if not args.input.exists():
259
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
260
+ return 1
261
+
262
+ if not args.constraints.exists():
263
+ print(f"Error: Constraints file not found: {args.constraints}", file=sys.stderr)
264
+ return 1
265
+
266
+ output = args.output or args.input.with_suffix(".samples.parquet")
267
+
268
+ if args.verbose:
269
+ print(f"Loading point cloud: {args.input}")
270
+
271
+ try:
272
+ xyz, normals = load_point_cloud(str(args.input))
273
+ except Exception as e:
274
+ print(f"Error loading point cloud: {e}", file=sys.stderr)
275
+ return 1
276
+
277
+ if args.verbose:
278
+ print(f"Loading constraints: {args.constraints}")
279
+
280
+ with open(args.constraints) as f:
281
+ constraints = json.load(f)
282
+
283
+ if args.verbose:
284
+ print(f" Constraints: {len(constraints)}")
285
+ print(f"Generating {args.total_samples:,} samples with strategy: {args.strategy}")
286
+
287
+ sampler = SDFSampler()
288
+ samples = sampler.generate(
289
+ xyz=xyz,
290
+ normals=normals,
291
+ constraints=constraints,
292
+ total_samples=args.total_samples,
293
+ strategy=args.strategy,
294
+ seed=args.seed,
295
+ )
296
+
297
+ if args.verbose:
298
+ print(f"Generated {len(samples)} samples")
299
+
300
+ sampler.export_parquet(samples, str(output))
301
+ print(f"Saved samples to: {output}")
302
+ return 0
303
+
304
+
305
+ def cmd_pipeline(args: argparse.Namespace) -> int:
306
+ """Run full pipeline: analyze + sample + export."""
307
+ from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
308
+ from sdf_sampler.config import AutoAnalysisOptions
309
+
310
+ if not args.input.exists():
311
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
312
+ return 1
313
+
314
+ output = args.output or args.input.with_suffix(".samples.parquet")
315
+
316
+ if args.verbose:
317
+ print(f"Loading point cloud: {args.input}")
318
+
319
+ try:
320
+ xyz, normals = load_point_cloud(str(args.input))
321
+ except Exception as e:
322
+ print(f"Error loading point cloud: {e}", file=sys.stderr)
323
+ return 1
324
+
325
+ if args.verbose:
326
+ print(f" Points: {len(xyz):,}")
327
+ print(f" Normals: {'yes' if normals is not None else 'no'}")
328
+
329
+ # Analyze
330
+ if args.verbose:
331
+ algos = args.algorithms or ["all"]
332
+ print(f"Running analysis: {', '.join(algos)}")
333
+
334
+ options = AutoAnalysisOptions()
335
+ analyzer = SDFAnalyzer()
336
+ result = analyzer.analyze(
337
+ xyz=xyz,
338
+ normals=normals,
339
+ algorithms=args.algorithms,
340
+ options=options,
341
+ )
342
+
343
+ if args.verbose:
344
+ print(f"Generated {len(result.constraints)} constraints")
345
+ print(f" SOLID: {result.summary.solid_constraints}")
346
+ print(f" EMPTY: {result.summary.empty_constraints}")
347
+
348
+ # Optionally save constraints
349
+ if args.save_constraints:
350
+ with open(args.save_constraints, "w") as f:
351
+ json.dump(result.constraints, f, indent=2, default=_json_serializer)
352
+ if args.verbose:
353
+ print(f"Saved constraints to: {args.save_constraints}")
354
+
355
+ # Sample
356
+ if args.verbose:
357
+ print(f"Generating {args.total_samples:,} samples with strategy: {args.strategy}")
358
+
359
+ sampler = SDFSampler()
360
+ samples = sampler.generate(
361
+ xyz=xyz,
362
+ normals=normals,
363
+ constraints=result.constraints,
364
+ total_samples=args.total_samples,
365
+ strategy=args.strategy,
366
+ seed=args.seed,
367
+ )
368
+
369
+ if args.verbose:
370
+ print(f"Generated {len(samples)} samples")
371
+
372
+ # Export
373
+ sampler.export_parquet(samples, str(output))
374
+ print(f"Saved samples to: {output}")
375
+ return 0
376
+
377
+
378
+ def cmd_info(args: argparse.Namespace) -> int:
379
+ """Show information about a file."""
380
+ if not args.input.exists():
381
+ print(f"Error: File not found: {args.input}", file=sys.stderr)
382
+ return 1
383
+
384
+ suffix = args.input.suffix.lower()
385
+
386
+ if suffix == ".json":
387
+ # Constraints file
388
+ with open(args.input) as f:
389
+ constraints = json.load(f)
390
+
391
+ print(f"Constraints file: {args.input}")
392
+ print(f" Total constraints: {len(constraints)}")
393
+
394
+ # Count by type and sign
395
+ by_type: dict[str, int] = {}
396
+ by_sign: dict[str, int] = {}
397
+ for c in constraints:
398
+ ctype = c.get("type", "unknown")
399
+ sign = c.get("sign", "unknown")
400
+ by_type[ctype] = by_type.get(ctype, 0) + 1
401
+ by_sign[sign] = by_sign.get(sign, 0) + 1
402
+
403
+ print(" By type:")
404
+ for t, count in sorted(by_type.items()):
405
+ print(f" {t}: {count}")
406
+ print(" By sign:")
407
+ for s, count in sorted(by_sign.items()):
408
+ print(f" {s}: {count}")
409
+
410
+ elif suffix == ".parquet":
411
+ import pandas as pd
412
+ df = pd.read_parquet(args.input)
413
+
414
+ print(f"Parquet file: {args.input}")
415
+ print(f" Samples: {len(df):,}")
416
+ print(f" Columns: {', '.join(df.columns)}")
417
+
418
+ if "source" in df.columns:
419
+ print(" By source:")
420
+ for source, count in df["source"].value_counts().items():
421
+ print(f" {source}: {count:,}")
422
+
423
+ if "phi" in df.columns:
424
+ print(f" Phi range: [{df['phi'].min():.4f}, {df['phi'].max():.4f}]")
425
+
426
+ else:
427
+ # Point cloud file
428
+ from sdf_sampler import load_point_cloud
429
+
430
+ try:
431
+ xyz, normals = load_point_cloud(str(args.input))
432
+ except Exception as e:
433
+ print(f"Error loading file: {e}", file=sys.stderr)
434
+ return 1
435
+
436
+ print(f"Point cloud: {args.input}")
437
+ print(f" Points: {len(xyz):,}")
438
+ print(f" Normals: {'yes' if normals is not None else 'no'}")
439
+ print(f" Bounds:")
440
+ print(f" X: [{xyz[:, 0].min():.4f}, {xyz[:, 0].max():.4f}]")
441
+ print(f" Y: [{xyz[:, 1].min():.4f}, {xyz[:, 1].max():.4f}]")
442
+ print(f" Z: [{xyz[:, 2].min():.4f}, {xyz[:, 2].max():.4f}]")
443
+
444
+ return 0
445
+
446
+
447
+ def _json_serializer(obj):
448
+ """JSON serializer for numpy types."""
449
+ if isinstance(obj, np.ndarray):
450
+ return obj.tolist()
451
+ if isinstance(obj, (np.integer, np.floating)):
452
+ return obj.item()
453
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
454
+
455
+
456
+ if __name__ == "__main__":
457
+ sys.exit(main())
@@ -1 +0,0 @@
1
- GITHUB_TOKEN=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
@@ -1,182 +0,0 @@
1
- # sdf-sampler
2
-
3
- Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation.
4
-
5
- A lightweight, standalone Python package for generating SDF training hints from point clouds. Automatically detects SOLID (inside) and EMPTY (outside) regions and generates training samples suitable for SDF regression models.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- pip install sdf-sampler
11
- ```
12
-
13
- For additional I/O format support (PLY, LAS/LAZ):
14
-
15
- ```bash
16
- pip install sdf-sampler[io]
17
- ```
18
-
19
- ## Quick Start
20
-
21
- ```python
22
- from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
23
-
24
- # 1. Load point cloud (supports PLY, LAS, CSV, NPZ, Parquet)
25
- xyz, normals = load_point_cloud("scan.ply")
26
-
27
- # 2. Auto-analyze to detect EMPTY/SOLID regions
28
- analyzer = SDFAnalyzer()
29
- result = analyzer.analyze(xyz=xyz, normals=normals)
30
- print(f"Generated {len(result.constraints)} constraints")
31
-
32
- # 3. Generate training samples
33
- sampler = SDFSampler()
34
- samples = sampler.generate(
35
- xyz=xyz,
36
- constraints=result.constraints,
37
- strategy="inverse_square",
38
- total_samples=50000,
39
- )
40
-
41
- # 4. Export to parquet
42
- sampler.export_parquet(samples, "training_data.parquet")
43
- ```
44
-
45
- ## Features
46
-
47
- ### Auto-Analysis Algorithms
48
-
49
- - **flood_fill**: Detects EMPTY (outside) regions by ray propagation from sky
50
- - **voxel_regions**: Detects SOLID (underground) regions
51
- - **normal_offset**: Generates paired SOLID/EMPTY boxes along surface normals
52
- - **normal_idw**: Inverse distance weighted sampling along normals
53
- - **pocket**: Detects interior cavities
54
-
55
- ### Sampling Strategies
56
-
57
- - **CONSTANT**: Fixed number of samples per constraint
58
- - **DENSITY**: Samples proportional to constraint volume
59
- - **INVERSE_SQUARE**: More samples near surface, fewer far away (recommended)
60
-
61
- ## API Reference
62
-
63
- ### SDFAnalyzer
64
-
65
- ```python
66
- from sdf_sampler import SDFAnalyzer, AnalyzerConfig
67
-
68
- # With default config
69
- analyzer = SDFAnalyzer()
70
-
71
- # With custom config
72
- analyzer = SDFAnalyzer(config=AnalyzerConfig(
73
- min_gap_size=0.10, # Minimum gap for flood fill
74
- max_grid_dim=200, # Maximum voxel grid dimension
75
- cone_angle=15.0, # Ray propagation cone angle
76
- hull_filter_enabled=True, # Filter outside X-Y hull
77
- ))
78
-
79
- # Run analysis
80
- result = analyzer.analyze(
81
- xyz=xyz, # (N, 3) point positions
82
- normals=normals, # (N, 3) point normals (optional)
83
- algorithms=["flood_fill", "voxel_regions"], # Which algorithms to run
84
- )
85
-
86
- # Access results
87
- print(f"Total constraints: {result.summary.total_constraints}")
88
- print(f"SOLID: {result.summary.solid_constraints}")
89
- print(f"EMPTY: {result.summary.empty_constraints}")
90
-
91
- # Get constraint dicts for sampling
92
- constraints = result.constraints
93
- ```
94
-
95
- ### SDFSampler
96
-
97
- ```python
98
- from sdf_sampler import SDFSampler, SamplerConfig
99
-
100
- # With default config
101
- sampler = SDFSampler()
102
-
103
- # With custom config
104
- sampler = SDFSampler(config=SamplerConfig(
105
- total_samples=10000,
106
- inverse_square_base_samples=100,
107
- inverse_square_falloff=2.0,
108
- near_band=0.02,
109
- ))
110
-
111
- # Generate samples
112
- samples = sampler.generate(
113
- xyz=xyz, # Point cloud for distance computation
114
- constraints=constraints, # From analyzer.analyze().constraints
115
- strategy="inverse_square", # Sampling strategy
116
- seed=42, # For reproducibility
117
- )
118
-
119
- # Export
120
- sampler.export_parquet(samples, "output.parquet")
121
-
122
- # Or get DataFrame
123
- df = sampler.to_dataframe(samples)
124
- ```
125
-
126
- ### Constraint Types
127
-
128
- The analyzer generates various constraint types:
129
-
130
- - **BoxConstraint**: Axis-aligned bounding box
131
- - **SphereConstraint**: Spherical region
132
- - **SamplePointConstraint**: Direct point with signed distance
133
- - **PocketConstraint**: Detected cavity region
134
-
135
- Each constraint has:
136
- - `sign`: "solid" (negative SDF) or "empty" (positive SDF)
137
- - `weight`: Sample weight (default 1.0)
138
-
139
- ## Output Format
140
-
141
- The exported parquet file contains columns:
142
-
143
- | Column | Type | Description |
144
- |--------|------|-------------|
145
- | x, y, z | float | 3D position |
146
- | phi | float | Signed distance (negative=solid, positive=empty) |
147
- | nx, ny, nz | float | Normal vector (if available) |
148
- | weight | float | Sample weight |
149
- | source | string | Sample origin (e.g., "box_solid", "flood_fill_empty") |
150
- | is_surface | bool | Whether sample is on surface |
151
- | is_free | bool | Whether sample is in free space (EMPTY) |
152
-
153
- ## Configuration Options
154
-
155
- ### AnalyzerConfig
156
-
157
- | Option | Default | Description |
158
- |--------|---------|-------------|
159
- | min_gap_size | 0.10 | Minimum gap size for flood fill (meters) |
160
- | max_grid_dim | 200 | Maximum voxel grid dimension |
161
- | cone_angle | 15.0 | Ray propagation cone half-angle (degrees) |
162
- | normal_offset_pairs | 40 | Number of box pairs for normal_offset |
163
- | idw_sample_count | 1000 | Total IDW samples |
164
- | idw_max_distance | 0.5 | Maximum IDW distance (meters) |
165
- | hull_filter_enabled | True | Filter outside X-Y alpha shape |
166
- | hull_alpha | 1.0 | Alpha shape parameter |
167
-
168
- ### SamplerConfig
169
-
170
- | Option | Default | Description |
171
- |--------|---------|-------------|
172
- | total_samples | 10000 | Default total samples |
173
- | samples_per_primitive | 100 | Samples per constraint (CONSTANT) |
174
- | samples_per_cubic_meter | 10000 | Sample density (DENSITY) |
175
- | inverse_square_base_samples | 100 | Base samples (INVERSE_SQUARE) |
176
- | inverse_square_falloff | 2.0 | Falloff exponent |
177
- | near_band | 0.02 | Near-band width |
178
- | seed | 0 | Random seed |
179
-
180
- ## License
181
-
182
- MIT
File without changes
File without changes