sdf-sampler 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdf_sampler/__init__.py +78 -0
- sdf_sampler/algorithms/__init__.py +19 -0
- sdf_sampler/algorithms/flood_fill.py +233 -0
- sdf_sampler/algorithms/normal_idw.py +99 -0
- sdf_sampler/algorithms/normal_offset.py +111 -0
- sdf_sampler/algorithms/pocket.py +146 -0
- sdf_sampler/algorithms/voxel_grid.py +339 -0
- sdf_sampler/algorithms/voxel_regions.py +80 -0
- sdf_sampler/analyzer.py +299 -0
- sdf_sampler/config.py +171 -0
- sdf_sampler/io.py +178 -0
- sdf_sampler/models/__init__.py +49 -0
- sdf_sampler/models/analysis.py +85 -0
- sdf_sampler/models/constraints.py +192 -0
- sdf_sampler/models/samples.py +49 -0
- sdf_sampler/sampler.py +439 -0
- sdf_sampler/sampling/__init__.py +15 -0
- sdf_sampler/sampling/box.py +131 -0
- sdf_sampler/sampling/brush.py +63 -0
- sdf_sampler/sampling/ray_carve.py +134 -0
- sdf_sampler/sampling/sphere.py +57 -0
- sdf_sampler-0.1.0.dist-info/METADATA +226 -0
- sdf_sampler-0.1.0.dist-info/RECORD +25 -0
- sdf_sampler-0.1.0.dist-info/WHEEL +4 -0
- sdf_sampler-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ABOUTME: Auto-analysis result models
|
|
2
|
+
# ABOUTME: Defines algorithm types and analysis result structures
|
|
3
|
+
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AlgorithmType(str, Enum):
|
|
12
|
+
"""Available auto-analysis algorithms."""
|
|
13
|
+
|
|
14
|
+
POCKET = "pocket" # Cavity detection via voxel flood-fill
|
|
15
|
+
NORMAL_OFFSET = "normal_offset" # Surface-relative offset regions
|
|
16
|
+
FLOOD_FILL = "flood_fill" # Exterior flood fill from sky for EMPTY regions
|
|
17
|
+
VOXEL_REGIONS = "voxel_regions" # Underground regions via voxel classification for SOLID boxes
|
|
18
|
+
NORMAL_IDW = "normal_idw" # Inverse distance weighted normal sampling
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ALL_ALGORITHMS = [
|
|
22
|
+
AlgorithmType.POCKET,
|
|
23
|
+
AlgorithmType.NORMAL_OFFSET,
|
|
24
|
+
AlgorithmType.FLOOD_FILL,
|
|
25
|
+
AlgorithmType.VOXEL_REGIONS,
|
|
26
|
+
AlgorithmType.NORMAL_IDW,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeneratedConstraint(BaseModel):
|
|
31
|
+
"""A constraint generated by auto-analysis.
|
|
32
|
+
|
|
33
|
+
Wraps standard constraint data with algorithm metadata.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
constraint: dict[str, Any] = Field(..., description="Constraint data (type, sign, geometry)")
|
|
37
|
+
algorithm: AlgorithmType = Field(..., description="Algorithm that generated this")
|
|
38
|
+
confidence: float = Field(
|
|
39
|
+
..., ge=0.0, le=1.0, description="Algorithm's confidence in this constraint"
|
|
40
|
+
)
|
|
41
|
+
description: str = Field(..., description="Human-readable description")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AlgorithmStats(BaseModel):
|
|
45
|
+
"""Statistics for a single algorithm's contribution."""
|
|
46
|
+
|
|
47
|
+
constraints_generated: int = Field(0, description="Number of constraints from this algorithm")
|
|
48
|
+
coverage_description: str = Field("", description="What this algorithm detected")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AnalysisSummary(BaseModel):
|
|
52
|
+
"""Summary statistics for auto-analysis results."""
|
|
53
|
+
|
|
54
|
+
total_constraints: int = Field(..., description="Total constraints generated")
|
|
55
|
+
solid_constraints: int = Field(..., description="Constraints marking solid regions")
|
|
56
|
+
empty_constraints: int = Field(..., description="Constraints marking empty regions")
|
|
57
|
+
algorithms_contributing: int = Field(..., description="Number of algorithms that contributed")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AnalysisResult(BaseModel):
|
|
61
|
+
"""Complete auto-analysis result with generated constraints.
|
|
62
|
+
|
|
63
|
+
This is the primary return type from SDFAnalyzer.analyze().
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(ser_json_timedelta="iso8601")
|
|
67
|
+
|
|
68
|
+
analysis_id: str = Field(..., description="Unique identifier for this analysis")
|
|
69
|
+
computed_at: datetime = Field(
|
|
70
|
+
default_factory=lambda: datetime.now(UTC),
|
|
71
|
+
description="When analysis was performed",
|
|
72
|
+
)
|
|
73
|
+
algorithms_run: list[str] = Field(..., description="Algorithms that were executed")
|
|
74
|
+
summary: AnalysisSummary = Field(..., description="Aggregated statistics")
|
|
75
|
+
algorithm_stats: dict[str, AlgorithmStats] = Field(
|
|
76
|
+
default_factory=dict, description="Per-algorithm statistics"
|
|
77
|
+
)
|
|
78
|
+
generated_constraints: list[GeneratedConstraint] = Field(
|
|
79
|
+
default_factory=list, description="Constraints ready for use"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def constraints(self) -> list[dict[str, Any]]:
|
|
84
|
+
"""Get just the constraint dicts for convenience."""
|
|
85
|
+
return [gc.constraint for gc in self.generated_constraints]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# ABOUTME: Constraint models for SDF region marking
|
|
2
|
+
# ABOUTME: Defines geometric primitives and regions for inside/outside labeling
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Annotated, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SignConvention(str, Enum):
|
|
12
|
+
"""Sign convention for SDF values.
|
|
13
|
+
|
|
14
|
+
User-friendly terminology:
|
|
15
|
+
- SOLID = negative SDF (inside material)
|
|
16
|
+
- EMPTY = positive SDF (outside, free space)
|
|
17
|
+
- SURFACE = zero SDF (on the boundary)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
SOLID = "solid" # Inside / negative SDF
|
|
21
|
+
EMPTY = "empty" # Outside / positive SDF
|
|
22
|
+
SURFACE = "surface" # On boundary / zero SDF
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseConstraint(BaseModel):
|
|
26
|
+
"""Base class for all constraint types."""
|
|
27
|
+
|
|
28
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
29
|
+
name: str | None = Field(default=None, description="Optional user-friendly name")
|
|
30
|
+
sign: SignConvention = Field(..., description="Label: solid (inside) or empty (outside)")
|
|
31
|
+
weight: float = Field(default=1.0, ge=0.0, le=10.0, description="Sample weight")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BoxConstraint(BaseConstraint):
|
|
35
|
+
"""Axis-aligned bounding box constraint."""
|
|
36
|
+
|
|
37
|
+
type: Literal["box"] = "box"
|
|
38
|
+
center: tuple[float, float, float] = Field(..., description="Box center (x, y, z)")
|
|
39
|
+
half_extents: tuple[float, float, float] = Field(
|
|
40
|
+
..., description="Half-extents in each dimension"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SphereConstraint(BaseConstraint):
|
|
45
|
+
"""Spherical region constraint."""
|
|
46
|
+
|
|
47
|
+
type: Literal["sphere"] = "sphere"
|
|
48
|
+
center: tuple[float, float, float] = Field(..., description="Sphere center (x, y, z)")
|
|
49
|
+
radius: float = Field(..., gt=0, description="Sphere radius")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HalfspaceConstraint(BaseConstraint):
|
|
53
|
+
"""Half-space (infinite plane) constraint.
|
|
54
|
+
|
|
55
|
+
Defines a plane and which side is labeled.
|
|
56
|
+
The normal points toward the EMPTY (outside) side by convention.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
type: Literal["halfspace"] = "halfspace"
|
|
60
|
+
point: tuple[float, float, float] = Field(..., description="A point on the plane")
|
|
61
|
+
normal: tuple[float, float, float] = Field(
|
|
62
|
+
..., description="Outward normal (points toward empty/outside)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CylinderConstraint(BaseConstraint):
|
|
67
|
+
"""Cylindrical region constraint."""
|
|
68
|
+
|
|
69
|
+
type: Literal["cylinder"] = "cylinder"
|
|
70
|
+
center: tuple[float, float, float] = Field(..., description="Center of cylinder base")
|
|
71
|
+
axis: tuple[float, float, float] = Field(default=(0, 0, 1), description="Cylinder axis")
|
|
72
|
+
radius: float = Field(..., gt=0, description="Cylinder radius")
|
|
73
|
+
height: float = Field(..., gt=0, description="Cylinder height")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BrushStrokeConstraint(BaseConstraint):
|
|
77
|
+
"""User-painted volumetric stroke in 3D space."""
|
|
78
|
+
|
|
79
|
+
type: Literal["brush_stroke"] = "brush_stroke"
|
|
80
|
+
stroke_points: list[tuple[float, float, float]] = Field(
|
|
81
|
+
..., description="Path of brush center positions"
|
|
82
|
+
)
|
|
83
|
+
radius: float = Field(..., gt=0, description="Brush/stroke radius")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SeedPropagationConstraint(BaseConstraint):
|
|
87
|
+
"""Seed point with spatial propagation."""
|
|
88
|
+
|
|
89
|
+
type: Literal["seed_propagation"] = "seed_propagation"
|
|
90
|
+
seed_point: tuple[float, float, float] = Field(..., description="Seed location")
|
|
91
|
+
seed_index: int | None = Field(default=None, description="Nearest point index")
|
|
92
|
+
propagation_radius: float = Field(..., gt=0, description="Max propagation distance")
|
|
93
|
+
propagation_method: Literal["euclidean", "geodesic"] = Field(
|
|
94
|
+
default="euclidean", description="Distance metric for propagation"
|
|
95
|
+
)
|
|
96
|
+
propagated_indices: list[int] = Field(
|
|
97
|
+
default_factory=list, description="Points reached by propagation"
|
|
98
|
+
)
|
|
99
|
+
confidences: list[float] = Field(
|
|
100
|
+
default_factory=list, description="Confidence per propagated point"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RayInfo(BaseModel):
|
|
105
|
+
"""Single ray from a ray-scribble interaction."""
|
|
106
|
+
|
|
107
|
+
origin: tuple[float, float, float] = Field(..., description="Ray origin (camera position)")
|
|
108
|
+
direction: tuple[float, float, float] = Field(..., description="Normalized ray direction")
|
|
109
|
+
hit_distance: float = Field(..., gt=0, description="Distance to first point cloud hit")
|
|
110
|
+
surface_normal: tuple[float, float, float] | None = Field(
|
|
111
|
+
default=None, description="Surface normal at hit point if available"
|
|
112
|
+
)
|
|
113
|
+
hit_point_index: int | None = Field(
|
|
114
|
+
default=None, description="Index of hit point in consolidated positions"
|
|
115
|
+
)
|
|
116
|
+
local_spacing: float | None = Field(
|
|
117
|
+
default=None, description="Local point spacing at hit point (k-NN mean distance)"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class RayCarveConstraint(BaseConstraint):
|
|
122
|
+
"""Constraint from ray-scribble interaction.
|
|
123
|
+
|
|
124
|
+
Each ray defines:
|
|
125
|
+
- EMPTY samples along ray from origin to (hit_distance - empty_band)
|
|
126
|
+
- SURFACE samples near hit_distance (from -surface_band to +back_buffer)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
type: Literal["ray_carve"] = "ray_carve"
|
|
130
|
+
rays: list[RayInfo] = Field(..., description="Rays cast during scribble stroke")
|
|
131
|
+
empty_band_width: float = Field(
|
|
132
|
+
default=0.1, gt=0, description="Distance before hit to sample as EMPTY"
|
|
133
|
+
)
|
|
134
|
+
surface_band_width: float = Field(
|
|
135
|
+
default=0.02, gt=0, description="Distance before hit for SURFACE samples"
|
|
136
|
+
)
|
|
137
|
+
back_buffer_width: float = Field(
|
|
138
|
+
default=0.0, ge=0, description="Fixed distance past hit (fallback when no local_spacing)"
|
|
139
|
+
)
|
|
140
|
+
back_buffer_coefficient: float = Field(
|
|
141
|
+
default=1.0, ge=0, description="Multiplier for per-ray local_spacing"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PocketConstraint(BaseConstraint):
|
|
146
|
+
"""Pocket (cavity) constraint from voxel analysis."""
|
|
147
|
+
|
|
148
|
+
type: Literal["pocket"] = "pocket"
|
|
149
|
+
pocket_id: int = Field(..., description="Pocket identifier from analysis")
|
|
150
|
+
voxel_count: int = Field(..., description="Number of voxels in pocket")
|
|
151
|
+
centroid: tuple[float, float, float] = Field(..., description="Pocket centroid")
|
|
152
|
+
bounds_low: tuple[float, float, float] = Field(..., description="Pocket AABB min")
|
|
153
|
+
bounds_high: tuple[float, float, float] = Field(..., description="Pocket AABB max")
|
|
154
|
+
volume_estimate: float = Field(..., description="Estimated volume in world units")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SliceSelectionConstraint(BaseConstraint):
|
|
158
|
+
"""Constraint from 2D slice selection."""
|
|
159
|
+
|
|
160
|
+
type: Literal["slice_selection"] = "slice_selection"
|
|
161
|
+
point_indices: list[int] = Field(..., description="Selected point indices")
|
|
162
|
+
slice_plane: Literal["xy", "xz", "yz"] = Field(..., description="Which plane was used")
|
|
163
|
+
slice_position: float = Field(..., description="Position along perpendicular axis")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SamplePointConstraint(BaseConstraint):
|
|
167
|
+
"""Single point sample constraint from IDW normal sampling.
|
|
168
|
+
|
|
169
|
+
Direct training samples at specific 3D positions with known signed distance.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
type: Literal["sample_point"] = "sample_point"
|
|
173
|
+
position: tuple[float, float, float] = Field(..., description="Sample position (x, y, z)")
|
|
174
|
+
distance: float = Field(
|
|
175
|
+
..., description="Signed distance to surface (negative=solid, positive=empty)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Union type for all constraints
|
|
180
|
+
Constraint = Annotated[
|
|
181
|
+
BoxConstraint
|
|
182
|
+
| SphereConstraint
|
|
183
|
+
| HalfspaceConstraint
|
|
184
|
+
| CylinderConstraint
|
|
185
|
+
| BrushStrokeConstraint
|
|
186
|
+
| SeedPropagationConstraint
|
|
187
|
+
| RayCarveConstraint
|
|
188
|
+
| PocketConstraint
|
|
189
|
+
| SliceSelectionConstraint
|
|
190
|
+
| SamplePointConstraint,
|
|
191
|
+
Field(discriminator="type"),
|
|
192
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ABOUTME: Training sample models for SDF data generation
|
|
2
|
+
# ABOUTME: Defines sample formats and sampling strategies
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SamplingStrategy(str, Enum):
|
|
10
|
+
"""Sampling strategy for generating training samples from constraints."""
|
|
11
|
+
|
|
12
|
+
CONSTANT = "constant" # Fixed samples per constraint
|
|
13
|
+
DENSITY = "density" # Samples proportional to constraint volume
|
|
14
|
+
INVERSE_SQUARE = "inverse_square" # More samples near surface, fewer far away
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TrainingSample(BaseModel):
|
|
18
|
+
"""Single training sample with SDF value.
|
|
19
|
+
|
|
20
|
+
This is the core output format for SDF training data.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
x: float
|
|
24
|
+
y: float
|
|
25
|
+
z: float
|
|
26
|
+
phi: float = Field(..., description="Signed distance value")
|
|
27
|
+
nx: float | None = None
|
|
28
|
+
ny: float | None = None
|
|
29
|
+
nz: float | None = None
|
|
30
|
+
weight: float = 1.0
|
|
31
|
+
source: str = Field(..., description="Sample source (e.g., 'surface_anchor', 'near_band')")
|
|
32
|
+
is_surface: bool = False
|
|
33
|
+
is_free: bool = False
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""Convert to dictionary for DataFrame construction."""
|
|
37
|
+
return {
|
|
38
|
+
"x": self.x,
|
|
39
|
+
"y": self.y,
|
|
40
|
+
"z": self.z,
|
|
41
|
+
"phi": self.phi,
|
|
42
|
+
"nx": self.nx,
|
|
43
|
+
"ny": self.ny,
|
|
44
|
+
"nz": self.nz,
|
|
45
|
+
"weight": self.weight,
|
|
46
|
+
"source": self.source,
|
|
47
|
+
"is_surface": self.is_surface,
|
|
48
|
+
"is_free": self.is_free,
|
|
49
|
+
}
|