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,339 @@
|
|
|
1
|
+
# ABOUTME: Voxel grid construction utilities
|
|
2
|
+
# ABOUTME: Shared voxelization and hull computation for analysis algorithms
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.ndimage import binary_dilation
|
|
6
|
+
from scipy.spatial import ConvexHull, Delaunay, KDTree
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def estimate_mean_spacing(xyz: np.ndarray, tree: KDTree | None = None, k: int = 8) -> float:
|
|
10
|
+
"""Estimate mean point spacing using k-NN.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
xyz: Point positions (N, 3)
|
|
14
|
+
tree: Optional pre-built KDTree
|
|
15
|
+
k: Number of neighbors to use
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Mean distance to k nearest neighbors
|
|
19
|
+
"""
|
|
20
|
+
if tree is None:
|
|
21
|
+
tree = KDTree(xyz)
|
|
22
|
+
|
|
23
|
+
n_sample = min(1000, len(xyz))
|
|
24
|
+
rng = np.random.default_rng(42)
|
|
25
|
+
sample_indices = rng.choice(len(xyz), n_sample, replace=False)
|
|
26
|
+
|
|
27
|
+
distances = []
|
|
28
|
+
for idx in sample_indices:
|
|
29
|
+
dists, _ = tree.query(xyz[idx], k=k + 1) # +1 for self
|
|
30
|
+
distances.extend(dists[1:]) # Exclude self (distance 0)
|
|
31
|
+
|
|
32
|
+
return float(np.mean(distances))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_voxel_grid(
|
|
36
|
+
xyz: np.ndarray,
|
|
37
|
+
min_gap_size: float = 0.10,
|
|
38
|
+
max_dim: int = 200,
|
|
39
|
+
voxel_size: float | None = None,
|
|
40
|
+
z_extension: float | None = None,
|
|
41
|
+
) -> tuple[np.ndarray, np.ndarray, float, tuple[int, int, int]] | None:
|
|
42
|
+
"""Build a voxel grid from point cloud data.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
xyz: Point cloud coordinates (N, 3)
|
|
46
|
+
min_gap_size: Minimum gap size flood fill should traverse
|
|
47
|
+
max_dim: Maximum voxel grid dimension
|
|
48
|
+
voxel_size: Optional voxel size (auto-computed if None)
|
|
49
|
+
z_extension: How much to extend grid above point cloud in +Z
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (occupied_grid, bbox_min, voxel_size, grid_shape) or None if invalid.
|
|
53
|
+
"""
|
|
54
|
+
if len(xyz) < 10:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Determine voxel size based on point cloud density
|
|
58
|
+
tree = KDTree(xyz)
|
|
59
|
+
mean_spacing = estimate_mean_spacing(xyz, tree)
|
|
60
|
+
|
|
61
|
+
if voxel_size is None:
|
|
62
|
+
# Voxel size based on point density, constrained by min_gap_size
|
|
63
|
+
density_based = mean_spacing * 2.0
|
|
64
|
+
gap_based = min_gap_size / 3.0
|
|
65
|
+
voxel_size = min(density_based, gap_based)
|
|
66
|
+
|
|
67
|
+
voxel_size_float: float = float(voxel_size)
|
|
68
|
+
if voxel_size_float <= 0 or not np.isfinite(voxel_size_float):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
voxel_size = voxel_size_float
|
|
72
|
+
|
|
73
|
+
# Compute bounding box with padding
|
|
74
|
+
bbox_min = xyz.min(axis=0) - voxel_size
|
|
75
|
+
bbox_max = xyz.max(axis=0) + voxel_size
|
|
76
|
+
|
|
77
|
+
# Extend in +Z direction for sky space (outdoor scenes)
|
|
78
|
+
if z_extension is None:
|
|
79
|
+
z_range = xyz[:, 2].max() - xyz[:, 2].min()
|
|
80
|
+
z_extension = max(z_range * 0.5, voxel_size * 5)
|
|
81
|
+
bbox_max[2] += z_extension
|
|
82
|
+
bbox_min[2] -= voxel_size * 5 # Small extension for underground
|
|
83
|
+
bbox_size = bbox_max - bbox_min
|
|
84
|
+
|
|
85
|
+
if np.any(bbox_size <= 0) or not np.all(np.isfinite(bbox_size)):
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
grid_shape = np.ceil(bbox_size / voxel_size).astype(int)
|
|
89
|
+
|
|
90
|
+
if np.any(grid_shape <= 0):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Cap grid size for performance
|
|
94
|
+
vs: float = voxel_size
|
|
95
|
+
if grid_shape.max() > max_dim:
|
|
96
|
+
scale = float(max_dim / grid_shape.max())
|
|
97
|
+
vs = vs / scale
|
|
98
|
+
grid_shape = np.ceil(bbox_size / vs).astype(int)
|
|
99
|
+
grid_shape = np.minimum(grid_shape, max_dim)
|
|
100
|
+
|
|
101
|
+
# Mark occupied voxels
|
|
102
|
+
point_voxel_indices = ((xyz - bbox_min) / vs).astype(int)
|
|
103
|
+
point_voxel_indices = np.clip(point_voxel_indices, 0, grid_shape - 1)
|
|
104
|
+
|
|
105
|
+
occupied = np.zeros(tuple(grid_shape), dtype=bool)
|
|
106
|
+
for idx in point_voxel_indices:
|
|
107
|
+
occupied[tuple(idx)] = True
|
|
108
|
+
|
|
109
|
+
# Dilate to ensure surface blocks flood fill
|
|
110
|
+
structure = np.ones((3, 3, 3), dtype=bool)
|
|
111
|
+
occupied = binary_dilation(occupied, structure, iterations=1)
|
|
112
|
+
|
|
113
|
+
shape_tuple: tuple[int, int, int] = (
|
|
114
|
+
int(grid_shape[0]),
|
|
115
|
+
int(grid_shape[1]),
|
|
116
|
+
int(grid_shape[2]),
|
|
117
|
+
)
|
|
118
|
+
return occupied, bbox_min, vs, shape_tuple
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def compute_hull_mask(
|
|
122
|
+
xyz: np.ndarray,
|
|
123
|
+
bbox_min: np.ndarray,
|
|
124
|
+
voxel_size: float,
|
|
125
|
+
grid_shape: tuple[int, int, int],
|
|
126
|
+
) -> np.ndarray:
|
|
127
|
+
"""Compute a 2D mask of which XY voxel positions are inside the convex hull.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
xyz: Point cloud coordinates
|
|
131
|
+
bbox_min: Bounding box minimum
|
|
132
|
+
voxel_size: Size of each voxel
|
|
133
|
+
grid_shape: Shape of the voxel grid
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
2D boolean array (nx, ny) where True = inside the XY convex hull.
|
|
137
|
+
"""
|
|
138
|
+
nx, ny, _nz = grid_shape
|
|
139
|
+
|
|
140
|
+
xy_points = xyz[:, :2]
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
hull = ConvexHull(xy_points)
|
|
144
|
+
hull_delaunay = Delaunay(xy_points[hull.vertices])
|
|
145
|
+
except Exception:
|
|
146
|
+
return np.ones((nx, ny), dtype=bool)
|
|
147
|
+
|
|
148
|
+
inside_hull = np.zeros((nx, ny), dtype=bool)
|
|
149
|
+
for ix in range(nx):
|
|
150
|
+
for iy in range(ny):
|
|
151
|
+
world_x = bbox_min[0] + (ix + 0.5) * voxel_size
|
|
152
|
+
world_y = bbox_min[1] + (iy + 0.5) * voxel_size
|
|
153
|
+
inside_hull[ix, iy] = hull_delaunay.find_simplex([world_x, world_y]) >= 0
|
|
154
|
+
|
|
155
|
+
return inside_hull
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ray_propagation_with_bounces(
|
|
159
|
+
occupied: np.ndarray,
|
|
160
|
+
grid_shape: tuple[int, int, int],
|
|
161
|
+
inside_hull: np.ndarray,
|
|
162
|
+
cone_angle_degrees: float,
|
|
163
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
164
|
+
"""Propagate EMPTY/SOLID using ray model with cone angles and flood fill.
|
|
165
|
+
|
|
166
|
+
Physical model:
|
|
167
|
+
1. EMPTY rays shine from +Z (sky) in a cone, then flood-fill from seeds
|
|
168
|
+
2. SOLID rays shine from -Z (underground) in a cone, then flood-fill
|
|
169
|
+
3. EMPTY has priority - SOLID flood-fill never overwrites EMPTY
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
occupied: Boolean grid of occupied voxels
|
|
173
|
+
grid_shape: Grid dimensions
|
|
174
|
+
inside_hull: 2D mask of XY positions inside hull
|
|
175
|
+
cone_angle_degrees: Half-angle of the cone
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (empty_mask, solid_mask) boolean arrays.
|
|
179
|
+
"""
|
|
180
|
+
from scipy.ndimage import label
|
|
181
|
+
|
|
182
|
+
nx, ny, nz = grid_shape
|
|
183
|
+
empty = np.zeros(grid_shape, dtype=bool)
|
|
184
|
+
solid = np.zeros(grid_shape, dtype=bool)
|
|
185
|
+
|
|
186
|
+
# Phase 1: Rays from multiple angles within cone
|
|
187
|
+
tan_angle = np.tan(np.radians(cone_angle_degrees))
|
|
188
|
+
diag = tan_angle * 0.707
|
|
189
|
+
|
|
190
|
+
ray_tilts = [
|
|
191
|
+
(0.0, 0.0),
|
|
192
|
+
(tan_angle, 0.0),
|
|
193
|
+
(-tan_angle, 0.0),
|
|
194
|
+
(0.0, tan_angle),
|
|
195
|
+
(0.0, -tan_angle),
|
|
196
|
+
(diag, diag),
|
|
197
|
+
(diag, -diag),
|
|
198
|
+
(-diag, diag),
|
|
199
|
+
(-diag, -diag),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# EMPTY rays from sky (top-down with cone)
|
|
203
|
+
for dx_rate, dy_rate in ray_tilts:
|
|
204
|
+
for start_ix in range(nx):
|
|
205
|
+
for start_iy in range(ny):
|
|
206
|
+
fx, fy = float(start_ix), float(start_iy)
|
|
207
|
+
for iz in range(nz - 1, -1, -1):
|
|
208
|
+
ix, iy = int(round(fx)), int(round(fy))
|
|
209
|
+
if ix < 0 or ix >= nx or iy < 0 or iy >= ny:
|
|
210
|
+
break
|
|
211
|
+
if occupied[ix, iy, iz]:
|
|
212
|
+
break
|
|
213
|
+
empty[ix, iy, iz] = True
|
|
214
|
+
fx += dx_rate
|
|
215
|
+
fy += dy_rate
|
|
216
|
+
|
|
217
|
+
# SOLID rays from underground (bottom-up with cone), only inside hull
|
|
218
|
+
for dx_rate, dy_rate in ray_tilts:
|
|
219
|
+
for start_ix in range(nx):
|
|
220
|
+
for start_iy in range(ny):
|
|
221
|
+
if not inside_hull[start_ix, start_iy]:
|
|
222
|
+
continue
|
|
223
|
+
fx, fy = float(start_ix), float(start_iy)
|
|
224
|
+
for iz in range(nz):
|
|
225
|
+
ix, iy = int(round(fx)), int(round(fy))
|
|
226
|
+
if ix < 0 or ix >= nx or iy < 0 or iy >= ny:
|
|
227
|
+
break
|
|
228
|
+
if occupied[ix, iy, iz]:
|
|
229
|
+
break
|
|
230
|
+
if not empty[ix, iy, iz]:
|
|
231
|
+
solid[ix, iy, iz] = True
|
|
232
|
+
fx += dx_rate
|
|
233
|
+
fy += dy_rate
|
|
234
|
+
|
|
235
|
+
# Phase 2: Full flood fill from seeds
|
|
236
|
+
directions = [
|
|
237
|
+
(1, 0, 0),
|
|
238
|
+
(-1, 0, 0),
|
|
239
|
+
(0, 1, 0),
|
|
240
|
+
(0, -1, 0),
|
|
241
|
+
(0, 0, 1),
|
|
242
|
+
(0, 0, -1),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
# Compute per-column floor
|
|
246
|
+
column_floor = np.full((nx, ny), -1, dtype=int)
|
|
247
|
+
for ix in range(nx):
|
|
248
|
+
for iy in range(ny):
|
|
249
|
+
occupied_z = np.where(occupied[ix, iy, :])[0]
|
|
250
|
+
if len(occupied_z) > 0:
|
|
251
|
+
column_floor[ix, iy] = occupied_z.min()
|
|
252
|
+
|
|
253
|
+
# Flood fill EMPTY
|
|
254
|
+
empty_stack = [tuple(coord) for coord in np.argwhere(empty)]
|
|
255
|
+
while empty_stack:
|
|
256
|
+
ix, iy, iz = empty_stack.pop()
|
|
257
|
+
for dx, dy, dz in directions:
|
|
258
|
+
nx_, ny_, nz_ = ix + dx, iy + dy, iz + dz
|
|
259
|
+
if 0 <= nx_ < nx and 0 <= ny_ < ny and 0 <= nz_ < nz:
|
|
260
|
+
floor_z = column_floor[nx_, ny_]
|
|
261
|
+
if floor_z >= 0 and nz_ < floor_z:
|
|
262
|
+
continue
|
|
263
|
+
if not occupied[nx_, ny_, nz_] and not empty[nx_, ny_, nz_]:
|
|
264
|
+
empty[nx_, ny_, nz_] = True
|
|
265
|
+
empty_stack.append((nx_, ny_, nz_))
|
|
266
|
+
|
|
267
|
+
# Filter EMPTY by sky connectivity
|
|
268
|
+
labeled_empty, num_components = label(empty)
|
|
269
|
+
if num_components > 0:
|
|
270
|
+
top_slice = labeled_empty[:, :, -1]
|
|
271
|
+
sky_labels = set(top_slice[top_slice > 0])
|
|
272
|
+
if sky_labels:
|
|
273
|
+
sky_connected = np.isin(labeled_empty, list(sky_labels))
|
|
274
|
+
empty = empty & sky_connected
|
|
275
|
+
|
|
276
|
+
# Remove small isolated EMPTY regions
|
|
277
|
+
labeled_empty, num_components = label(empty)
|
|
278
|
+
if num_components > 0:
|
|
279
|
+
component_sizes = np.bincount(labeled_empty.ravel())
|
|
280
|
+
min_component_voxels = max(10, (nx * ny) // 100)
|
|
281
|
+
large_enough = component_sizes >= min_component_voxels
|
|
282
|
+
large_enough[0] = False
|
|
283
|
+
keep_mask = large_enough[labeled_empty]
|
|
284
|
+
empty = empty & keep_mask
|
|
285
|
+
|
|
286
|
+
# Flood fill SOLID
|
|
287
|
+
solid_stack = [tuple(coord) for coord in np.argwhere(solid)]
|
|
288
|
+
while solid_stack:
|
|
289
|
+
ix, iy, iz = solid_stack.pop()
|
|
290
|
+
for dx, dy, dz in directions:
|
|
291
|
+
nx_, ny_, nz_ = ix + dx, iy + dy, iz + dz
|
|
292
|
+
if 0 <= nx_ < nx and 0 <= ny_ < ny and 0 <= nz_ < nz:
|
|
293
|
+
if (
|
|
294
|
+
not occupied[nx_, ny_, nz_]
|
|
295
|
+
and not empty[nx_, ny_, nz_]
|
|
296
|
+
and not solid[nx_, ny_, nz_]
|
|
297
|
+
):
|
|
298
|
+
solid[nx_, ny_, nz_] = True
|
|
299
|
+
solid_stack.append((nx_, ny_, nz_))
|
|
300
|
+
|
|
301
|
+
return empty, solid
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def greedy_2d_mesh(mask_2d: np.ndarray) -> list[tuple[int, int, int, int]]:
|
|
305
|
+
"""Decompose a 2D boolean mask into axis-aligned rectangles.
|
|
306
|
+
|
|
307
|
+
Uses a greedy algorithm: find first True voxel, expand as far as
|
|
308
|
+
possible in X, then Y, record rectangle, clear voxels, repeat.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
mask_2d: 2D boolean array to decompose
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of (x_min, x_max, y_min, y_max) rectangles (exclusive max).
|
|
315
|
+
"""
|
|
316
|
+
mask = mask_2d.copy()
|
|
317
|
+
boxes: list[tuple[int, int, int, int]] = []
|
|
318
|
+
|
|
319
|
+
while mask.any():
|
|
320
|
+
coords = np.argwhere(mask)
|
|
321
|
+
if len(coords) == 0:
|
|
322
|
+
break
|
|
323
|
+
x, y = coords[0]
|
|
324
|
+
|
|
325
|
+
x_max = x
|
|
326
|
+
while x_max + 1 < mask.shape[0] and mask[x_max + 1, y]:
|
|
327
|
+
x_max += 1
|
|
328
|
+
|
|
329
|
+
y_max = y
|
|
330
|
+
while y_max + 1 < mask.shape[1]:
|
|
331
|
+
if mask[x : x_max + 1, y_max + 1].all():
|
|
332
|
+
y_max += 1
|
|
333
|
+
else:
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
boxes.append((x, x_max + 1, y, y_max + 1))
|
|
337
|
+
mask[x : x_max + 1, y : y_max + 1] = False
|
|
338
|
+
|
|
339
|
+
return boxes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ABOUTME: Voxel region algorithm for SOLID region detection
|
|
2
|
+
# ABOUTME: Uses ray propagation from underground to identify solid material
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from sdf_sampler.algorithms.flood_fill import (
|
|
7
|
+
_generate_boxes_from_mask,
|
|
8
|
+
_generate_samples_from_mask,
|
|
9
|
+
)
|
|
10
|
+
from sdf_sampler.algorithms.voxel_grid import (
|
|
11
|
+
build_voxel_grid,
|
|
12
|
+
compute_hull_mask,
|
|
13
|
+
ray_propagation_with_bounces,
|
|
14
|
+
)
|
|
15
|
+
from sdf_sampler.config import AutoAnalysisOptions
|
|
16
|
+
from sdf_sampler.models.analysis import AlgorithmType, GeneratedConstraint
|
|
17
|
+
from sdf_sampler.models.constraints import SignConvention
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_voxel_region_constraints(
|
|
21
|
+
xyz: np.ndarray,
|
|
22
|
+
normals: np.ndarray | None,
|
|
23
|
+
options: AutoAnalysisOptions,
|
|
24
|
+
) -> list[GeneratedConstraint]:
|
|
25
|
+
"""Generate SOLID constraints for underground regions.
|
|
26
|
+
|
|
27
|
+
Uses directional Z-ray propagation: SOLID propagates up from Z_min
|
|
28
|
+
until hitting the surface. Only voxels inside the 2D convex hull
|
|
29
|
+
are marked SOLID.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
xyz: Point cloud positions (N, 3)
|
|
33
|
+
normals: Point normals (N, 3) or None
|
|
34
|
+
options: Algorithm options
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of GeneratedConstraint objects
|
|
38
|
+
"""
|
|
39
|
+
constraints: list[GeneratedConstraint] = []
|
|
40
|
+
|
|
41
|
+
grid_result = build_voxel_grid(xyz, options.min_gap_size, options.max_grid_dim)
|
|
42
|
+
if grid_result is None:
|
|
43
|
+
return constraints
|
|
44
|
+
|
|
45
|
+
occupied, bbox_min, voxel_size, grid_shape = grid_result
|
|
46
|
+
_nx, _ny, nz = grid_shape
|
|
47
|
+
|
|
48
|
+
inside_hull = compute_hull_mask(xyz, bbox_min, voxel_size, grid_shape)
|
|
49
|
+
|
|
50
|
+
_, solid_mask = ray_propagation_with_bounces(
|
|
51
|
+
occupied, grid_shape, inside_hull, options.cone_angle
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
output_mode = options.voxel_regions_output.lower()
|
|
55
|
+
|
|
56
|
+
if output_mode in ("samples", "both"):
|
|
57
|
+
sample_constraints = _generate_samples_from_mask(
|
|
58
|
+
solid_mask,
|
|
59
|
+
bbox_min,
|
|
60
|
+
voxel_size,
|
|
61
|
+
xyz,
|
|
62
|
+
options.voxel_regions_sample_count,
|
|
63
|
+
SignConvention.SOLID,
|
|
64
|
+
AlgorithmType.VOXEL_REGIONS,
|
|
65
|
+
)
|
|
66
|
+
constraints.extend(sample_constraints)
|
|
67
|
+
|
|
68
|
+
if output_mode in ("boxes", "both"):
|
|
69
|
+
box_constraints = _generate_boxes_from_mask(
|
|
70
|
+
solid_mask,
|
|
71
|
+
bbox_min,
|
|
72
|
+
voxel_size,
|
|
73
|
+
nz,
|
|
74
|
+
options,
|
|
75
|
+
SignConvention.SOLID,
|
|
76
|
+
AlgorithmType.VOXEL_REGIONS,
|
|
77
|
+
)
|
|
78
|
+
constraints.extend(box_constraints)
|
|
79
|
+
|
|
80
|
+
return constraints
|