DeepSDFStruct 1.7.2__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.
- DeepSDFStruct/SDF.py +1734 -0
- DeepSDFStruct/__init__.py +66 -0
- DeepSDFStruct/deep_sdf/__init__.py +69 -0
- DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
- DeepSDFStruct/deep_sdf/data.py +258 -0
- DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
- DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
- DeepSDFStruct/deep_sdf/models.py +197 -0
- DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
- DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
- DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
- DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
- DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
- DeepSDFStruct/deep_sdf/plotting.py +143 -0
- DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
- DeepSDFStruct/deep_sdf/training.py +840 -0
- DeepSDFStruct/deep_sdf/workspace.py +363 -0
- DeepSDFStruct/design_of_experiments.py +235 -0
- DeepSDFStruct/flexicubes/__init__.py +15 -0
- DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
- DeepSDFStruct/flexicubes/tables.py +2338 -0
- DeepSDFStruct/flexisquares/__init__.py +15 -0
- DeepSDFStruct/flexisquares/flexisquares.py +967 -0
- DeepSDFStruct/flexisquares/tables.py +63 -0
- DeepSDFStruct/lattice_structure.py +254 -0
- DeepSDFStruct/local_shapes.py +226 -0
- DeepSDFStruct/mesh.py +1184 -0
- DeepSDFStruct/optimization.py +357 -0
- DeepSDFStruct/parametrization.py +215 -0
- DeepSDFStruct/pretrained_models.py +128 -0
- DeepSDFStruct/sampling.py +670 -0
- DeepSDFStruct/sdf_operations.py +990 -0
- DeepSDFStruct/sdf_primitives.py +1416 -0
- DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
- DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
- DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
- DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
- DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
- DeepSDFStruct/torch_spline.py +446 -0
- DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
- DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/specs.json +46 -0
- DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
- DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
- DeepSDFStruct/utils.py +96 -0
- DeepSDFStruct/visualization/latent_viewer.py +211 -0
- DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
- benchmarks/generate_sdf_showcase.py +622 -0
- benchmarks/sdf_from_mesh_benchmark.py +52 -0
- benchmarks/sdf_from_mesh_comparison.md +33 -0
- benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
- deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
- deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
- deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
- deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
- deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
- deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
- deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
- deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
- docs/Makefile +20 -0
- docs/api_reference.rst +6 -0
- docs/conf.py +60 -0
- docs/index.rst +136 -0
- docs/make.bat +35 -0
- docs/qr_code_github.png +0 -0
- docs/qr_code_paper.png +0 -0
- docs/readme_images/example_output_01.png +0 -0
- docs/readme_images/example_output_02.png +0 -0
- docs/readme_images/example_output_03.png +0 -0
- docs/readme_images/example_output_04.png +0 -0
- tests/data/chairs/1005.obj +282 -0
- tests/data/chairs/1024.obj +222 -0
- tests/data/chairs/README.md +1 -0
- tests/data/circular_balls_test_case.stl +0 -0
- tests/data/cone.stl +1794 -0
- tests/data/example_disconnectd_mesh.inp +1784 -0
- tests/data/example_line_mesh.vtk +0 -0
- tests/data/stanford_bunny.stl +0 -0
- tests/data/sweep_test_case.stl +0 -0
- tests/test_DOE.py +49 -0
- tests/test_elongate_sdf.py +195 -0
- tests/test_flexisquares.py +187 -0
- tests/test_generate_dataset.py +76 -0
- tests/test_lattice_evaluation.py +137 -0
- tests/test_mesh_export.py +137 -0
- tests/test_mesh_functions.py +49 -0
- tests/test_networks.py +71 -0
- tests/test_pretrained_models.py +40 -0
- tests/test_reconstruction.py +101 -0
- tests/test_sdf_from_mesh.py +52 -0
- tests/test_sdf_functions.py +117 -0
- tests/test_sdf_primitives.py +668 -0
- tests/test_sdf_trimesh_comparison.py +482 -0
- tests/test_splinepy_unitcells.py +64 -0
- tests/test_structural_optimization.py +178 -0
- tests/test_torch_spline.py +216 -0
- tests/test_train_model.py +110 -0
- tests/test_training_data_ids.py +41 -0
- tests/tmp_outputs/temp_file.txt +0 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlexiSquares Implementation
|
|
3
|
+
===========================
|
|
4
|
+
|
|
5
|
+
This module contains the core implementation of the FlexiSquares algorithm,
|
|
6
|
+
a differentiable 2D mesh extraction method adapted from FlexiCubes.
|
|
7
|
+
|
|
8
|
+
FlexiSquares improves upon traditional Marching Squares by allowing gradient-
|
|
9
|
+
based optimization of the extracted contours, making it suitable for inverse
|
|
10
|
+
design and shape optimization problems.
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
|
|
13
|
+
Licensed under the Apache License, Version 2.0.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
|
|
17
|
+
# All rights reserved.
|
|
18
|
+
#
|
|
19
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
20
|
+
# you may not use this file except in compliance with the License.
|
|
21
|
+
# You may obtain a copy of the License at
|
|
22
|
+
#
|
|
23
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
24
|
+
#
|
|
25
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
26
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
27
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
28
|
+
# See the License for the specific language governing permissions and
|
|
29
|
+
# limitations under the License.
|
|
30
|
+
import torch
|
|
31
|
+
import logging
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
import DeepSDFStruct
|
|
35
|
+
from DeepSDFStruct.flexisquares.tables import num_vd_table, dmc_table, tet_table
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(DeepSDFStruct.__name__)
|
|
38
|
+
|
|
39
|
+
__all__ = ["FlexiSquares"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FlexiSquares:
|
|
43
|
+
"""
|
|
44
|
+
Implements the 2D variant of the flexicubes method, adapted as
|
|
45
|
+
flexisquares, for extracting contour meshes from scalar fields.
|
|
46
|
+
This class uses lookup tables and indexed operations to perform
|
|
47
|
+
differentiable isocontour extraction from signed distance fields
|
|
48
|
+
defined on 2D grids.
|
|
49
|
+
|
|
50
|
+
Flexisquares is a differentiable variant of the Dual Marching
|
|
51
|
+
Squares (DMS) algorithm. It improves geometric fidelity by
|
|
52
|
+
optimizing surface representations using gradient-based methods.
|
|
53
|
+
|
|
54
|
+
During initialization, the class loads precomputed lookup tables
|
|
55
|
+
and converts them into PyTorch tensors on the specified device.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
device (str): Computational device, usually "cuda" or "cpu".
|
|
59
|
+
dmc_table (torch.Tensor): Dual Marching Squares (DMS) table
|
|
60
|
+
encoding the edges associated with each dual vertex in 16
|
|
61
|
+
possible Marching Squares configurations.
|
|
62
|
+
num_vd_table (torch.Tensor): Table holding the number of dual
|
|
63
|
+
vertices for each configuration.
|
|
64
|
+
tet_table (torch.Tensor): Lookup table used during triangle
|
|
65
|
+
generation inside the contour.
|
|
66
|
+
cube_corners (torch.Tensor): Positions of the four corners of
|
|
67
|
+
a unit square in 2D space.
|
|
68
|
+
cube_corners_idx (torch.Tensor): Corner indices encoded as
|
|
69
|
+
binary powers to compute case IDs for Marching Squares.
|
|
70
|
+
cube_edges (torch.Tensor): Edge connections in a square,
|
|
71
|
+
listed in pairs of corner indices.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, device="cuda", qef_reg_scale=1e-3, weight_scale=0.99):
|
|
75
|
+
|
|
76
|
+
self.device = device
|
|
77
|
+
self.dmc_table = torch.tensor(
|
|
78
|
+
dmc_table, dtype=torch.long, device=device, requires_grad=False
|
|
79
|
+
)
|
|
80
|
+
self.num_vd_table = torch.tensor(
|
|
81
|
+
num_vd_table, dtype=torch.long, device=device, requires_grad=False
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.tet_table = torch.tensor(
|
|
85
|
+
tet_table, dtype=torch.long, device=device, requires_grad=False
|
|
86
|
+
)
|
|
87
|
+
self.quad_split_1 = torch.tensor(
|
|
88
|
+
[0, 1, 2, 0, 2, 3], dtype=torch.long, device=device, requires_grad=False
|
|
89
|
+
)
|
|
90
|
+
self.quad_split_2 = torch.tensor(
|
|
91
|
+
[0, 1, 3, 3, 1, 2], dtype=torch.long, device=device, requires_grad=False
|
|
92
|
+
)
|
|
93
|
+
self.quad_split_train = torch.tensor(
|
|
94
|
+
[0, 1, 1, 2, 2, 3, 3, 0],
|
|
95
|
+
dtype=torch.long,
|
|
96
|
+
device=device,
|
|
97
|
+
requires_grad=False,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.cube_corners = torch.tensor(
|
|
101
|
+
[[0, 0], [1, 0], [0, 1], [1, 1]], device=device
|
|
102
|
+
)
|
|
103
|
+
# Each corner corresponds to a binary bit (for the 2^4 possible inside/outside cases):
|
|
104
|
+
self.cube_corners_idx = torch.pow(2, torch.arange(4, requires_grad=False))
|
|
105
|
+
|
|
106
|
+
# (0,1) ─ 2 ─ (1,1)
|
|
107
|
+
# | |
|
|
108
|
+
# 0 1
|
|
109
|
+
# | |
|
|
110
|
+
# (0,0) ─ 3 ─ (1,0)
|
|
111
|
+
|
|
112
|
+
# (2) ─ 2 ─ (3)
|
|
113
|
+
# | |
|
|
114
|
+
# 0 1
|
|
115
|
+
# | |
|
|
116
|
+
# (0) ─ 3 ─ (1)
|
|
117
|
+
|
|
118
|
+
# Edges are:
|
|
119
|
+
# e0: bottom → top along left side → (0,0)-(0,1) → corners (0,2)
|
|
120
|
+
# e1: bottom → top along right side → (1,0)-(1,1) → corners (1,3)
|
|
121
|
+
# e2: left → right along bottom → (0,0)-(1,0) → corners (0,1)
|
|
122
|
+
# e3: left → right along top → (0,1)-(1,1) → corners (2,3)
|
|
123
|
+
|
|
124
|
+
self.cube_edges = torch.tensor(
|
|
125
|
+
[0, 2, 1, 3, 2, 3, 0, 1], dtype=torch.long, device=device
|
|
126
|
+
)
|
|
127
|
+
# should be equivalently, reshaped as pairs:
|
|
128
|
+
# self.cube_edges = torch.tensor(
|
|
129
|
+
# [[0, 2], [1, 3], [0, 1], [2, 3]], dtype=torch.long, device=device
|
|
130
|
+
# )
|
|
131
|
+
|
|
132
|
+
self.edge_dir_table = torch.tensor(
|
|
133
|
+
[1, 1, 0, 0], dtype=torch.long, device=device
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.dir_faces_table = torch.tensor(
|
|
137
|
+
[[[0, 1], [2, 3]], [[0, 2], [1, 3]]], # x-dir edges # y-dir edges
|
|
138
|
+
dtype=torch.long,
|
|
139
|
+
device=device,
|
|
140
|
+
)
|
|
141
|
+
# not sure which version is correct
|
|
142
|
+
self.adj_pairs_2233 = torch.tensor(
|
|
143
|
+
[0, 1, 1, 2, 2, 3, 3, 0], dtype=torch.long, device=device
|
|
144
|
+
)
|
|
145
|
+
self.adj_pairs_3322 = torch.tensor(
|
|
146
|
+
[0, 1, 1, 3, 3, 2, 2, 0], dtype=torch.long, device=device
|
|
147
|
+
)
|
|
148
|
+
self.qef_reg_scale = qef_reg_scale
|
|
149
|
+
self.weight_scale = weight_scale
|
|
150
|
+
|
|
151
|
+
def construct_voxel_grid(
|
|
152
|
+
self, resolution, bounds=None
|
|
153
|
+
) -> tuple[torch.tensor, torch.tensor]:
|
|
154
|
+
"""
|
|
155
|
+
Generates a 2D grid of vertices and their square indices given
|
|
156
|
+
the specified resolution.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
resolution (int or list[int]): Resolution of the 2D grid.
|
|
160
|
+
If an integer is provided, it is applied to both x and
|
|
161
|
+
y dimensions. If a tuple/list of two integers is given,
|
|
162
|
+
they specify (x_res, y_res).
|
|
163
|
+
bounds (torch.Tensor, optional): 2×2 tensor defining the
|
|
164
|
+
[min, max] bounds in x and y.
|
|
165
|
+
Defaults to [[-0.05, -0.05], [1.05, 1.05]].
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
(torch.Tensor, torch.Tensor): Tuple containing:
|
|
169
|
+
- Vertices (N×2): Scaled vertex coordinates of the
|
|
170
|
+
2D grid.
|
|
171
|
+
- Squares (F×4): Indices into `vertices` defining each
|
|
172
|
+
square's corner connectivity.
|
|
173
|
+
"""
|
|
174
|
+
res = resolution
|
|
175
|
+
base_cube_f = torch.arange(4).to(self.device)
|
|
176
|
+
if isinstance(res, int):
|
|
177
|
+
res = (res, res)
|
|
178
|
+
voxel_grid_template = torch.ones(res, device=self.device)
|
|
179
|
+
|
|
180
|
+
res = torch.tensor([res], device=self.device)
|
|
181
|
+
coords = (
|
|
182
|
+
torch.nonzero(voxel_grid_template).to(torch.get_default_dtype()) / res
|
|
183
|
+
) # N, 2
|
|
184
|
+
verts = (self.cube_corners.unsqueeze(0) / res + coords.unsqueeze(1)).reshape(
|
|
185
|
+
-1, 2
|
|
186
|
+
)
|
|
187
|
+
cubes = (
|
|
188
|
+
base_cube_f.unsqueeze(0)
|
|
189
|
+
+ torch.arange(coords.shape[0], device=self.device).unsqueeze(1) * 4
|
|
190
|
+
).reshape(-1)
|
|
191
|
+
|
|
192
|
+
verts_rounded = torch.round(verts * 10**5) / (10**5)
|
|
193
|
+
verts_unique, inverse_indices = torch.unique(
|
|
194
|
+
verts_rounded, dim=0, return_inverse=True
|
|
195
|
+
)
|
|
196
|
+
cubes = inverse_indices[cubes.reshape(-1)].reshape(-1, 4)
|
|
197
|
+
|
|
198
|
+
if bounds is None:
|
|
199
|
+
bounds = torch.tensor(
|
|
200
|
+
[[-0.05, -0.05], [1.05, 1.05]],
|
|
201
|
+
device=verts_unique.device,
|
|
202
|
+
dtype=verts_unique.dtype,
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
bounds = torch.as_tensor(
|
|
206
|
+
bounds, device=verts_unique.device, dtype=verts_unique.dtype
|
|
207
|
+
)
|
|
208
|
+
assert bounds.shape == (2, 2), "bounds must have shape [2, 2]"
|
|
209
|
+
|
|
210
|
+
# Scale samples from [0, 1] to the given bounds
|
|
211
|
+
# samples = samples * 1.1 + _torch.tensor([0.5, 0.5, 0.5], device=device)
|
|
212
|
+
samples = verts_unique - 0.5
|
|
213
|
+
verts_scaled = (
|
|
214
|
+
bounds[0]
|
|
215
|
+
+ 0.5 * (bounds[1] - bounds[0])
|
|
216
|
+
+ (bounds[1] - bounds[0]) * samples
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
tolerance = 1e-6
|
|
220
|
+
torch._assert(
|
|
221
|
+
torch.all(
|
|
222
|
+
verts_scaled.ge(bounds[0] - tolerance)
|
|
223
|
+
& verts_scaled.le(bounds[1] + tolerance)
|
|
224
|
+
),
|
|
225
|
+
"Samples are out of specified bounds",
|
|
226
|
+
)
|
|
227
|
+
return verts_scaled, cubes
|
|
228
|
+
|
|
229
|
+
# changes mkofler: adapted function signature to match the kaolin package
|
|
230
|
+
# function signature on kaolin package:
|
|
231
|
+
# verts, faces, loss = DeepSDFStruct.flexicubes_reconstructor(voxelgrid_vertices=samples_orig[:, :3],
|
|
232
|
+
# scalar_field=sdf_values,
|
|
233
|
+
# cube_idx=cube_idx,
|
|
234
|
+
# resolution=tuple(N),
|
|
235
|
+
# output_tetmesh=output_tetmesh)
|
|
236
|
+
def __call__(
|
|
237
|
+
self,
|
|
238
|
+
voxelgrid_vertices,
|
|
239
|
+
scalar_field,
|
|
240
|
+
cube_idx,
|
|
241
|
+
beta_fx4=None,
|
|
242
|
+
alpha_fx4=None,
|
|
243
|
+
output_tetmesh=False,
|
|
244
|
+
resolution=None,
|
|
245
|
+
n_smoothing_iterations=5,
|
|
246
|
+
):
|
|
247
|
+
r"""
|
|
248
|
+
Extracts a differentiable 2D contour mesh from a scalar field using DeepSDFStruct.flexisquares.
|
|
249
|
+
|
|
250
|
+
Converts discrete signed distance fields on a 2D grid into polygonal contours using a
|
|
251
|
+
differentiable Dual Marching Squares process, as described in
|
|
252
|
+
*Flexible Isosurface Extraction for Gradient-Based Mesh Optimization* (adapted to 2D).
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
voxelgrid_vertices (torch.Tensor): 2D coordinates of grid vertices (N×2).
|
|
256
|
+
scalar_field (torch.Tensor): Signed distance field values at each vertex.
|
|
257
|
+
Negative values are inside the contour.
|
|
258
|
+
cube_idx (torch.LongTensor): Indices of 4 vertices per grid cell (F×4).
|
|
259
|
+
resolution (int or tuple[int, int]): Resolution of the grid.
|
|
260
|
+
beta_fx4 (torch.Tensor, optional): Edge weights adjusting dual vertex positioning.
|
|
261
|
+
alpha_fx4 (torch.Tensor, optional): Corner weights adjusting interpolation.
|
|
262
|
+
output_tetmesh (bool, optional): Retained for compatibility; in 2D, returns triangles only.
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
(torch.Tensor, torch.LongTensor, torch.Tensor): Tuple of:
|
|
267
|
+
- Vertices (V×2): Coordinates of contour vertices.
|
|
268
|
+
- Faces (F×3): Triangle indices of the contour mesh.
|
|
269
|
+
- Regularization loss (L_dev): Deviation loss per dual vertex.
|
|
270
|
+
|
|
271
|
+
.. _Flexible Isosurface Extraction for Gradient-Based Mesh Optimization:
|
|
272
|
+
https://research.nvidia.com/labs/toronto-ai/DeepSDFStruct.flexicubes/
|
|
273
|
+
.. _Manifold Dual Contouring:
|
|
274
|
+
https://people.engr.tamu.edu/schaefer/research/dualsimp_tvcg.pdf
|
|
275
|
+
"""
|
|
276
|
+
x_nx2 = voxelgrid_vertices
|
|
277
|
+
s_n = scalar_field
|
|
278
|
+
cube_fx4 = cube_idx
|
|
279
|
+
surf_cubes, occ_fx4 = self._identify_surf_cubes(s_n, cube_fx4)
|
|
280
|
+
if surf_cubes.sum() == 0:
|
|
281
|
+
return (
|
|
282
|
+
torch.zeros((0, 3), device=self.device),
|
|
283
|
+
(
|
|
284
|
+
torch.zeros((0, 4), dtype=torch.long, device=self.device)
|
|
285
|
+
if output_tetmesh
|
|
286
|
+
else torch.zeros((0, 3), dtype=torch.long, device=self.device)
|
|
287
|
+
),
|
|
288
|
+
torch.zeros((0), device=self.device),
|
|
289
|
+
)
|
|
290
|
+
beta_fx4, alpha_fx4 = self._normalize_weights(beta_fx4, alpha_fx4, surf_cubes)
|
|
291
|
+
|
|
292
|
+
case_ids = self._get_case_id(occ_fx4, surf_cubes)
|
|
293
|
+
|
|
294
|
+
surf_edges, idx_map, edge_counts, surf_edges_mask = self._identify_surf_edges(
|
|
295
|
+
s_n, cube_fx4, surf_cubes
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
vd, L_dev, vd_idx_map = self._compute_vd(
|
|
299
|
+
x_nx2, surf_edges, s_n, case_ids, beta_fx4, alpha_fx4, idx_map
|
|
300
|
+
)
|
|
301
|
+
boundary_vertices, boundary_lines, s_edges, edge_indices = (
|
|
302
|
+
self._extract_zero_contour(
|
|
303
|
+
s_n, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
if not output_tetmesh:
|
|
307
|
+
return boundary_vertices, boundary_lines, L_dev
|
|
308
|
+
else:
|
|
309
|
+
self.check_open_mesh(boundary_lines)
|
|
310
|
+
all_vertices, triangles = self._triangulate(
|
|
311
|
+
x_nx2,
|
|
312
|
+
s_n,
|
|
313
|
+
cube_fx4,
|
|
314
|
+
boundary_vertices,
|
|
315
|
+
boundary_lines,
|
|
316
|
+
surf_edges,
|
|
317
|
+
s_edges,
|
|
318
|
+
case_ids,
|
|
319
|
+
edge_indices,
|
|
320
|
+
surf_cubes,
|
|
321
|
+
)
|
|
322
|
+
vertices_smoothed = self._apply_laplacian_smoothing(
|
|
323
|
+
all_vertices,
|
|
324
|
+
triangles,
|
|
325
|
+
boundary_lines,
|
|
326
|
+
iterations=n_smoothing_iterations,
|
|
327
|
+
)
|
|
328
|
+
return vertices_smoothed, triangles, L_dev
|
|
329
|
+
|
|
330
|
+
def _apply_laplacian_smoothing(
|
|
331
|
+
self, vertices, triangles, boundary_vertices, iterations=5, lamb=0.5
|
|
332
|
+
):
|
|
333
|
+
"""
|
|
334
|
+
Vectorized Laplacian smoothing using PyTorch sparse matrices.
|
|
335
|
+
"""
|
|
336
|
+
logger.info(f"applying {iterations} iterations of laplacian smoothing")
|
|
337
|
+
num_total = vertices.shape[0]
|
|
338
|
+
device = vertices.device
|
|
339
|
+
|
|
340
|
+
unique_boundary = torch.unique(boundary_vertices.long())
|
|
341
|
+
is_interior = torch.ones(num_total, 1, device=device, dtype=torch.bool)
|
|
342
|
+
is_interior[unique_boundary] = False
|
|
343
|
+
|
|
344
|
+
i, j, k = triangles[:, 0], triangles[:, 1], triangles[:, 2]
|
|
345
|
+
|
|
346
|
+
edges = torch.cat(
|
|
347
|
+
[
|
|
348
|
+
torch.stack([i, j], dim=1),
|
|
349
|
+
torch.stack([j, i], dim=1),
|
|
350
|
+
torch.stack([j, k], dim=1),
|
|
351
|
+
torch.stack([k, j], dim=1),
|
|
352
|
+
torch.stack([k, i], dim=1),
|
|
353
|
+
torch.stack([i, k], dim=1),
|
|
354
|
+
],
|
|
355
|
+
dim=0,
|
|
356
|
+
) # shape: (num_edges, 2)
|
|
357
|
+
|
|
358
|
+
# Remove duplicate edges
|
|
359
|
+
edges = torch.unique(edges, dim=0)
|
|
360
|
+
|
|
361
|
+
src = edges[:, 0] # neighbor index
|
|
362
|
+
dst = edges[:, 1] # vertex to accumulate into
|
|
363
|
+
|
|
364
|
+
degree = torch.zeros(num_total, device=device, dtype=vertices.dtype)
|
|
365
|
+
one = torch.ones_like(dst, dtype=vertices.dtype)
|
|
366
|
+
degree.scatter_add_(0, dst, one)
|
|
367
|
+
degree_inv = 1.0 / torch.clamp(degree, min=1.0)
|
|
368
|
+
|
|
369
|
+
v_smoothed = vertices.clone()
|
|
370
|
+
|
|
371
|
+
for _ in range(iterations):
|
|
372
|
+
neighbor_sum = torch.zeros_like(v_smoothed) # (N, 3)
|
|
373
|
+
|
|
374
|
+
neighbor_sum.scatter_add_(
|
|
375
|
+
0, dst.unsqueeze(1).expand(-1, v_smoothed.shape[1]), v_smoothed[src]
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
neighbor_avg = neighbor_sum * degree_inv.unsqueeze(1)
|
|
379
|
+
|
|
380
|
+
v_update = (1 - lamb) * v_smoothed + lamb * neighbor_avg
|
|
381
|
+
v_smoothed = torch.where(is_interior, v_update, v_smoothed)
|
|
382
|
+
|
|
383
|
+
return v_smoothed
|
|
384
|
+
|
|
385
|
+
def _compute_reg_loss(self, vd, ue, edge_group_to_vd, vd_num_edges):
|
|
386
|
+
"""
|
|
387
|
+
Regularizer L_dev as in Equation 8
|
|
388
|
+
"""
|
|
389
|
+
dist = torch.norm(
|
|
390
|
+
ue - torch.index_select(input=vd, index=edge_group_to_vd, dim=0), dim=-1
|
|
391
|
+
)
|
|
392
|
+
mean_l2 = torch.zeros_like(vd[:, 0])
|
|
393
|
+
mean_l2 = (mean_l2).index_add_(
|
|
394
|
+
0, edge_group_to_vd, dist
|
|
395
|
+
) / vd_num_edges.squeeze(1).to(torch.get_default_dtype())
|
|
396
|
+
mad = (
|
|
397
|
+
dist - torch.index_select(input=mean_l2, index=edge_group_to_vd, dim=0)
|
|
398
|
+
).abs()
|
|
399
|
+
return mad
|
|
400
|
+
|
|
401
|
+
def _normalize_weights(self, beta_fx4, alpha_fx4, surf_cubes):
|
|
402
|
+
"""
|
|
403
|
+
Normalizes the given weights to be non-negative. If input weights are None, it creates and returns a set of weights of ones.
|
|
404
|
+
"""
|
|
405
|
+
n_cubes = surf_cubes.shape[0]
|
|
406
|
+
|
|
407
|
+
if beta_fx4 is not None:
|
|
408
|
+
beta_fx4 = torch.tanh(beta_fx4) * self.weight_scale + 1
|
|
409
|
+
else:
|
|
410
|
+
beta_fx4 = torch.ones((n_cubes, 4), device=self.device)
|
|
411
|
+
|
|
412
|
+
if alpha_fx4 is not None:
|
|
413
|
+
alpha_fx4 = torch.tanh(alpha_fx4) * self.weight_scale + 1
|
|
414
|
+
else:
|
|
415
|
+
alpha_fx4 = torch.ones((n_cubes, 4), device=self.device)
|
|
416
|
+
|
|
417
|
+
return beta_fx4[surf_cubes], alpha_fx4[surf_cubes]
|
|
418
|
+
|
|
419
|
+
@torch.no_grad()
|
|
420
|
+
def _get_case_id(self, occ_fx4, surf_cubes):
|
|
421
|
+
"""
|
|
422
|
+
Obtains the ID of topology cases based on cell corner occupancy. This function resolves the
|
|
423
|
+
ambiguity in the Dual Marching Cubes (DMC) configurations as described in Section 1.3 of the
|
|
424
|
+
supplementary material. It should be noted that this function assumes a regular grid.
|
|
425
|
+
"""
|
|
426
|
+
case_ids = (
|
|
427
|
+
occ_fx4[surf_cubes] * self.cube_corners_idx.to(self.device).unsqueeze(0)
|
|
428
|
+
).sum(-1)
|
|
429
|
+
|
|
430
|
+
return case_ids
|
|
431
|
+
|
|
432
|
+
@torch.no_grad()
|
|
433
|
+
def _identify_surf_edges(self, s_n, cube_fx4, surf_cubes):
|
|
434
|
+
"""
|
|
435
|
+
Finds edges of active squares that intersect the contour by checking for opposite signs
|
|
436
|
+
of the scalar field at edge endpoints.
|
|
437
|
+
Assigns unique indices to surface-intersecting edges and produces a mapping
|
|
438
|
+
from square edges to unique edge IDs.
|
|
439
|
+
"""
|
|
440
|
+
occ_n = s_n < 0
|
|
441
|
+
all_edges = cube_fx4[surf_cubes][:, self.cube_edges].reshape(-1, 2)
|
|
442
|
+
|
|
443
|
+
unique_edges, _idx_map, counts = torch.unique(
|
|
444
|
+
all_edges, dim=0, return_inverse=True, return_counts=True
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
unique_edges = unique_edges.long()
|
|
448
|
+
mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
|
|
449
|
+
|
|
450
|
+
surf_edges_mask = mask_edges[_idx_map]
|
|
451
|
+
counts = counts[_idx_map]
|
|
452
|
+
|
|
453
|
+
mapping = (
|
|
454
|
+
torch.ones(
|
|
455
|
+
(unique_edges.shape[0]), dtype=torch.long, device=cube_fx4.device
|
|
456
|
+
)
|
|
457
|
+
* -1
|
|
458
|
+
)
|
|
459
|
+
mapping[mask_edges] = torch.arange(mask_edges.sum(), device=cube_fx4.device)
|
|
460
|
+
# Shaped as [number of cubes x 4 edges per cube]. This is later used to map a cube edge to the unique index
|
|
461
|
+
# for a surface-intersecting edge. Non-surface-intersecting edges are marked with -1.
|
|
462
|
+
idx_map = mapping[_idx_map]
|
|
463
|
+
surf_edges = unique_edges[mask_edges]
|
|
464
|
+
return surf_edges, idx_map, counts, surf_edges_mask
|
|
465
|
+
|
|
466
|
+
@torch.no_grad()
|
|
467
|
+
def _identify_surf_cubes(self, s_n, cube_fx4):
|
|
468
|
+
"""
|
|
469
|
+
Identifies grid cubes that intersect with the underlying surface by checking if the signs at
|
|
470
|
+
all corners are not identical.
|
|
471
|
+
"""
|
|
472
|
+
occ_n = s_n < 0
|
|
473
|
+
occ_fx4 = occ_n[cube_fx4.reshape(-1)].reshape(-1, 4)
|
|
474
|
+
_occ_sum = torch.sum(occ_fx4, -1)
|
|
475
|
+
surf_cubes = (_occ_sum > 0) & (_occ_sum < 4)
|
|
476
|
+
return surf_cubes, occ_fx4
|
|
477
|
+
|
|
478
|
+
def _linear_interp(self, edges_weight, edges_x):
|
|
479
|
+
"""
|
|
480
|
+
Computes the location of zero-crossings on 'edges_x' using linear interpolation with 'edges_weight'.
|
|
481
|
+
"""
|
|
482
|
+
edge_dim = edges_weight.dim() - 2
|
|
483
|
+
assert edges_weight.shape[edge_dim] == 2
|
|
484
|
+
edges_weight = torch.cat(
|
|
485
|
+
[
|
|
486
|
+
torch.index_select(
|
|
487
|
+
input=edges_weight,
|
|
488
|
+
index=torch.tensor([1], device=self.device),
|
|
489
|
+
dim=edge_dim,
|
|
490
|
+
),
|
|
491
|
+
-torch.index_select(
|
|
492
|
+
input=edges_weight,
|
|
493
|
+
index=torch.tensor([0], device=self.device),
|
|
494
|
+
dim=edge_dim,
|
|
495
|
+
),
|
|
496
|
+
],
|
|
497
|
+
edge_dim,
|
|
498
|
+
)
|
|
499
|
+
denominator = edges_weight.sum(edge_dim)
|
|
500
|
+
ue = (edges_x * edges_weight).sum(edge_dim) / denominator
|
|
501
|
+
return ue
|
|
502
|
+
|
|
503
|
+
def _compute_vd(
|
|
504
|
+
self, x_nx2, surf_edges, s_n, case_ids, beta_fx4, alpha_fx4, idx_map
|
|
505
|
+
):
|
|
506
|
+
"""
|
|
507
|
+
Computes the location of dual vertices as described in Section 4.2
|
|
508
|
+
"""
|
|
509
|
+
alpha_nx4x2 = torch.index_select(
|
|
510
|
+
input=alpha_fx4, index=self.cube_edges, dim=1
|
|
511
|
+
).reshape(-1, 4, 2)
|
|
512
|
+
surf_edges_x = torch.index_select(
|
|
513
|
+
input=x_nx2, index=surf_edges.reshape(-1), dim=0
|
|
514
|
+
).reshape(-1, 2, 2)
|
|
515
|
+
surf_edges_s = torch.index_select(
|
|
516
|
+
input=s_n, index=surf_edges.reshape(-1), dim=0
|
|
517
|
+
).reshape(-1, 2, 1)
|
|
518
|
+
zero_crossing = self._linear_interp(surf_edges_s, surf_edges_x)
|
|
519
|
+
|
|
520
|
+
idx_map = idx_map.reshape(-1, 4)
|
|
521
|
+
|
|
522
|
+
edge_group, edge_group_to_vd, edge_group_to_cube, vd_num_edges = (
|
|
523
|
+
[],
|
|
524
|
+
[],
|
|
525
|
+
[],
|
|
526
|
+
[],
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
num_vd = self.num_vd_table[case_ids]
|
|
530
|
+
# This is the "Source of Truth" for ordering
|
|
531
|
+
v_offsets = torch.cumsum(num_vd, dim=0) - num_vd
|
|
532
|
+
total_num_vd = num_vd.sum().item()
|
|
533
|
+
|
|
534
|
+
# total_num_vd = 0
|
|
535
|
+
vd = torch.zeros((total_num_vd, 2), device=self.device)
|
|
536
|
+
beta_sum = torch.zeros((total_num_vd, 1), device=self.device)
|
|
537
|
+
vd_num_edges = torch.zeros((total_num_vd, 1), device=self.device)
|
|
538
|
+
|
|
539
|
+
vd_idx_map = torch.zeros(
|
|
540
|
+
(case_ids.shape[0], 4),
|
|
541
|
+
dtype=torch.long,
|
|
542
|
+
device=self.device,
|
|
543
|
+
requires_grad=False,
|
|
544
|
+
)
|
|
545
|
+
all_edge_groups = []
|
|
546
|
+
all_edge_to_vd = []
|
|
547
|
+
all_edge_to_cube = []
|
|
548
|
+
all_vd_num_edges = []
|
|
549
|
+
for num in torch.unique(num_vd):
|
|
550
|
+
cur_cubes_mask = num_vd == num
|
|
551
|
+
num_selected = cur_cubes_mask.sum()
|
|
552
|
+
batch_v_starts = v_offsets[cur_cubes_mask]
|
|
553
|
+
# curr_num_vd = cur_cubes.sum() * num
|
|
554
|
+
curr_edge_group = self.dmc_table[case_ids[cur_cubes_mask], :num].reshape(
|
|
555
|
+
num_selected, -1
|
|
556
|
+
)
|
|
557
|
+
# Create global vertex IDs for this batch
|
|
558
|
+
# local_ids: [0, 1, 0, 1...] for num=2
|
|
559
|
+
local_ids = torch.arange(num, device=self.device).repeat(num_selected)
|
|
560
|
+
# global_ids: [start0, start0, start1, start1...]
|
|
561
|
+
global_starts = batch_v_starts.repeat_interleave(num)
|
|
562
|
+
|
|
563
|
+
# This aligns the dual vertex ID with your triangulation offsets
|
|
564
|
+
curr_edge_group_to_vd = (
|
|
565
|
+
(global_starts + local_ids).reshape(-1, 1).repeat(1, 2).reshape(-1)
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Mapping cubes to their global indices
|
|
569
|
+
curr_edge_group_to_cube = torch.arange(
|
|
570
|
+
case_ids.shape[0], device=self.device
|
|
571
|
+
)[cur_cubes_mask].repeat_interleave(num * 2)
|
|
572
|
+
|
|
573
|
+
curr_mask = curr_edge_group.reshape(-1) != -1
|
|
574
|
+
curr_vd_edge_counts = curr_mask.reshape(-1, 2).sum(-1, keepdims=True)
|
|
575
|
+
all_vd_num_edges.append(curr_vd_edge_counts)
|
|
576
|
+
all_edge_groups.append(curr_edge_group.reshape(-1)[curr_mask])
|
|
577
|
+
all_edge_to_vd.append(curr_edge_group_to_vd[curr_mask])
|
|
578
|
+
all_edge_to_cube.append(curr_edge_group_to_cube[curr_mask])
|
|
579
|
+
|
|
580
|
+
edge_group = torch.cat(all_edge_groups)
|
|
581
|
+
edge_group_to_vd = torch.cat(all_edge_to_vd)
|
|
582
|
+
edge_group_to_cube = torch.cat(all_edge_to_cube)
|
|
583
|
+
vd_num_edges = torch.cat(all_vd_num_edges)
|
|
584
|
+
|
|
585
|
+
vd = torch.zeros((total_num_vd, 2), device=self.device)
|
|
586
|
+
beta_sum = torch.zeros((total_num_vd, 1), device=self.device)
|
|
587
|
+
|
|
588
|
+
idx_group = torch.gather(
|
|
589
|
+
input=idx_map.reshape(-1), dim=0, index=edge_group_to_cube * 4 + edge_group
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# remove the idx_group>0, should not be needed
|
|
593
|
+
x_group = torch.index_select(
|
|
594
|
+
input=surf_edges_x, index=idx_group.reshape(-1), dim=0
|
|
595
|
+
).reshape(-1, 2, 2)
|
|
596
|
+
s_group = torch.index_select(
|
|
597
|
+
input=surf_edges_s, index=idx_group.reshape(-1), dim=0
|
|
598
|
+
).reshape(-1, 2, 1)
|
|
599
|
+
|
|
600
|
+
zero_crossing_group = torch.index_select(
|
|
601
|
+
input=zero_crossing, index=idx_group.reshape(-1), dim=0
|
|
602
|
+
).reshape(-1, 2)
|
|
603
|
+
|
|
604
|
+
alpha_group = torch.index_select(
|
|
605
|
+
input=alpha_nx4x2.reshape(-1, 2),
|
|
606
|
+
dim=0,
|
|
607
|
+
index=edge_group_to_cube * 4 + edge_group,
|
|
608
|
+
).reshape(-1, 2, 1)
|
|
609
|
+
ue_group = self._linear_interp(s_group * alpha_group, x_group)
|
|
610
|
+
|
|
611
|
+
beta_group = torch.gather(
|
|
612
|
+
input=beta_fx4.reshape(-1), dim=0, index=edge_group_to_cube * 4 + edge_group
|
|
613
|
+
).reshape(-1, 1)
|
|
614
|
+
beta_sum = beta_sum.index_add_(0, index=edge_group_to_vd, source=beta_group)
|
|
615
|
+
vd = (
|
|
616
|
+
vd.index_add_(0, index=edge_group_to_vd, source=ue_group * beta_group)
|
|
617
|
+
/ beta_sum
|
|
618
|
+
)
|
|
619
|
+
L_dev = self._compute_reg_loss(
|
|
620
|
+
vd, zero_crossing_group, edge_group_to_vd, vd_num_edges
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# v_idx = torch.arange(vd.shape[0], device=self.device) # + total_num_vd
|
|
624
|
+
|
|
625
|
+
vd_idx_map = vd_idx_map.reshape(-1).scatter(
|
|
626
|
+
dim=0, index=edge_group_to_cube * 4 + edge_group, src=edge_group_to_vd
|
|
627
|
+
)
|
|
628
|
+
# for vert in vd:
|
|
629
|
+
# ax.scatter(vert[0], vert[1], marker="x", color="lightblue")
|
|
630
|
+
|
|
631
|
+
return vd, L_dev, vd_idx_map
|
|
632
|
+
|
|
633
|
+
def _extract_zero_contour(
|
|
634
|
+
self, s_n, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask
|
|
635
|
+
):
|
|
636
|
+
"""
|
|
637
|
+
Connects two adjacent dual vertices around an active square to form a contour.
|
|
638
|
+
"""
|
|
639
|
+
|
|
640
|
+
# for e_idx, e in enumerate(surf_edges):
|
|
641
|
+
# v0, v1 = x_nx2[e]
|
|
642
|
+
# color = "red" if edge_counts[surf_edges_mask][e_idx] == 1 else "green"
|
|
643
|
+
# mid = (v0 + v1) / 2
|
|
644
|
+
# ax.plot([v0[0], v1[0]], [v0[1], v1[1]], color=color, linewidth=1.5)
|
|
645
|
+
# # ax.text(
|
|
646
|
+
# # mid[0], mid[1], str(edge_counts[e_idx].item()), color=color, fontsize=7
|
|
647
|
+
# # )
|
|
648
|
+
# fig.savefig("sdf.png", dpi=1000, bbox_inches="tight")
|
|
649
|
+
with torch.no_grad():
|
|
650
|
+
group_mask = (
|
|
651
|
+
edge_counts == 2
|
|
652
|
+
) & surf_edges_mask # surface edges shared by 2 cubes.
|
|
653
|
+
group = idx_map.reshape(-1)[group_mask]
|
|
654
|
+
vd_idx = vd_idx_map[group_mask]
|
|
655
|
+
edge_indices, indices = torch.sort(group, stable=True)
|
|
656
|
+
quad_vd_idx = vd_idx[indices].reshape(-1, 2)
|
|
657
|
+
|
|
658
|
+
# Ensure all face directions point towards the positive SDF to maintain consistent winding.
|
|
659
|
+
s_edges = s_n[
|
|
660
|
+
surf_edges[edge_indices.reshape(-1, 2)[:, 0]].reshape(-1)
|
|
661
|
+
].reshape(-1, 2)
|
|
662
|
+
flip_mask = s_edges[:, 0] > 0
|
|
663
|
+
quad_vd_idx[flip_mask] = quad_vd_idx[flip_mask][:, [0, 1]]
|
|
664
|
+
quad_vd_idx[~flip_mask] = quad_vd_idx[~flip_mask][:, [1, 0]]
|
|
665
|
+
|
|
666
|
+
return vd, quad_vd_idx, s_edges, edge_indices
|
|
667
|
+
|
|
668
|
+
def _triangulate(
|
|
669
|
+
self,
|
|
670
|
+
x_nx2,
|
|
671
|
+
s_n,
|
|
672
|
+
square_fx4,
|
|
673
|
+
surf_vertices,
|
|
674
|
+
edges,
|
|
675
|
+
surf_edges_global,
|
|
676
|
+
s_edges,
|
|
677
|
+
case_ids,
|
|
678
|
+
edge_indices,
|
|
679
|
+
surf_cubes,
|
|
680
|
+
):
|
|
681
|
+
"""
|
|
682
|
+
Triangulates the interior surface to produce a triangular mesh, adopted from 3D as described in Section 4.5.
|
|
683
|
+
"""
|
|
684
|
+
# occupancy field per query point
|
|
685
|
+
occ_n = s_n < 0
|
|
686
|
+
# occupancy field for each square
|
|
687
|
+
occ_fx4 = occ_n[square_fx4.reshape(-1)].reshape(-1, 4)
|
|
688
|
+
# sum of occupancy
|
|
689
|
+
# == 1: 1 corner inside
|
|
690
|
+
# == 2: 2 corners inside
|
|
691
|
+
# == 3: 3 corners inside
|
|
692
|
+
# == 4: 4 corners inside
|
|
693
|
+
occ_sum = torch.sum(occ_fx4, -1)
|
|
694
|
+
# get number of vd, most have 1, but the edge cases have 2
|
|
695
|
+
# the fully filled and completely empty squares have none
|
|
696
|
+
vd_counts = self.num_vd_table[case_ids]
|
|
697
|
+
v_offsets = torch.cumsum(vd_counts, dim=0) - vd_counts
|
|
698
|
+
tris_list = []
|
|
699
|
+
"""
|
|
700
|
+
The first step is to connect all surface edges to the interior vertices
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
inside_verts = x_nx2[occ_n.reshape(-1)]
|
|
704
|
+
mapping_inside_verts = (
|
|
705
|
+
torch.ones((occ_n.shape[0]), dtype=torch.long, device=self.device) * -1
|
|
706
|
+
)
|
|
707
|
+
mapping_inside_verts[occ_n.reshape(-1)] = (
|
|
708
|
+
torch.arange(occ_n.sum(), device=self.device) + surf_vertices.shape[0]
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
s_edges = s_n[surf_edges_global].reshape(-1, 2) # signed distance at each end
|
|
712
|
+
inside_mask = s_edges < 0
|
|
713
|
+
inside_verts_idx = mapping_inside_verts[surf_edges_global[inside_mask]]
|
|
714
|
+
|
|
715
|
+
tris_surface = torch.cat([edges, inside_verts_idx.unsqueeze(-1)], -1)
|
|
716
|
+
tris_list.append(tris_surface)
|
|
717
|
+
vertices = torch.cat([surf_vertices, inside_verts])
|
|
718
|
+
# plot_triangles(
|
|
719
|
+
# "tris_inside_after_surf.png",
|
|
720
|
+
# triangles=tris_surface,
|
|
721
|
+
# vertices=vertices,
|
|
722
|
+
# x_nx2=x_nx2,
|
|
723
|
+
# s_n=s_n,
|
|
724
|
+
# )
|
|
725
|
+
|
|
726
|
+
# =====================================================================
|
|
727
|
+
#
|
|
728
|
+
# The next step is to find all quads that are fully inside (occsum==4)
|
|
729
|
+
# and connect the center to the edges
|
|
730
|
+
#
|
|
731
|
+
# =====================================================================
|
|
732
|
+
|
|
733
|
+
quads_occ4_center = (
|
|
734
|
+
x_nx2[square_fx4[occ_sum == 4].reshape(-1)].reshape(-1, 4, 2).mean(1)
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
quad_occ4_center_x = x_nx2[square_fx4[occ_sum == 4]].mean(1)
|
|
738
|
+
center_idx = (
|
|
739
|
+
torch.arange(quad_occ4_center_x.shape[0], device=self.device)
|
|
740
|
+
+ surf_vertices.shape[0]
|
|
741
|
+
+ inside_verts.shape[0]
|
|
742
|
+
)
|
|
743
|
+
index_occ4 = torch.tensor([[0, 1], [1, 3], [3, 2], [2, 0]], device=self.device)
|
|
744
|
+
quads_occ4 = square_fx4[occ_sum == 4]
|
|
745
|
+
centers_occ4_x4 = center_idx.unsqueeze(1).expand(-1, 4).reshape(-1, 1)
|
|
746
|
+
|
|
747
|
+
outside_edges_occ4 = mapping_inside_verts[quads_occ4[:, index_occ4]]
|
|
748
|
+
# and connect it to the center node:
|
|
749
|
+
# each triangle = [center, corner1, corner2]
|
|
750
|
+
tris_inside_occ4 = torch.cat(
|
|
751
|
+
[centers_occ4_x4, outside_edges_occ4.reshape(-1, 2)], dim=1
|
|
752
|
+
)
|
|
753
|
+
vertices = torch.cat([surf_vertices, inside_verts, quads_occ4_center])
|
|
754
|
+
tris_list.append(tris_inside_occ4)
|
|
755
|
+
# plot_triangles(
|
|
756
|
+
# "tris_inside_after_4.png",
|
|
757
|
+
# triangles=tris_inside_occ4,
|
|
758
|
+
# vertices=vertices,
|
|
759
|
+
# x_nx2=x_nx2,
|
|
760
|
+
# s_n=s_n,
|
|
761
|
+
# )
|
|
762
|
+
# =====================================================================
|
|
763
|
+
#
|
|
764
|
+
# The next step is to find all quads where three corners are inside
|
|
765
|
+
#
|
|
766
|
+
# =====================================================================
|
|
767
|
+
|
|
768
|
+
surf_vert_ids_occ3 = v_offsets[occ_sum[surf_cubes] == 3]
|
|
769
|
+
case_ids_occ3 = case_ids[occ_sum[surf_cubes] == 3]
|
|
770
|
+
quads_occ3 = square_fx4[occ_sum == 3]
|
|
771
|
+
index_occ3 = self.tet_table[case_ids_occ3]
|
|
772
|
+
K_occ3 = index_occ3.shape[1] # number of triangles per quad
|
|
773
|
+
|
|
774
|
+
# Expand quads so we can gather per-row
|
|
775
|
+
quads_expanded_occ3 = quads_occ3.unsqueeze(1).expand(-1, K_occ3, -1)
|
|
776
|
+
|
|
777
|
+
outside_edges_occ3 = mapping_inside_verts[
|
|
778
|
+
torch.gather(quads_expanded_occ3, 2, index_occ3)
|
|
779
|
+
]
|
|
780
|
+
# and connect it to the center node:
|
|
781
|
+
# each triangle = [center, corner1, corner2]
|
|
782
|
+
centers = surf_vert_ids_occ3.unsqueeze(1).expand(-1, K_occ3) # (Q, K)
|
|
783
|
+
tris_inside_occ3 = torch.cat(
|
|
784
|
+
[centers.reshape(-1, 1), outside_edges_occ3.reshape(-1, 2)], dim=1
|
|
785
|
+
)
|
|
786
|
+
tris_list.append(tris_inside_occ3)
|
|
787
|
+
# plot_triangles(
|
|
788
|
+
# "tris_inside_after_3.png",
|
|
789
|
+
# triangles=tris_inside_occ3,
|
|
790
|
+
# vertices=vertices,
|
|
791
|
+
# x_nx2=x_nx2,
|
|
792
|
+
# s_n=s_n,
|
|
793
|
+
# )
|
|
794
|
+
# =====================================================================
|
|
795
|
+
#
|
|
796
|
+
# The next step is to find all quads where two corners are inside
|
|
797
|
+
# non edge cases (i.e. all except 6 and 9)
|
|
798
|
+
# =====================================================================
|
|
799
|
+
mask_occ2_standard = torch.logical_and(vd_counts == 1, occ_sum[surf_cubes] == 2)
|
|
800
|
+
if mask_occ2_standard.any():
|
|
801
|
+
surf_vert_ids_occ2_standard = v_offsets[mask_occ2_standard]
|
|
802
|
+
case_ids_occ2_standard = case_ids[mask_occ2_standard]
|
|
803
|
+
quads_occ2_standard = square_fx4[surf_cubes][mask_occ2_standard] # (Q,4)
|
|
804
|
+
quads_expanded_occ2_standard = quads_occ2_standard.unsqueeze(1).expand(
|
|
805
|
+
-1, 2, -1
|
|
806
|
+
)
|
|
807
|
+
index_occ2_standard = self.tet_table[case_ids_occ2_standard] # (Q, 2, 2)
|
|
808
|
+
|
|
809
|
+
valid_mask_occ2_standard = index_occ2_standard[..., 0] != -1 # (Q, 2)
|
|
810
|
+
|
|
811
|
+
outside_edges_occ2_standard = torch.gather(
|
|
812
|
+
quads_expanded_occ2_standard[valid_mask_occ2_standard, :],
|
|
813
|
+
1,
|
|
814
|
+
index_occ2_standard[valid_mask_occ2_standard],
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
outside_edges_occ2_standard = mapping_inside_verts[
|
|
818
|
+
outside_edges_occ2_standard
|
|
819
|
+
]
|
|
820
|
+
|
|
821
|
+
centers_occ2_standard = surf_vert_ids_occ2_standard.unsqueeze(1) # (Q, K)
|
|
822
|
+
tris_inside_occ2_standard = torch.cat(
|
|
823
|
+
[
|
|
824
|
+
centers_occ2_standard.reshape(-1, 1),
|
|
825
|
+
outside_edges_occ2_standard.reshape(-1, 2),
|
|
826
|
+
],
|
|
827
|
+
dim=1,
|
|
828
|
+
)
|
|
829
|
+
tris_list.append(tris_inside_occ2_standard)
|
|
830
|
+
# plot_triangles(
|
|
831
|
+
# "tris_inside_after_2_standard.png",
|
|
832
|
+
# triangles=tris_inside_occ2_standard,
|
|
833
|
+
# vertices=vertices,
|
|
834
|
+
# x_nx2=x_nx2,
|
|
835
|
+
# s_n=s_n,
|
|
836
|
+
# )
|
|
837
|
+
# =====================================================================
|
|
838
|
+
#
|
|
839
|
+
# The next step is to find all quads where two corners are inside
|
|
840
|
+
# edge cases (i.e. 6 and 9)
|
|
841
|
+
# =====================================================================
|
|
842
|
+
mask_occ2_edge = torch.logical_and(vd_counts == 2, occ_sum[surf_cubes] == 2)
|
|
843
|
+
if mask_occ2_edge.any():
|
|
844
|
+
center_1_odd2_edge = (v_offsets[mask_occ2_edge]).unsqueeze(1).repeat(1, 2)
|
|
845
|
+
center_2_odd2_edge = center_1_odd2_edge + 1
|
|
846
|
+
case_ids_occ2_edge = case_ids[mask_occ2_edge]
|
|
847
|
+
# occ sum = on the global level, mask_occ2_edge = on surface level
|
|
848
|
+
quads_occ2_edge = square_fx4[surf_cubes][mask_occ2_edge] # (Q,4)
|
|
849
|
+
quads_expanded_occ2_edge = quads_occ2_edge.unsqueeze(1).expand(-1, 2, -1)
|
|
850
|
+
index_occ2_edge = self.tet_table[case_ids_occ2_edge] # (Q, 2, 2)
|
|
851
|
+
|
|
852
|
+
valid_mask_occ2_edge = index_occ2_edge[..., 0] != -1 # (Q, 2)
|
|
853
|
+
|
|
854
|
+
outside_edges_occ2_edge = torch.gather(
|
|
855
|
+
quads_expanded_occ2_edge[valid_mask_occ2_edge, :],
|
|
856
|
+
1,
|
|
857
|
+
index_occ2_edge[valid_mask_occ2_edge],
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
outside_edges_occ2_edge = mapping_inside_verts[outside_edges_occ2_edge]
|
|
861
|
+
|
|
862
|
+
tris_inside_occ2_edge = torch.cat(
|
|
863
|
+
[
|
|
864
|
+
center_1_odd2_edge.reshape(-1, 1),
|
|
865
|
+
center_2_odd2_edge.reshape(-1, 1),
|
|
866
|
+
outside_edges_occ2_edge.reshape(-1, 1),
|
|
867
|
+
],
|
|
868
|
+
dim=1,
|
|
869
|
+
)
|
|
870
|
+
# plot_triangles(
|
|
871
|
+
# "tris_inside_after_2_edge.png",
|
|
872
|
+
# triangles=tris_inside_occ2_edge,
|
|
873
|
+
# vertices=vertices,
|
|
874
|
+
# x_nx2=x_nx2,
|
|
875
|
+
# s_n=s_n,
|
|
876
|
+
# )
|
|
877
|
+
|
|
878
|
+
tris_list.append(tris_inside_occ2_edge)
|
|
879
|
+
tris = torch.cat(tris_list)
|
|
880
|
+
# fix orientation
|
|
881
|
+
p0 = vertices[tris[:, 0]]
|
|
882
|
+
p1 = vertices[tris[:, 1]]
|
|
883
|
+
p2 = vertices[tris[:, 2]]
|
|
884
|
+
|
|
885
|
+
cross = (p1[:, 0] - p0[:, 0]) * (p2[:, 1] - p0[:, 1]) - (
|
|
886
|
+
p1[:, 1] - p0[:, 1]
|
|
887
|
+
) * (p2[:, 0] - p0[:, 0])
|
|
888
|
+
|
|
889
|
+
flip = cross < 0
|
|
890
|
+
|
|
891
|
+
# swap columns 1 and 2 where needed
|
|
892
|
+
tris_flipped = tris.clone()
|
|
893
|
+
tris_flipped[flip, 1] = tris[flip, 2]
|
|
894
|
+
tris_flipped[flip, 2] = tris[flip, 1]
|
|
895
|
+
|
|
896
|
+
tris = tris_flipped
|
|
897
|
+
|
|
898
|
+
return vertices, tris
|
|
899
|
+
|
|
900
|
+
def check_open_mesh(self, edges, num_vertices=None):
|
|
901
|
+
# edges shape: (Num_Edges, 2)
|
|
902
|
+
flattened_edges = edges.reshape(-1)
|
|
903
|
+
|
|
904
|
+
# Count occurrences of each vertex ID
|
|
905
|
+
if num_vertices is None:
|
|
906
|
+
num_vertices = flattened_edges.max() + 1
|
|
907
|
+
|
|
908
|
+
counts = torch.bincount(flattened_edges, minlength=num_vertices)
|
|
909
|
+
|
|
910
|
+
# A mesh is open if any vertex is used only once
|
|
911
|
+
open_points = (counts == 1).sum()
|
|
912
|
+
if open_points != 0:
|
|
913
|
+
raise RuntimeError(
|
|
914
|
+
"Unclosed mesh found. Check your bounds or SDF definition!"
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
# def plot_triangles(filename, triangles, vertices, x_nx2, s_n):
|
|
919
|
+
# import matplotlib.pyplot as plt
|
|
920
|
+
|
|
921
|
+
# fig, ax = plt.subplots()
|
|
922
|
+
# ax.set_aspect(1)
|
|
923
|
+
# for tri in triangles:
|
|
924
|
+
# v0, v1, c = tri
|
|
925
|
+
# p1 = vertices[v0]
|
|
926
|
+
# p2 = vertices[v1]
|
|
927
|
+
# p3 = vertices[c]
|
|
928
|
+
# ax.plot(
|
|
929
|
+
# [p1[0].item(), p2[0].item(), p3[0].item(), p1[0].item()],
|
|
930
|
+
# [p1[1].item(), p2[1].item(), p3[1].item(), p1[1].item()],
|
|
931
|
+
# "-k",
|
|
932
|
+
# )
|
|
933
|
+
# ax.text(
|
|
934
|
+
# p1[0].item(),
|
|
935
|
+
# p1[1].item(),
|
|
936
|
+
# str(v0),
|
|
937
|
+
# color="black",
|
|
938
|
+
# fontsize=8,
|
|
939
|
+
# ha="center",
|
|
940
|
+
# va="center",
|
|
941
|
+
# )
|
|
942
|
+
# ax.text(
|
|
943
|
+
# p2[0].item(),
|
|
944
|
+
# p2[1].item(),
|
|
945
|
+
# str(v1),
|
|
946
|
+
# color="black",
|
|
947
|
+
# fontsize=8,
|
|
948
|
+
# ha="center",
|
|
949
|
+
# va="center",
|
|
950
|
+
# )
|
|
951
|
+
# ax.text(
|
|
952
|
+
# p3[0].item(),
|
|
953
|
+
# p3[1].item(),
|
|
954
|
+
# str(c),
|
|
955
|
+
# color="black",
|
|
956
|
+
# fontsize=8,
|
|
957
|
+
# ha="center",
|
|
958
|
+
# va="center",
|
|
959
|
+
# )
|
|
960
|
+
|
|
961
|
+
# ax.scatter(
|
|
962
|
+
# x_nx2[:, 0].detach().cpu().numpy(),
|
|
963
|
+
# x_nx2[:, 1].detach().cpu().numpy(),
|
|
964
|
+
# c=s_n.detach().cpu().numpy(),
|
|
965
|
+
# cmap="coolwarm",
|
|
966
|
+
# )
|
|
967
|
+
# fig.savefig(filename, dpi=1000)
|