sdf-sampler 0.3.0__tar.gz → 0.4.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.3.0 → sdf_sampler-0.4.0}/CHANGELOG.md +11 -2
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/PKG-INFO +1 -1
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/pyproject.toml +1 -1
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/__init__.py +1 -1
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/analyzer.py +2 -1
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/cli.py +9 -9
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/analysis.py +7 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampler.py +9 -7
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_equivalence.py +117 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/uv.lock +1 -1
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/.gitignore +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/LICENSE +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/README.md +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/__main__.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/__init__.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/flood_fill.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/normal_idw.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/normal_offset.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/pocket.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/voxel_grid.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/voxel_regions.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/config.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/io.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/__init__.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/constraints.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/samples.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/__init__.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/box.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/brush.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/ray_carve.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/sphere.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/__init__.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_analyzer.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_integration.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_models.py +0 -0
- {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_sampler.py +0 -0
|
@@ -5,6 +5,15 @@ 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.4.0] - 2025-01-30
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Default algorithms no longer include `normal_idw`** - The `normal_idw` algorithm is now opt-in only. Default algorithms are: `flood_fill`, `voxel_regions`, `normal_offset`. To use `normal_idw`, explicitly pass `algorithms=["normal_idw"]` or include it in your algorithm list.
|
|
13
|
+
- **Surface point count is now a direct count** - Replaced `surface_point_ratio` with `surface_point_count`. Instead of specifying a percentage, you now specify the exact number of surface points to include.
|
|
14
|
+
- CLI: `--surface-point-count 1000` (default: 1000)
|
|
15
|
+
- SDK: `sampler.generate(..., include_surface_points=True, surface_point_count=1000)`
|
|
16
|
+
|
|
8
17
|
## [0.3.0] - 2025-01-29
|
|
9
18
|
|
|
10
19
|
### Added
|
|
@@ -15,8 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
24
|
- Output mode control: `--flood-fill-output`, `--voxel-regions-output` (boxes/samples/both)
|
|
16
25
|
- **Surface point inclusion**
|
|
17
26
|
- `--include-surface-points` flag to include original points with phi=0
|
|
18
|
-
- `--surface-point-
|
|
19
|
-
- SDK: `sampler.generate(..., include_surface_points=True,
|
|
27
|
+
- `--surface-point-count` to specify number of surface points (default 1000)
|
|
28
|
+
- SDK: `sampler.generate(..., include_surface_points=True, surface_point_count=1000)`
|
|
20
29
|
|
|
21
30
|
## [0.2.0] - 2025-01-29
|
|
22
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdf-sampler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation
|
|
5
5
|
Project-URL: Repository, https://github.com/Chiark-Collective/sdf-sampler
|
|
6
6
|
Author-email: Liam <liam@example.com>
|
|
@@ -14,6 +14,7 @@ from sdf_sampler.algorithms.voxel_regions import generate_voxel_region_constrain
|
|
|
14
14
|
from sdf_sampler.config import AnalyzerConfig, AutoAnalysisOptions
|
|
15
15
|
from sdf_sampler.models.analysis import (
|
|
16
16
|
ALL_ALGORITHMS,
|
|
17
|
+
DEFAULT_ALGORITHMS,
|
|
17
18
|
AlgorithmStats,
|
|
18
19
|
AlgorithmType,
|
|
19
20
|
AnalysisResult,
|
|
@@ -93,7 +94,7 @@ class SDFAnalyzer:
|
|
|
93
94
|
raise ValueError(f"normals shape {normals.shape} doesn't match xyz {xyz.shape}")
|
|
94
95
|
|
|
95
96
|
# Determine which algorithms to run
|
|
96
|
-
algo_list = algorithms if algorithms else [a.value for a in
|
|
97
|
+
algo_list = algorithms if algorithms else [a.value for a in DEFAULT_ALGORITHMS]
|
|
97
98
|
algo_list = [a for a in algo_list if a in [alg.value for alg in ALL_ALGORITHMS]]
|
|
98
99
|
|
|
99
100
|
# Run algorithms and collect constraints
|
|
@@ -169,10 +169,10 @@ def add_output_options(parser: argparse.ArgumentParser) -> None:
|
|
|
169
169
|
help="Include original surface points (phi=0) in output",
|
|
170
170
|
)
|
|
171
171
|
group.add_argument(
|
|
172
|
-
"--surface-point-
|
|
173
|
-
type=
|
|
174
|
-
default=
|
|
175
|
-
help="
|
|
172
|
+
"--surface-point-count",
|
|
173
|
+
type=int,
|
|
174
|
+
default=1000,
|
|
175
|
+
help="Number of surface points to include (default: 1000)",
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
|
|
@@ -463,7 +463,7 @@ def cmd_sample(args: argparse.Namespace) -> int:
|
|
|
463
463
|
# Include surface points if requested
|
|
464
464
|
if args.include_surface_points:
|
|
465
465
|
samples = _add_surface_points(
|
|
466
|
-
samples, xyz, normals, args.
|
|
466
|
+
samples, xyz, normals, args.surface_point_count, args.verbose
|
|
467
467
|
)
|
|
468
468
|
|
|
469
469
|
if args.verbose:
|
|
@@ -541,7 +541,7 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
|
541
541
|
# Include surface points if requested
|
|
542
542
|
if args.include_surface_points:
|
|
543
543
|
samples = _add_surface_points(
|
|
544
|
-
samples, xyz, normals, args.
|
|
544
|
+
samples, xyz, normals, args.surface_point_count, args.verbose
|
|
545
545
|
)
|
|
546
546
|
|
|
547
547
|
if args.verbose:
|
|
@@ -557,14 +557,14 @@ def _add_surface_points(
|
|
|
557
557
|
samples: list,
|
|
558
558
|
xyz: np.ndarray,
|
|
559
559
|
normals: np.ndarray | None,
|
|
560
|
-
|
|
560
|
+
count: int,
|
|
561
561
|
verbose: bool,
|
|
562
562
|
) -> list:
|
|
563
563
|
"""Add surface points to sample list."""
|
|
564
564
|
from sdf_sampler.models import TrainingSample
|
|
565
565
|
|
|
566
|
-
n_surface =
|
|
567
|
-
if n_surface
|
|
566
|
+
n_surface = min(count, len(xyz))
|
|
567
|
+
if n_surface <= 0:
|
|
568
568
|
return samples
|
|
569
569
|
|
|
570
570
|
# Subsample if needed
|
|
@@ -26,6 +26,13 @@ ALL_ALGORITHMS = [
|
|
|
26
26
|
AlgorithmType.NORMAL_IDW,
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
# Default algorithms (excludes normal_idw which is opt-in)
|
|
30
|
+
DEFAULT_ALGORITHMS = [
|
|
31
|
+
AlgorithmType.FLOOD_FILL,
|
|
32
|
+
AlgorithmType.VOXEL_REGIONS,
|
|
33
|
+
AlgorithmType.NORMAL_OFFSET,
|
|
34
|
+
]
|
|
35
|
+
|
|
29
36
|
|
|
30
37
|
class GeneratedConstraint(BaseModel):
|
|
31
38
|
"""A constraint generated by auto-analysis.
|
|
@@ -66,7 +66,7 @@ class SDFSampler:
|
|
|
66
66
|
strategy: str | SamplingStrategy = SamplingStrategy.INVERSE_SQUARE,
|
|
67
67
|
seed: int | None = None,
|
|
68
68
|
include_surface_points: bool = False,
|
|
69
|
-
|
|
69
|
+
surface_point_count: int | None = None,
|
|
70
70
|
) -> list[TrainingSample]:
|
|
71
71
|
"""Generate training samples from constraints.
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ class SDFSampler:
|
|
|
78
78
|
strategy: Sampling strategy (CONSTANT, DENSITY, or INVERSE_SQUARE)
|
|
79
79
|
seed: Random seed for reproducibility
|
|
80
80
|
include_surface_points: If True, include original surface points with phi=0
|
|
81
|
-
|
|
81
|
+
surface_point_count: Number of surface points to include (default: 1000, or len(xyz) if smaller)
|
|
82
82
|
|
|
83
83
|
Returns:
|
|
84
84
|
List of TrainingSample objects
|
|
@@ -160,8 +160,10 @@ class SDFSampler:
|
|
|
160
160
|
|
|
161
161
|
# Add surface points if requested
|
|
162
162
|
if include_surface_points:
|
|
163
|
+
# Default to 1000 surface points, or all points if smaller
|
|
164
|
+
count = surface_point_count if surface_point_count is not None else min(1000, len(xyz))
|
|
163
165
|
samples.extend(
|
|
164
|
-
self._generate_surface_points(xyz, normals,
|
|
166
|
+
self._generate_surface_points(xyz, normals, count, rng)
|
|
165
167
|
)
|
|
166
168
|
|
|
167
169
|
return samples
|
|
@@ -170,7 +172,7 @@ class SDFSampler:
|
|
|
170
172
|
self,
|
|
171
173
|
xyz: np.ndarray,
|
|
172
174
|
normals: np.ndarray | None,
|
|
173
|
-
|
|
175
|
+
count: int,
|
|
174
176
|
rng: np.random.Generator,
|
|
175
177
|
) -> list[TrainingSample]:
|
|
176
178
|
"""Generate surface point samples (phi=0) from the input point cloud.
|
|
@@ -178,14 +180,14 @@ class SDFSampler:
|
|
|
178
180
|
Args:
|
|
179
181
|
xyz: Point cloud positions (N, 3)
|
|
180
182
|
normals: Optional point normals (N, 3)
|
|
181
|
-
|
|
183
|
+
count: Number of surface points to include
|
|
182
184
|
rng: Random number generator
|
|
183
185
|
|
|
184
186
|
Returns:
|
|
185
187
|
List of TrainingSample objects with phi=0
|
|
186
188
|
"""
|
|
187
|
-
n_surface =
|
|
188
|
-
if n_surface
|
|
189
|
+
n_surface = min(count, len(xyz))
|
|
190
|
+
if n_surface <= 0:
|
|
189
191
|
return []
|
|
190
192
|
|
|
191
193
|
# Subsample if needed
|
|
@@ -759,3 +759,120 @@ class TestFullPipelineEquivalence:
|
|
|
759
759
|
f"Sample count ratio too high: {ratio:.2f} "
|
|
760
760
|
f"(standalone={standalone_sample_count}, backend={backend_sample_count})"
|
|
761
761
|
)
|
|
762
|
+
|
|
763
|
+
@requires_backend
|
|
764
|
+
def test_inverse_square_pipeline_equivalence(self, trench_pointcloud):
|
|
765
|
+
"""Test inverse_square sampling produces equivalent results.
|
|
766
|
+
|
|
767
|
+
This is the recommended production workflow: auto-analyze + inverse_square sampling.
|
|
768
|
+
"""
|
|
769
|
+
xyz, normals = trench_pointcloud
|
|
770
|
+
|
|
771
|
+
# Shared analysis options
|
|
772
|
+
analysis_options = AutoAnalysisOptions(
|
|
773
|
+
flood_fill_output="samples",
|
|
774
|
+
flood_fill_sample_count=100,
|
|
775
|
+
voxel_regions_output="samples",
|
|
776
|
+
voxel_regions_sample_count=100,
|
|
777
|
+
idw_sample_count=100,
|
|
778
|
+
hull_filter_enabled=False,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
# Run standalone with inverse_square
|
|
782
|
+
standalone_analyzer = SDFAnalyzer()
|
|
783
|
+
standalone_result = standalone_analyzer.analyze(
|
|
784
|
+
xyz=xyz,
|
|
785
|
+
normals=normals,
|
|
786
|
+
algorithms=["flood_fill", "voxel_regions", "normal_idw"],
|
|
787
|
+
options=analysis_options,
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
standalone_sampler = SDFSampler()
|
|
791
|
+
standalone_samples = standalone_sampler.generate(
|
|
792
|
+
xyz=xyz,
|
|
793
|
+
normals=normals,
|
|
794
|
+
constraints=standalone_result.constraints,
|
|
795
|
+
strategy="inverse_square",
|
|
796
|
+
total_samples=5000,
|
|
797
|
+
seed=42,
|
|
798
|
+
include_surface_points=False, # Test without surface points first
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Run backend with inverse_square
|
|
802
|
+
from sdf_labeler_api.config import Settings
|
|
803
|
+
from sdf_labeler_api.services.auto_analysis_service import AutoAnalysisService
|
|
804
|
+
from sdf_labeler_api.services.sampling_service import SamplingService
|
|
805
|
+
from sdf_labeler_api.services.project_service import ProjectService
|
|
806
|
+
from sdf_labeler_api.services.constraint_service import ConstraintService
|
|
807
|
+
from sdf_labeler_api.models.project import ProjectCreate
|
|
808
|
+
from sdf_labeler_api.models.samples import SampleGenerationRequest, SamplingStrategy
|
|
809
|
+
|
|
810
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
811
|
+
data_dir = Path(tmpdir)
|
|
812
|
+
|
|
813
|
+
import sdf_labeler_api.config as backend_config
|
|
814
|
+
original_settings = backend_config.settings
|
|
815
|
+
backend_config.settings = Settings(data_dir=data_dir)
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
project_service = ProjectService(data_dir)
|
|
819
|
+
project = project_service.create(ProjectCreate(name="test"))
|
|
820
|
+
project_id = project.id
|
|
821
|
+
|
|
822
|
+
setup_backend_project(data_dir, project_id, xyz, normals)
|
|
823
|
+
|
|
824
|
+
# Analyze
|
|
825
|
+
backend_analysis = AutoAnalysisService(backend_config.settings)
|
|
826
|
+
backend_options = get_backend_options(analysis_options)
|
|
827
|
+
backend_result = asyncio.run(backend_analysis.analyze(
|
|
828
|
+
project_id=project_id,
|
|
829
|
+
algorithms=["flood_fill", "voxel_regions", "normal_idw"],
|
|
830
|
+
recompute=True,
|
|
831
|
+
options=backend_options,
|
|
832
|
+
))
|
|
833
|
+
|
|
834
|
+
# Add constraints to project
|
|
835
|
+
constraint_service = ConstraintService()
|
|
836
|
+
for gc in backend_result.generated_constraints:
|
|
837
|
+
constraint_service.add_from_dict(project_id, gc.constraint)
|
|
838
|
+
|
|
839
|
+
# Sample with inverse_square
|
|
840
|
+
sampling_service = SamplingService()
|
|
841
|
+
request = SampleGenerationRequest(
|
|
842
|
+
total_samples=5000,
|
|
843
|
+
strategy=SamplingStrategy.INVERSE_SQUARE,
|
|
844
|
+
seed=42,
|
|
845
|
+
)
|
|
846
|
+
backend_sample_result = sampling_service.generate(project_id, request)
|
|
847
|
+
backend_samples = backend_sample_result.samples
|
|
848
|
+
finally:
|
|
849
|
+
backend_config.settings = original_settings
|
|
850
|
+
|
|
851
|
+
# Compare results
|
|
852
|
+
print(f"\nInverse square pipeline comparison:")
|
|
853
|
+
print(f" Standalone constraints: {len(standalone_result.constraints)}")
|
|
854
|
+
print(f" Backend constraints: {len(backend_result.generated_constraints)}")
|
|
855
|
+
print(f" Standalone samples: {len(standalone_samples)}")
|
|
856
|
+
print(f" Backend samples: {len(backend_samples)}")
|
|
857
|
+
|
|
858
|
+
# Verify phi distribution is similar (more samples near 0)
|
|
859
|
+
standalone_near_surface = sum(1 for s in standalone_samples if abs(s.phi) < 0.1)
|
|
860
|
+
backend_near_surface = sum(1 for s in backend_samples if abs(s.phi) < 0.1)
|
|
861
|
+
|
|
862
|
+
print(f" Standalone near-surface (|phi|<0.1): {standalone_near_surface}")
|
|
863
|
+
print(f" Backend near-surface (|phi|<0.1): {backend_near_surface}")
|
|
864
|
+
|
|
865
|
+
# Both should have majority of samples near surface (inverse_square characteristic)
|
|
866
|
+
standalone_ratio = standalone_near_surface / len(standalone_samples) if standalone_samples else 0
|
|
867
|
+
backend_ratio = backend_near_surface / len(backend_samples) if backend_samples else 0
|
|
868
|
+
|
|
869
|
+
assert standalone_ratio > 0.3, f"Standalone should have >30% near-surface, got {standalone_ratio:.1%}"
|
|
870
|
+
assert backend_ratio > 0.3, f"Backend should have >30% near-surface, got {backend_ratio:.1%}"
|
|
871
|
+
|
|
872
|
+
# Ratios should be similar
|
|
873
|
+
if standalone_ratio > 0 and backend_ratio > 0:
|
|
874
|
+
ratio_diff = abs(standalone_ratio - backend_ratio)
|
|
875
|
+
assert ratio_diff < 0.2, (
|
|
876
|
+
f"Near-surface ratio difference too high: {ratio_diff:.1%} "
|
|
877
|
+
f"(standalone={standalone_ratio:.1%}, backend={backend_ratio:.1%})"
|
|
878
|
+
)
|
|
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
|