sdf-sampler 0.4.0__py3-none-any.whl → 0.6.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 +4 -2
- sdf_sampler/cli.py +48 -60
- sdf_sampler/io.py +91 -11
- sdf_sampler/sampler.py +115 -1
- sdf_sampler/sampling/box.py +8 -4
- {sdf_sampler-0.4.0.dist-info → sdf_sampler-0.6.0.dist-info}/METADATA +1 -1
- {sdf_sampler-0.4.0.dist-info → sdf_sampler-0.6.0.dist-info}/RECORD +10 -10
- {sdf_sampler-0.4.0.dist-info → sdf_sampler-0.6.0.dist-info}/WHEEL +0 -0
- {sdf_sampler-0.4.0.dist-info → sdf_sampler-0.6.0.dist-info}/entry_points.txt +0 -0
- {sdf_sampler-0.4.0.dist-info → sdf_sampler-0.6.0.dist-info}/licenses/LICENSE +0 -0
sdf_sampler/__init__.py
CHANGED
|
@@ -28,7 +28,7 @@ Example usage:
|
|
|
28
28
|
|
|
29
29
|
from sdf_sampler.analyzer import SDFAnalyzer
|
|
30
30
|
from sdf_sampler.config import AnalyzerConfig, SamplerConfig
|
|
31
|
-
from sdf_sampler.io import export_parquet, load_point_cloud
|
|
31
|
+
from sdf_sampler.io import Mesh, export_parquet, load_mesh, load_point_cloud
|
|
32
32
|
from sdf_sampler.models import (
|
|
33
33
|
AlgorithmType,
|
|
34
34
|
AnalysisResult,
|
|
@@ -47,7 +47,7 @@ from sdf_sampler.models import (
|
|
|
47
47
|
)
|
|
48
48
|
from sdf_sampler.sampler import SDFSampler
|
|
49
49
|
|
|
50
|
-
__version__ = "0.
|
|
50
|
+
__version__ = "0.5.0"
|
|
51
51
|
|
|
52
52
|
__all__ = [
|
|
53
53
|
# Main classes
|
|
@@ -58,6 +58,8 @@ __all__ = [
|
|
|
58
58
|
"SamplerConfig",
|
|
59
59
|
# I/O
|
|
60
60
|
"load_point_cloud",
|
|
61
|
+
"load_mesh",
|
|
62
|
+
"Mesh",
|
|
61
63
|
"export_parquet",
|
|
62
64
|
# Models
|
|
63
65
|
"SignConvention",
|
sdf_sampler/cli.py
CHANGED
|
@@ -174,6 +174,14 @@ def add_output_options(parser: argparse.ArgumentParser) -> None:
|
|
|
174
174
|
default=1000,
|
|
175
175
|
help="Number of surface points to include (default: 1000)",
|
|
176
176
|
)
|
|
177
|
+
group.add_argument(
|
|
178
|
+
"--mesh",
|
|
179
|
+
type=Path,
|
|
180
|
+
default=None,
|
|
181
|
+
help="Mesh file for area-weighted surface sampling (PLY/OBJ/STL). "
|
|
182
|
+
"If provided, surface points are sampled uniformly by surface area "
|
|
183
|
+
"instead of by vertex count. Recommended for meshes with uneven vertex density.",
|
|
184
|
+
)
|
|
177
185
|
|
|
178
186
|
|
|
179
187
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -418,7 +426,7 @@ def cmd_analyze(args: argparse.Namespace) -> int:
|
|
|
418
426
|
|
|
419
427
|
def cmd_sample(args: argparse.Namespace) -> int:
|
|
420
428
|
"""Run sample command."""
|
|
421
|
-
from sdf_sampler import SDFSampler, load_point_cloud
|
|
429
|
+
from sdf_sampler import SDFSampler, load_mesh, load_point_cloud
|
|
422
430
|
|
|
423
431
|
if not args.input.exists():
|
|
424
432
|
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
|
@@ -439,6 +447,19 @@ def cmd_sample(args: argparse.Namespace) -> int:
|
|
|
439
447
|
print(f"Error loading point cloud: {e}", file=sys.stderr)
|
|
440
448
|
return 1
|
|
441
449
|
|
|
450
|
+
# Load mesh for area-weighted surface sampling if provided
|
|
451
|
+
mesh = None
|
|
452
|
+
if args.mesh:
|
|
453
|
+
if args.verbose:
|
|
454
|
+
print(f"Loading mesh for area-weighted sampling: {args.mesh}")
|
|
455
|
+
try:
|
|
456
|
+
mesh = load_mesh(str(args.mesh))
|
|
457
|
+
if args.verbose:
|
|
458
|
+
print(f" Vertices: {len(mesh.vertices):,}, Faces: {len(mesh.faces):,}")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
print(f"Error loading mesh: {e}", file=sys.stderr)
|
|
461
|
+
return 1
|
|
462
|
+
|
|
442
463
|
if args.verbose:
|
|
443
464
|
print(f"Loading constraints: {args.constraints}")
|
|
444
465
|
|
|
@@ -448,6 +469,9 @@ def cmd_sample(args: argparse.Namespace) -> int:
|
|
|
448
469
|
if args.verbose:
|
|
449
470
|
print(f" Constraints: {len(constraints)}")
|
|
450
471
|
print(f"Generating {args.total_samples:,} samples with strategy: {args.strategy}")
|
|
472
|
+
if args.include_surface_points:
|
|
473
|
+
mode = "area-weighted" if mesh else "vertex-based"
|
|
474
|
+
print(f" Including {args.surface_point_count:,} surface points ({mode})")
|
|
451
475
|
|
|
452
476
|
config = build_sampler_config(args)
|
|
453
477
|
sampler = SDFSampler(config=config)
|
|
@@ -458,14 +482,11 @@ def cmd_sample(args: argparse.Namespace) -> int:
|
|
|
458
482
|
total_samples=args.total_samples,
|
|
459
483
|
strategy=args.strategy,
|
|
460
484
|
seed=args.seed,
|
|
485
|
+
include_surface_points=args.include_surface_points,
|
|
486
|
+
surface_point_count=args.surface_point_count,
|
|
487
|
+
mesh=mesh,
|
|
461
488
|
)
|
|
462
489
|
|
|
463
|
-
# Include surface points if requested
|
|
464
|
-
if args.include_surface_points:
|
|
465
|
-
samples = _add_surface_points(
|
|
466
|
-
samples, xyz, normals, args.surface_point_count, args.verbose
|
|
467
|
-
)
|
|
468
|
-
|
|
469
490
|
if args.verbose:
|
|
470
491
|
print(f"Generated {len(samples)} samples")
|
|
471
492
|
|
|
@@ -476,7 +497,7 @@ def cmd_sample(args: argparse.Namespace) -> int:
|
|
|
476
497
|
|
|
477
498
|
def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
478
499
|
"""Run full pipeline: analyze + sample + export."""
|
|
479
|
-
from sdf_sampler import SDFAnalyzer, SDFSampler, load_point_cloud
|
|
500
|
+
from sdf_sampler import SDFAnalyzer, SDFSampler, load_mesh, load_point_cloud
|
|
480
501
|
|
|
481
502
|
if not args.input.exists():
|
|
482
503
|
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
|
@@ -497,6 +518,19 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
|
497
518
|
print(f" Points: {len(xyz):,}")
|
|
498
519
|
print(f" Normals: {'yes' if normals is not None else 'no'}")
|
|
499
520
|
|
|
521
|
+
# Load mesh for area-weighted surface sampling if provided
|
|
522
|
+
mesh = None
|
|
523
|
+
if args.mesh:
|
|
524
|
+
if args.verbose:
|
|
525
|
+
print(f"Loading mesh for area-weighted sampling: {args.mesh}")
|
|
526
|
+
try:
|
|
527
|
+
mesh = load_mesh(str(args.mesh))
|
|
528
|
+
if args.verbose:
|
|
529
|
+
print(f" Vertices: {len(mesh.vertices):,}, Faces: {len(mesh.faces):,}")
|
|
530
|
+
except Exception as e:
|
|
531
|
+
print(f"Error loading mesh: {e}", file=sys.stderr)
|
|
532
|
+
return 1
|
|
533
|
+
|
|
500
534
|
# Analyze
|
|
501
535
|
if args.verbose:
|
|
502
536
|
algos = args.algorithms or ["all"]
|
|
@@ -526,6 +560,9 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
|
526
560
|
# Sample
|
|
527
561
|
if args.verbose:
|
|
528
562
|
print(f"Generating {args.total_samples:,} samples with strategy: {args.strategy}")
|
|
563
|
+
if args.include_surface_points:
|
|
564
|
+
mode = "area-weighted" if mesh else "vertex-based"
|
|
565
|
+
print(f" Including {args.surface_point_count:,} surface points ({mode})")
|
|
529
566
|
|
|
530
567
|
config = build_sampler_config(args)
|
|
531
568
|
sampler = SDFSampler(config=config)
|
|
@@ -536,14 +573,11 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
|
536
573
|
total_samples=args.total_samples,
|
|
537
574
|
strategy=args.strategy,
|
|
538
575
|
seed=args.seed,
|
|
576
|
+
include_surface_points=args.include_surface_points,
|
|
577
|
+
surface_point_count=args.surface_point_count,
|
|
578
|
+
mesh=mesh,
|
|
539
579
|
)
|
|
540
580
|
|
|
541
|
-
# Include surface points if requested
|
|
542
|
-
if args.include_surface_points:
|
|
543
|
-
samples = _add_surface_points(
|
|
544
|
-
samples, xyz, normals, args.surface_point_count, args.verbose
|
|
545
|
-
)
|
|
546
|
-
|
|
547
581
|
if args.verbose:
|
|
548
582
|
print(f"Generated {len(samples)} samples")
|
|
549
583
|
|
|
@@ -553,52 +587,6 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
|
|
|
553
587
|
return 0
|
|
554
588
|
|
|
555
589
|
|
|
556
|
-
def _add_surface_points(
|
|
557
|
-
samples: list,
|
|
558
|
-
xyz: np.ndarray,
|
|
559
|
-
normals: np.ndarray | None,
|
|
560
|
-
count: int,
|
|
561
|
-
verbose: bool,
|
|
562
|
-
) -> list:
|
|
563
|
-
"""Add surface points to sample list."""
|
|
564
|
-
from sdf_sampler.models import TrainingSample
|
|
565
|
-
|
|
566
|
-
n_surface = min(count, len(xyz))
|
|
567
|
-
if n_surface <= 0:
|
|
568
|
-
return samples
|
|
569
|
-
|
|
570
|
-
# Subsample if needed
|
|
571
|
-
if n_surface < len(xyz):
|
|
572
|
-
indices = np.random.choice(len(xyz), n_surface, replace=False)
|
|
573
|
-
surface_xyz = xyz[indices]
|
|
574
|
-
surface_normals = normals[indices] if normals is not None else None
|
|
575
|
-
else:
|
|
576
|
-
surface_xyz = xyz
|
|
577
|
-
surface_normals = normals
|
|
578
|
-
|
|
579
|
-
if verbose:
|
|
580
|
-
print(f"Adding {len(surface_xyz):,} surface points (phi=0)")
|
|
581
|
-
|
|
582
|
-
for i in range(len(surface_xyz)):
|
|
583
|
-
sample = TrainingSample(
|
|
584
|
-
x=float(surface_xyz[i, 0]),
|
|
585
|
-
y=float(surface_xyz[i, 1]),
|
|
586
|
-
z=float(surface_xyz[i, 2]),
|
|
587
|
-
phi=0.0,
|
|
588
|
-
weight=1.0,
|
|
589
|
-
source="surface",
|
|
590
|
-
is_surface=True,
|
|
591
|
-
is_free=False,
|
|
592
|
-
)
|
|
593
|
-
if surface_normals is not None:
|
|
594
|
-
sample.nx = float(surface_normals[i, 0])
|
|
595
|
-
sample.ny = float(surface_normals[i, 1])
|
|
596
|
-
sample.nz = float(surface_normals[i, 2])
|
|
597
|
-
samples.append(sample)
|
|
598
|
-
|
|
599
|
-
return samples
|
|
600
|
-
|
|
601
|
-
|
|
602
590
|
def cmd_info(args: argparse.Namespace) -> int:
|
|
603
591
|
"""Show information about a file."""
|
|
604
592
|
if not args.input.exists():
|
sdf_sampler/io.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ABOUTME: I/O utilities for point cloud loading and sample export
|
|
2
2
|
# ABOUTME: Supports PLY, LAS/LAZ, CSV, NPZ, and Parquet formats
|
|
3
3
|
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -10,6 +11,79 @@ import pandas as pd
|
|
|
10
11
|
from sdf_sampler.models.samples import TrainingSample
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@dataclass
|
|
15
|
+
class Mesh:
|
|
16
|
+
"""Triangle mesh with vertices, faces, and optional normals.
|
|
17
|
+
|
|
18
|
+
Used for area-weighted surface sampling where we need face information.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
vertices: np.ndarray # (N, 3) vertex positions
|
|
22
|
+
faces: np.ndarray # (M, 3) triangle face indices
|
|
23
|
+
vertex_normals: np.ndarray | None = None # (N, 3) per-vertex normals
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_mesh(
|
|
27
|
+
path: str | Path,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
) -> Mesh:
|
|
30
|
+
"""Load mesh from file (preserves face information).
|
|
31
|
+
|
|
32
|
+
Use this instead of load_point_cloud() when you need area-weighted
|
|
33
|
+
surface sampling, which requires face information.
|
|
34
|
+
|
|
35
|
+
Supported formats:
|
|
36
|
+
- PLY (requires trimesh in [io] extras)
|
|
37
|
+
- OBJ (requires trimesh in [io] extras)
|
|
38
|
+
- STL (requires trimesh in [io] extras)
|
|
39
|
+
- OFF (requires trimesh in [io] extras)
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Path to mesh file
|
|
43
|
+
**kwargs: Additional arguments for trimesh loader
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Mesh object with vertices, faces, and optional normals
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> mesh = load_mesh("model.ply")
|
|
50
|
+
>>> print(f"Vertices: {len(mesh.vertices)}, Faces: {len(mesh.faces)}")
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
import trimesh
|
|
54
|
+
except ImportError as e:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
"trimesh is required for mesh loading. "
|
|
57
|
+
"Install with: pip install sdf-sampler[io]"
|
|
58
|
+
) from e
|
|
59
|
+
|
|
60
|
+
path = Path(path)
|
|
61
|
+
loaded = trimesh.load(path, **kwargs)
|
|
62
|
+
|
|
63
|
+
# Handle Scene objects (multiple meshes)
|
|
64
|
+
if isinstance(loaded, trimesh.Scene):
|
|
65
|
+
# Combine all meshes into one
|
|
66
|
+
meshes = [g for g in loaded.geometry.values() if isinstance(g, trimesh.Trimesh)]
|
|
67
|
+
if not meshes:
|
|
68
|
+
raise ValueError(f"No triangle meshes found in {path}")
|
|
69
|
+
loaded = trimesh.util.concatenate(meshes)
|
|
70
|
+
|
|
71
|
+
if not isinstance(loaded, trimesh.Trimesh):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"File {path} did not load as a triangle mesh. "
|
|
74
|
+
"Use load_point_cloud() for point cloud files."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
vertices = np.asarray(loaded.vertices)
|
|
78
|
+
faces = np.asarray(loaded.faces)
|
|
79
|
+
|
|
80
|
+
vertex_normals = None
|
|
81
|
+
if loaded.vertex_normals is not None and len(loaded.vertex_normals) == len(vertices):
|
|
82
|
+
vertex_normals = np.asarray(loaded.vertex_normals)
|
|
83
|
+
|
|
84
|
+
return Mesh(vertices=vertices, faces=faces, vertex_normals=vertex_normals)
|
|
85
|
+
|
|
86
|
+
|
|
13
87
|
def load_point_cloud(
|
|
14
88
|
path: str | Path,
|
|
15
89
|
**kwargs: Any,
|
|
@@ -37,8 +111,8 @@ def load_point_cloud(
|
|
|
37
111
|
path = Path(path)
|
|
38
112
|
suffix = path.suffix.lower()
|
|
39
113
|
|
|
40
|
-
if suffix
|
|
41
|
-
return
|
|
114
|
+
if suffix in (".ply", ".obj", ".stl", ".off"):
|
|
115
|
+
return _load_mesh_vertices(path, **kwargs)
|
|
42
116
|
elif suffix in (".las", ".laz"):
|
|
43
117
|
return _load_las(path, **kwargs)
|
|
44
118
|
elif suffix == ".csv":
|
|
@@ -76,27 +150,33 @@ def export_parquet(
|
|
|
76
150
|
return path
|
|
77
151
|
|
|
78
152
|
|
|
79
|
-
def
|
|
80
|
-
"""Load
|
|
153
|
+
def _load_mesh_vertices(path: Path, **kwargs: Any) -> tuple[np.ndarray, np.ndarray | None]:
|
|
154
|
+
"""Load mesh file using trimesh and return vertices."""
|
|
81
155
|
try:
|
|
82
156
|
import trimesh
|
|
83
157
|
except ImportError as e:
|
|
84
158
|
raise ImportError(
|
|
85
|
-
"trimesh is required for
|
|
159
|
+
"trimesh is required for mesh file support. "
|
|
86
160
|
"Install with: pip install sdf-sampler[io]"
|
|
87
161
|
) from e
|
|
88
162
|
|
|
89
|
-
|
|
163
|
+
loaded = trimesh.load(path, **kwargs)
|
|
164
|
+
|
|
165
|
+
# Handle Scene objects (multiple meshes)
|
|
166
|
+
if isinstance(loaded, trimesh.Scene):
|
|
167
|
+
meshes = [g for g in loaded.geometry.values() if isinstance(g, trimesh.Trimesh)]
|
|
168
|
+
if meshes:
|
|
169
|
+
loaded = trimesh.util.concatenate(meshes)
|
|
90
170
|
|
|
91
171
|
# Handle both PointCloud and Trimesh objects
|
|
92
|
-
if hasattr(
|
|
93
|
-
xyz = np.asarray(
|
|
172
|
+
if hasattr(loaded, "vertices"):
|
|
173
|
+
xyz = np.asarray(loaded.vertices)
|
|
94
174
|
else:
|
|
95
|
-
xyz = np.asarray(
|
|
175
|
+
xyz = np.asarray(loaded.points if hasattr(loaded, "points") else loaded)
|
|
96
176
|
|
|
97
177
|
normals = None
|
|
98
|
-
if hasattr(
|
|
99
|
-
normals = np.asarray(
|
|
178
|
+
if hasattr(loaded, "vertex_normals") and loaded.vertex_normals is not None:
|
|
179
|
+
normals = np.asarray(loaded.vertex_normals)
|
|
100
180
|
if normals.shape != xyz.shape:
|
|
101
181
|
normals = None
|
|
102
182
|
|
sdf_sampler/sampler.py
CHANGED
|
@@ -67,6 +67,7 @@ class SDFSampler:
|
|
|
67
67
|
seed: int | None = None,
|
|
68
68
|
include_surface_points: bool = False,
|
|
69
69
|
surface_point_count: int | None = None,
|
|
70
|
+
mesh: Any = None,
|
|
70
71
|
) -> list[TrainingSample]:
|
|
71
72
|
"""Generate training samples from constraints.
|
|
72
73
|
|
|
@@ -79,6 +80,8 @@ class SDFSampler:
|
|
|
79
80
|
seed: Random seed for reproducibility
|
|
80
81
|
include_surface_points: If True, include original surface points with phi=0
|
|
81
82
|
surface_point_count: Number of surface points to include (default: 1000, or len(xyz) if smaller)
|
|
83
|
+
mesh: Optional Mesh object for area-weighted surface sampling. If provided,
|
|
84
|
+
surface points are sampled uniformly by surface area instead of by vertex.
|
|
82
85
|
|
|
83
86
|
Returns:
|
|
84
87
|
List of TrainingSample objects
|
|
@@ -163,7 +166,7 @@ class SDFSampler:
|
|
|
163
166
|
# Default to 1000 surface points, or all points if smaller
|
|
164
167
|
count = surface_point_count if surface_point_count is not None else min(1000, len(xyz))
|
|
165
168
|
samples.extend(
|
|
166
|
-
self._generate_surface_points(xyz, normals, count, rng)
|
|
169
|
+
self._generate_surface_points(xyz, normals, count, rng, mesh)
|
|
167
170
|
)
|
|
168
171
|
|
|
169
172
|
return samples
|
|
@@ -174,6 +177,7 @@ class SDFSampler:
|
|
|
174
177
|
normals: np.ndarray | None,
|
|
175
178
|
count: int,
|
|
176
179
|
rng: np.random.Generator,
|
|
180
|
+
mesh: Any = None,
|
|
177
181
|
) -> list[TrainingSample]:
|
|
178
182
|
"""Generate surface point samples (phi=0) from the input point cloud.
|
|
179
183
|
|
|
@@ -182,10 +186,19 @@ class SDFSampler:
|
|
|
182
186
|
normals: Optional point normals (N, 3)
|
|
183
187
|
count: Number of surface points to include
|
|
184
188
|
rng: Random number generator
|
|
189
|
+
mesh: Optional Mesh object for area-weighted sampling
|
|
185
190
|
|
|
186
191
|
Returns:
|
|
187
192
|
List of TrainingSample objects with phi=0
|
|
188
193
|
"""
|
|
194
|
+
if count <= 0:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# Use area-weighted sampling if mesh is provided
|
|
198
|
+
if mesh is not None:
|
|
199
|
+
return self._generate_surface_points_area_weighted(mesh, count, rng)
|
|
200
|
+
|
|
201
|
+
# Fallback to vertex-based sampling
|
|
189
202
|
n_surface = min(count, len(xyz))
|
|
190
203
|
if n_surface <= 0:
|
|
191
204
|
return []
|
|
@@ -219,6 +232,107 @@ class SDFSampler:
|
|
|
219
232
|
|
|
220
233
|
return samples
|
|
221
234
|
|
|
235
|
+
def _generate_surface_points_area_weighted(
|
|
236
|
+
self,
|
|
237
|
+
mesh: Any,
|
|
238
|
+
count: int,
|
|
239
|
+
rng: np.random.Generator,
|
|
240
|
+
) -> list[TrainingSample]:
|
|
241
|
+
"""Generate surface points using area-weighted sampling.
|
|
242
|
+
|
|
243
|
+
Samples points uniformly by surface area, not by vertex count.
|
|
244
|
+
This ensures uniform coverage even when vertex density varies.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
mesh: Mesh object with vertices, faces, and optional vertex_normals
|
|
248
|
+
count: Number of surface points to generate
|
|
249
|
+
rng: Random number generator
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of TrainingSample objects with phi=0
|
|
253
|
+
"""
|
|
254
|
+
vertices = mesh.vertices
|
|
255
|
+
faces = mesh.faces
|
|
256
|
+
|
|
257
|
+
# Compute face areas
|
|
258
|
+
v0 = vertices[faces[:, 0]]
|
|
259
|
+
v1 = vertices[faces[:, 1]]
|
|
260
|
+
v2 = vertices[faces[:, 2]]
|
|
261
|
+
face_areas = 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0), axis=1)
|
|
262
|
+
|
|
263
|
+
# Sample faces proportional to their area
|
|
264
|
+
total_area = face_areas.sum()
|
|
265
|
+
if total_area <= 0:
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
face_probs = face_areas / total_area
|
|
269
|
+
sampled_faces = rng.choice(len(faces), size=count, p=face_probs)
|
|
270
|
+
|
|
271
|
+
# Sample random point within each selected face using barycentric coordinates
|
|
272
|
+
# Generate random barycentric coordinates
|
|
273
|
+
r1 = rng.random(count)
|
|
274
|
+
r2 = rng.random(count)
|
|
275
|
+
# Ensure uniform distribution within triangle
|
|
276
|
+
sqrt_r1 = np.sqrt(r1)
|
|
277
|
+
u = 1 - sqrt_r1
|
|
278
|
+
v = sqrt_r1 * (1 - r2)
|
|
279
|
+
w = sqrt_r1 * r2
|
|
280
|
+
|
|
281
|
+
# Get vertices for sampled faces
|
|
282
|
+
f_v0 = vertices[faces[sampled_faces, 0]]
|
|
283
|
+
f_v1 = vertices[faces[sampled_faces, 1]]
|
|
284
|
+
f_v2 = vertices[faces[sampled_faces, 2]]
|
|
285
|
+
|
|
286
|
+
# Compute sample positions
|
|
287
|
+
surface_xyz = (
|
|
288
|
+
u[:, np.newaxis] * f_v0 +
|
|
289
|
+
v[:, np.newaxis] * f_v1 +
|
|
290
|
+
w[:, np.newaxis] * f_v2
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Compute normals (interpolated from vertex normals if available, else face normals)
|
|
294
|
+
if mesh.vertex_normals is not None:
|
|
295
|
+
n0 = mesh.vertex_normals[faces[sampled_faces, 0]]
|
|
296
|
+
n1 = mesh.vertex_normals[faces[sampled_faces, 1]]
|
|
297
|
+
n2 = mesh.vertex_normals[faces[sampled_faces, 2]]
|
|
298
|
+
surface_normals = (
|
|
299
|
+
u[:, np.newaxis] * n0 +
|
|
300
|
+
v[:, np.newaxis] * n1 +
|
|
301
|
+
w[:, np.newaxis] * n2
|
|
302
|
+
)
|
|
303
|
+
# Normalize
|
|
304
|
+
norms = np.linalg.norm(surface_normals, axis=1, keepdims=True)
|
|
305
|
+
norms = np.where(norms > 0, norms, 1)
|
|
306
|
+
surface_normals = surface_normals / norms
|
|
307
|
+
else:
|
|
308
|
+
# Compute face normals
|
|
309
|
+
edge1 = f_v1 - f_v0
|
|
310
|
+
edge2 = f_v2 - f_v0
|
|
311
|
+
surface_normals = np.cross(edge1, edge2)
|
|
312
|
+
norms = np.linalg.norm(surface_normals, axis=1, keepdims=True)
|
|
313
|
+
norms = np.where(norms > 0, norms, 1)
|
|
314
|
+
surface_normals = surface_normals / norms
|
|
315
|
+
|
|
316
|
+
# Build samples
|
|
317
|
+
samples = []
|
|
318
|
+
for i in range(len(surface_xyz)):
|
|
319
|
+
sample = TrainingSample(
|
|
320
|
+
x=float(surface_xyz[i, 0]),
|
|
321
|
+
y=float(surface_xyz[i, 1]),
|
|
322
|
+
z=float(surface_xyz[i, 2]),
|
|
323
|
+
phi=0.0,
|
|
324
|
+
nx=float(surface_normals[i, 0]),
|
|
325
|
+
ny=float(surface_normals[i, 1]),
|
|
326
|
+
nz=float(surface_normals[i, 2]),
|
|
327
|
+
weight=1.0,
|
|
328
|
+
source="surface",
|
|
329
|
+
is_surface=True,
|
|
330
|
+
is_free=False,
|
|
331
|
+
)
|
|
332
|
+
samples.append(sample)
|
|
333
|
+
|
|
334
|
+
return samples
|
|
335
|
+
|
|
222
336
|
def to_dataframe(self, samples: list[TrainingSample]) -> pd.DataFrame:
|
|
223
337
|
"""Convert samples to pandas DataFrame.
|
|
224
338
|
|
sdf_sampler/sampling/box.py
CHANGED
|
@@ -80,17 +80,18 @@ def sample_box_inverse_square(
|
|
|
80
80
|
"""Generate samples from a box with inverse-square density distribution.
|
|
81
81
|
|
|
82
82
|
Samples more points near the surface (point cloud) and fewer far away.
|
|
83
|
+
Uses actual distance to surface for phi values (signed distance).
|
|
83
84
|
|
|
84
85
|
Args:
|
|
85
86
|
constraint: Box constraint to sample
|
|
86
87
|
rng: Random number generator
|
|
87
|
-
near_band: Near-band width for
|
|
88
|
+
near_band: Near-band width for density weighting (not phi assignment)
|
|
88
89
|
n_samples: Number of samples to generate
|
|
89
90
|
surface_tree: KDTree of surface points for distance computation
|
|
90
91
|
falloff: Falloff exponent (higher = faster falloff)
|
|
91
92
|
|
|
92
93
|
Returns:
|
|
93
|
-
List of TrainingSample objects
|
|
94
|
+
List of TrainingSample objects with phi = actual signed distance to surface
|
|
94
95
|
"""
|
|
95
96
|
samples = []
|
|
96
97
|
center = np.array(constraint.center)
|
|
@@ -109,8 +110,11 @@ def sample_box_inverse_square(
|
|
|
109
110
|
weight = (near_band / min_dist) ** falloff
|
|
110
111
|
|
|
111
112
|
if rng.random() < min(1.0, weight):
|
|
112
|
-
|
|
113
|
-
phi
|
|
113
|
+
# Use actual distance to surface for phi, with sign based on constraint type
|
|
114
|
+
# EMPTY regions have positive phi (outside surface)
|
|
115
|
+
# SOLID regions have negative phi (inside surface)
|
|
116
|
+
sign = 1.0 if constraint.sign == SignConvention.EMPTY else -1.0
|
|
117
|
+
phi = sign * float(dist_to_surface)
|
|
114
118
|
|
|
115
119
|
samples.append(
|
|
116
120
|
TrainingSample(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdf-sampler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
sdf_sampler/__init__.py,sha256=
|
|
1
|
+
sdf_sampler/__init__.py,sha256=V0cSSwx_MN5Q6Chmp1_rXKUzul1mPVBLdV3h7nQC24E,1937
|
|
2
2
|
sdf_sampler/__main__.py,sha256=6N7jJs2Efs1AmM7qXXXN9V-ErFTWpPqKzWTeiujtGhU,490
|
|
3
3
|
sdf_sampler/analyzer.py,sha256=bF0jiqJg0tc8H3c6O1f7Ac6tYxsHsT8Y0ShyoRcZnGg,11702
|
|
4
|
-
sdf_sampler/cli.py,sha256=
|
|
4
|
+
sdf_sampler/cli.py,sha256=wb49roSnPfhX6z9RB8hE88RupJ44Fj7cMDd3gvWmnfQ,20418
|
|
5
5
|
sdf_sampler/config.py,sha256=lrPM1ktFkv32RRtOz6R-ShUFlBZ2LvoAl1hRLThmgKw,5185
|
|
6
|
-
sdf_sampler/io.py,sha256=
|
|
7
|
-
sdf_sampler/sampler.py,sha256=
|
|
6
|
+
sdf_sampler/io.py,sha256=rGIf9nXGfv8o1AZ6QYm1pibWAraPTBmrEiOOi1cmDZc,7651
|
|
7
|
+
sdf_sampler/sampler.py,sha256=ycizKuy-fy5_y5te2sO-DGyeJF9PCiVNQ-9JgtsbqNs,22058
|
|
8
8
|
sdf_sampler/algorithms/__init__.py,sha256=pp7tSZ8q0zRXZ5S8D3tho7bJ62pmW8jceuuRXtiXIzU,777
|
|
9
9
|
sdf_sampler/algorithms/flood_fill.py,sha256=iWGPPtOPSs0Cg7pxUlXUKGloFNCr8PuCHguRNy3c56c,7042
|
|
10
10
|
sdf_sampler/algorithms/normal_idw.py,sha256=uX3MQDTDX0wVilwxDE9dFj9hm2xuBDHV-AZqspjz7sk,3270
|
|
@@ -17,12 +17,12 @@ sdf_sampler/models/analysis.py,sha256=xSzz1jmpV3mOSM4gIF3NR4dhPXPH6UlxIgxZSOke5y
|
|
|
17
17
|
sdf_sampler/models/constraints.py,sha256=k0v6-JwXmUkFXGycDpPG44amjoRo13xz1ggqdr9NVtE,7316
|
|
18
18
|
sdf_sampler/models/samples.py,sha256=0DD7Z8D70zJKRTq16pik9SgGiRDeXh56EqmBZfql_sk,1474
|
|
19
19
|
sdf_sampler/sampling/__init__.py,sha256=mbeU9DTHxR4R1D4WqjEzaYHq7bV1nSv-wywc3YoD_Lg,492
|
|
20
|
-
sdf_sampler/sampling/box.py,sha256=
|
|
20
|
+
sdf_sampler/sampling/box.py,sha256=_CNJ98tl09NegA1XOECEVZWpigwsrAx_nP6cUmNHYR4,4284
|
|
21
21
|
sdf_sampler/sampling/brush.py,sha256=CcAgOYLdYXMM3y_H4fIwyzRJ8PZivFxkUHP7d0ElpNM,1991
|
|
22
22
|
sdf_sampler/sampling/ray_carve.py,sha256=EsfzEGk33q0iWVzOJKDAJi2iWEsY-JZXmEfEZ0dmNdg,4444
|
|
23
23
|
sdf_sampler/sampling/sphere.py,sha256=Xqpwq-RcEnAD6HhoyIC-ErxRHDknDKMtYf6aWUJ43_U,1680
|
|
24
|
-
sdf_sampler-0.
|
|
25
|
-
sdf_sampler-0.
|
|
26
|
-
sdf_sampler-0.
|
|
27
|
-
sdf_sampler-0.
|
|
28
|
-
sdf_sampler-0.
|
|
24
|
+
sdf_sampler-0.6.0.dist-info/METADATA,sha256=Gi2B0UQmqjula9BBpah3C2WMAjrOk20mcP_EUss_rp0,10481
|
|
25
|
+
sdf_sampler-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
sdf_sampler-0.6.0.dist-info/entry_points.txt,sha256=2IMWFbDYEqVUkpiRF1BlRMOhipeirPJSbv5PIOIZrvA,53
|
|
27
|
+
sdf_sampler-0.6.0.dist-info/licenses/LICENSE,sha256=eeB8aLnEG-dgFYs2KqfMJaP52GFQT8sZPHwaYnHRW8E,1061
|
|
28
|
+
sdf_sampler-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|