sdf-sampler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ # ABOUTME: Ray carve constraint sampling functions
2
+ # ABOUTME: Generates training samples from ray-scribble interactions
3
+
4
+ import numpy as np
5
+
6
+ from sdf_sampler.models.constraints import RayCarveConstraint
7
+ from sdf_sampler.models.samples import TrainingSample
8
+
9
+
10
+ def sample_ray_carve(
11
+ constraint: RayCarveConstraint,
12
+ rng: np.random.Generator,
13
+ n_samples_per_ray: int,
14
+ ) -> list[TrainingSample]:
15
+ """Generate samples from ray-carve constraint.
16
+
17
+ For each ray:
18
+ 1. Sample EMPTY points uniformly along ray from origin to (hit - empty_band)
19
+ 2. Sample SURFACE points in band around hit point
20
+
21
+ Args:
22
+ constraint: Ray carve constraint to sample
23
+ rng: Random number generator
24
+ n_samples_per_ray: Number of samples per ray
25
+
26
+ Returns:
27
+ List of TrainingSample objects
28
+ """
29
+ samples = []
30
+
31
+ # Pre-compute ray data for outlier detection
32
+ ray_data = []
33
+ for ray in constraint.rays:
34
+ origin = np.array(ray.origin)
35
+ direction = np.array(ray.direction)
36
+ direction = direction / np.linalg.norm(direction)
37
+ hit_point = origin + direction * ray.hit_distance
38
+ ray_data.append(
39
+ {
40
+ "origin": origin,
41
+ "direction": direction,
42
+ "hit_distance": ray.hit_distance,
43
+ "hit_point": hit_point,
44
+ "local_spacing": ray.local_spacing,
45
+ "surface_normal": ray.surface_normal,
46
+ }
47
+ )
48
+
49
+ # Detect outliers: rays that pass through gaps
50
+ effective_hit_distances = []
51
+ for i, ray in enumerate(ray_data):
52
+ effective_dist = ray["hit_distance"]
53
+
54
+ for j, other in enumerate(ray_data):
55
+ if i == j:
56
+ continue
57
+
58
+ dir_dot = np.dot(ray["direction"], other["direction"])
59
+ if dir_dot > 0.95:
60
+ to_hit = ray["hit_point"] - other["origin"]
61
+ proj_dist = np.dot(to_hit, other["direction"])
62
+
63
+ if proj_dist > other["hit_distance"] * 1.1:
64
+ effective_dist = min(effective_dist, other["hit_distance"])
65
+
66
+ effective_hit_distances.append(effective_dist)
67
+
68
+ for idx, ray in enumerate(ray_data):
69
+ origin = ray["origin"]
70
+ direction = ray["direction"]
71
+ hit_dist = effective_hit_distances[idx]
72
+
73
+ if ray["local_spacing"] is not None:
74
+ buffer_zone = ray["local_spacing"] * constraint.back_buffer_coefficient
75
+ else:
76
+ buffer_zone = constraint.back_buffer_width
77
+
78
+ # EMPTY samples along ray (before hit, stopping at buffer zone)
79
+ empty_end = hit_dist - buffer_zone
80
+ n_empty = n_samples_per_ray // 2
81
+
82
+ if empty_end > 0:
83
+ for _ in range(n_empty):
84
+ t = rng.uniform(0, empty_end)
85
+ point = origin + t * direction
86
+
87
+ samples.append(
88
+ TrainingSample(
89
+ x=float(point[0]),
90
+ y=float(point[1]),
91
+ z=float(point[2]),
92
+ phi=hit_dist - t,
93
+ nx=float(direction[0]),
94
+ ny=float(direction[1]),
95
+ nz=float(direction[2]),
96
+ weight=constraint.weight,
97
+ source="ray_carve_empty",
98
+ is_surface=False,
99
+ is_free=True,
100
+ )
101
+ )
102
+
103
+ # SURFACE samples near hit
104
+ n_surface = n_samples_per_ray - n_empty
105
+ for _ in range(n_surface):
106
+ t = rng.uniform(
107
+ hit_dist - constraint.surface_band_width,
108
+ hit_dist,
109
+ )
110
+ point = origin + t * direction
111
+ phi = 0.0
112
+
113
+ if ray["surface_normal"]:
114
+ nx, ny, nz = ray["surface_normal"]
115
+ else:
116
+ nx, ny, nz = -direction[0], -direction[1], -direction[2]
117
+
118
+ samples.append(
119
+ TrainingSample(
120
+ x=float(point[0]),
121
+ y=float(point[1]),
122
+ z=float(point[2]),
123
+ phi=phi,
124
+ nx=float(nx),
125
+ ny=float(ny),
126
+ nz=float(nz),
127
+ weight=constraint.weight,
128
+ source="ray_carve_surface",
129
+ is_surface=True,
130
+ is_free=False,
131
+ )
132
+ )
133
+
134
+ return samples
@@ -0,0 +1,57 @@
1
+ # ABOUTME: Sphere constraint sampling functions
2
+ # ABOUTME: Generates training samples from spherical region constraints
3
+
4
+ import numpy as np
5
+
6
+ from sdf_sampler.models.constraints import SignConvention, SphereConstraint
7
+ from sdf_sampler.models.samples import TrainingSample
8
+
9
+
10
+ def sample_sphere(
11
+ constraint: SphereConstraint,
12
+ rng: np.random.Generator,
13
+ near_band: float,
14
+ n_samples: int,
15
+ ) -> list[TrainingSample]:
16
+ """Generate samples from a sphere constraint.
17
+
18
+ Args:
19
+ constraint: Sphere constraint to sample
20
+ rng: Random number generator
21
+ near_band: Near-band width for offset
22
+ n_samples: Number of samples to generate
23
+
24
+ Returns:
25
+ List of TrainingSample objects
26
+ """
27
+ samples = []
28
+ center = np.array(constraint.center)
29
+ radius = constraint.radius
30
+
31
+ for _ in range(n_samples):
32
+ direction = rng.standard_normal(3)
33
+ direction /= np.linalg.norm(direction)
34
+
35
+ point = center + radius * direction
36
+
37
+ offset = near_band if constraint.sign == SignConvention.EMPTY else -near_band
38
+ point = point + offset * direction
39
+ phi = offset
40
+
41
+ samples.append(
42
+ TrainingSample(
43
+ x=float(point[0]),
44
+ y=float(point[1]),
45
+ z=float(point[2]),
46
+ phi=phi,
47
+ nx=float(direction[0]),
48
+ ny=float(direction[1]),
49
+ nz=float(direction[2]),
50
+ weight=constraint.weight,
51
+ source=f"sphere_{constraint.sign.value}",
52
+ is_surface=False,
53
+ is_free=constraint.sign == SignConvention.EMPTY,
54
+ )
55
+ )
56
+
57
+ return samples
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: sdf-sampler
3
+ Version: 0.1.0
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
6
+ Author-email: Liam <liam@example.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: machine-learning,point-cloud,sampling,sdf,signed-distance-field
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: alphashape>=1.3.1
21
+ Requires-Dist: numpy>=1.26.0
22
+ Requires-Dist: pandas>=2.1.0
23
+ Requires-Dist: pyarrow>=14.0.0
24
+ Requires-Dist: pydantic>=2.5.0
25
+ Requires-Dist: scipy>=1.11.0
26
+ Provides-Extra: all
27
+ Requires-Dist: laspy[laszip]>=2.5.0; extra == 'all'
28
+ Requires-Dist: mypy>=1.8.0; extra == 'all'
29
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'all'
30
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'all'
31
+ Requires-Dist: pytest>=8.0.0; extra == 'all'
32
+ Requires-Dist: ruff>=0.5.0; extra == 'all'
33
+ Requires-Dist: trimesh>=4.0.0; extra == 'all'
34
+ Provides-Extra: dev
35
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
36
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
37
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
38
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.5.0; extra == 'dev'
40
+ Provides-Extra: io
41
+ Requires-Dist: laspy[laszip]>=2.5.0; extra == 'io'
42
+ Requires-Dist: trimesh>=4.0.0; extra == 'io'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # sdf-sampler
46
+
47
+ Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation.
48
+
49
+ 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.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install sdf-sampler
55
+ ```
56
+
57
+ For additional I/O format support (PLY, LAS/LAZ):
58
+
59
+ ```bash
60
+ pip install sdf-sampler[io]
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
67
+
68
+ # 1. Load point cloud (supports PLY, LAS, CSV, NPZ, Parquet)
69
+ xyz, normals = load_point_cloud("scan.ply")
70
+
71
+ # 2. Auto-analyze to detect EMPTY/SOLID regions
72
+ analyzer = SDFAnalyzer()
73
+ result = analyzer.analyze(xyz=xyz, normals=normals)
74
+ print(f"Generated {len(result.constraints)} constraints")
75
+
76
+ # 3. Generate training samples
77
+ sampler = SDFSampler()
78
+ samples = sampler.generate(
79
+ xyz=xyz,
80
+ constraints=result.constraints,
81
+ strategy="inverse_square",
82
+ total_samples=50000,
83
+ )
84
+
85
+ # 4. Export to parquet
86
+ sampler.export_parquet(samples, "training_data.parquet")
87
+ ```
88
+
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
+ ### SDFAnalyzer
108
+
109
+ ```python
110
+ from sdf_sampler import SDFAnalyzer, AnalyzerConfig
111
+
112
+ # With default config
113
+ analyzer = SDFAnalyzer()
114
+
115
+ # With custom config
116
+ analyzer = SDFAnalyzer(config=AnalyzerConfig(
117
+ min_gap_size=0.10, # Minimum gap for flood fill
118
+ max_grid_dim=200, # Maximum voxel grid dimension
119
+ cone_angle=15.0, # Ray propagation cone angle
120
+ hull_filter_enabled=True, # Filter outside X-Y hull
121
+ ))
122
+
123
+ # Run analysis
124
+ result = analyzer.analyze(
125
+ xyz=xyz, # (N, 3) point positions
126
+ normals=normals, # (N, 3) point normals (optional)
127
+ algorithms=["flood_fill", "voxel_regions"], # Which algorithms to run
128
+ )
129
+
130
+ # Access results
131
+ print(f"Total constraints: {result.summary.total_constraints}")
132
+ print(f"SOLID: {result.summary.solid_constraints}")
133
+ print(f"EMPTY: {result.summary.empty_constraints}")
134
+
135
+ # Get constraint dicts for sampling
136
+ constraints = result.constraints
137
+ ```
138
+
139
+ ### SDFSampler
140
+
141
+ ```python
142
+ from sdf_sampler import SDFSampler, SamplerConfig
143
+
144
+ # With default config
145
+ sampler = SDFSampler()
146
+
147
+ # With custom config
148
+ sampler = SDFSampler(config=SamplerConfig(
149
+ total_samples=10000,
150
+ inverse_square_base_samples=100,
151
+ inverse_square_falloff=2.0,
152
+ near_band=0.02,
153
+ ))
154
+
155
+ # Generate samples
156
+ samples = sampler.generate(
157
+ xyz=xyz, # Point cloud for distance computation
158
+ constraints=constraints, # From analyzer.analyze().constraints
159
+ strategy="inverse_square", # Sampling strategy
160
+ seed=42, # For reproducibility
161
+ )
162
+
163
+ # Export
164
+ sampler.export_parquet(samples, "output.parquet")
165
+
166
+ # Or get DataFrame
167
+ df = sampler.to_dataframe(samples)
168
+ ```
169
+
170
+ ### Constraint Types
171
+
172
+ The analyzer generates various constraint types:
173
+
174
+ - **BoxConstraint**: Axis-aligned bounding box
175
+ - **SphereConstraint**: Spherical region
176
+ - **SamplePointConstraint**: Direct point with signed distance
177
+ - **PocketConstraint**: Detected cavity region
178
+
179
+ Each constraint has:
180
+ - `sign`: "solid" (negative SDF) or "empty" (positive SDF)
181
+ - `weight`: Sample weight (default 1.0)
182
+
183
+ ## Output Format
184
+
185
+ The exported parquet file contains columns:
186
+
187
+ | Column | Type | Description |
188
+ |--------|------|-------------|
189
+ | x, y, z | float | 3D position |
190
+ | phi | float | Signed distance (negative=solid, positive=empty) |
191
+ | nx, ny, nz | float | Normal vector (if available) |
192
+ | weight | float | Sample weight |
193
+ | source | string | Sample origin (e.g., "box_solid", "flood_fill_empty") |
194
+ | is_surface | bool | Whether sample is on surface |
195
+ | is_free | bool | Whether sample is in free space (EMPTY) |
196
+
197
+ ## Configuration Options
198
+
199
+ ### AnalyzerConfig
200
+
201
+ | Option | Default | Description |
202
+ |--------|---------|-------------|
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 |
211
+
212
+ ### SamplerConfig
213
+
214
+ | Option | Default | Description |
215
+ |--------|---------|-------------|
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 |
223
+
224
+ ## License
225
+
226
+ MIT
@@ -0,0 +1,25 @@
1
+ sdf_sampler/__init__.py,sha256=9LyfRS8RsgknQvp5TKUKKknuQD9Ovm74qlfbxlOvfCE,1891
2
+ sdf_sampler/analyzer.py,sha256=p5Pkoa01dBGFqqQs2wpWrr8ilPMsjkB4ODJEI2IWbdo,11674
3
+ sdf_sampler/config.py,sha256=lrPM1ktFkv32RRtOz6R-ShUFlBZ2LvoAl1hRLThmgKw,5185
4
+ sdf_sampler/io.py,sha256=DfdXJa_2KQhja_T_a-jlVADcAABeF-NR8e3_vnIJHnk,4968
5
+ sdf_sampler/sampler.py,sha256=QVrI1TxfnSgQWYcJdRCEQqvFekPvFBUYJfrbjQj-ABY,15551
6
+ sdf_sampler/algorithms/__init__.py,sha256=pp7tSZ8q0zRXZ5S8D3tho7bJ62pmW8jceuuRXtiXIzU,777
7
+ sdf_sampler/algorithms/flood_fill.py,sha256=iWGPPtOPSs0Cg7pxUlXUKGloFNCr8PuCHguRNy3c56c,7042
8
+ sdf_sampler/algorithms/normal_idw.py,sha256=uX3MQDTDX0wVilwxDE9dFj9hm2xuBDHV-AZqspjz7sk,3270
9
+ sdf_sampler/algorithms/normal_offset.py,sha256=SzWMaK_Ws2Bg-vu_b5_viCEqJts92ezdXaFfhE97RaE,3712
10
+ sdf_sampler/algorithms/pocket.py,sha256=d6cvFz8C1ZFGgaOEySnFcKMPlHZ-kqOU_bBvOoXTUpw,4978
11
+ sdf_sampler/algorithms/voxel_grid.py,sha256=LJz5V02AxSBRL0-kNEV3yEN_cMWFymGiepX0yysE6dw,11133
12
+ sdf_sampler/algorithms/voxel_regions.py,sha256=YHr-apdq9wTo_po6v5L9HxQLkcZ1kP3KfJhoaQDZJOE,2422
13
+ sdf_sampler/models/__init__.py,sha256=XWzMnPgvZyZF1ZUSfqlspIitLkzMn_0Yxj-hRF-ALEQ,1134
14
+ sdf_sampler/models/analysis.py,sha256=h9CIxBeTQ89Z2-B3qdrlHM8qtOA0caijTYwzd9HsyUw,3251
15
+ sdf_sampler/models/constraints.py,sha256=k0v6-JwXmUkFXGycDpPG44amjoRo13xz1ggqdr9NVtE,7316
16
+ sdf_sampler/models/samples.py,sha256=0DD7Z8D70zJKRTq16pik9SgGiRDeXh56EqmBZfql_sk,1474
17
+ sdf_sampler/sampling/__init__.py,sha256=mbeU9DTHxR4R1D4WqjEzaYHq7bV1nSv-wywc3YoD_Lg,492
18
+ sdf_sampler/sampling/box.py,sha256=qRAumR1z_7vU9rfNvk-B6xNu62UeHSyQNghYiTwVR_Y,3912
19
+ sdf_sampler/sampling/brush.py,sha256=CcAgOYLdYXMM3y_H4fIwyzRJ8PZivFxkUHP7d0ElpNM,1991
20
+ sdf_sampler/sampling/ray_carve.py,sha256=EsfzEGk33q0iWVzOJKDAJi2iWEsY-JZXmEfEZ0dmNdg,4444
21
+ sdf_sampler/sampling/sphere.py,sha256=Xqpwq-RcEnAD6HhoyIC-ErxRHDknDKMtYf6aWUJ43_U,1680
22
+ sdf_sampler-0.1.0.dist-info/METADATA,sha256=CygvgjodkmWtCsBMMXz1MLEGufR1XGZUSabpA7dII-w,7165
23
+ sdf_sampler-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ sdf_sampler-0.1.0.dist-info/licenses/LICENSE,sha256=eeB8aLnEG-dgFYs2KqfMJaP52GFQT8sZPHwaYnHRW8E,1061
25
+ sdf_sampler-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Liam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.