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.
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/.gitignore +1 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/CHANGELOG.md +13 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/PKG-INFO +150 -39
- sdf_sampler-0.2.0/README.md +293 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/pyproject.toml +5 -2
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/__init__.py +1 -1
- sdf_sampler-0.2.0/src/sdf_sampler/__main__.py +17 -0
- sdf_sampler-0.2.0/src/sdf_sampler/cli.py +457 -0
- sdf_sampler-0.1.0/.github_token.env +0 -1
- sdf_sampler-0.1.0/README.md +0 -182
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/LICENSE +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/__init__.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/flood_fill.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/normal_idw.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/normal_offset.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/pocket.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/voxel_grid.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/algorithms/voxel_regions.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/analyzer.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/config.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/io.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/__init__.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/analysis.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/constraints.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/models/samples.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampler.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/__init__.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/box.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/brush.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/ray_carve.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/src/sdf_sampler/sampling/sphere.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/__init__.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_analyzer.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_equivalence.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_integration.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_models.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/tests/test_sampler.py +0 -0
- {sdf_sampler-0.1.0 → sdf_sampler-0.2.0}/uv.lock +0 -0
|
@@ -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.
|
|
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/
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
49
|
+
Repository = "https://github.com/Chiark-Collective/sdf-sampler"
|
|
47
50
|
|
|
48
51
|
[build-system]
|
|
49
52
|
requires = ["hatchling"]
|
|
@@ -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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
|
sdf_sampler-0.1.0/README.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|