iarap 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iarap-0.1.0/PKG-INFO +83 -0
- iarap-0.1.0/README.md +70 -0
- iarap-0.1.0/iarap/__init__.py +3 -0
- iarap-0.1.0/iarap/__polar_decomposition.py +204 -0
- iarap-0.1.0/iarap/energy.py +50 -0
- iarap-0.1.0/iarap/sampling.py +174 -0
- iarap-0.1.0/iarap.egg-info/PKG-INFO +83 -0
- iarap-0.1.0/iarap.egg-info/SOURCES.txt +12 -0
- iarap-0.1.0/iarap.egg-info/dependency_links.txt +1 -0
- iarap-0.1.0/iarap.egg-info/requires.txt +1 -0
- iarap-0.1.0/iarap.egg-info/top_level.txt +1 -0
- iarap-0.1.0/licence.txt +19 -0
- iarap-0.1.0/pyproject.toml +27 -0
- iarap-0.1.0/setup.cfg +4 -0
iarap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iarap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ARAP regularization for implicit surfaces
|
|
5
|
+
Author-email: Tobias Djuren <t.djuren@tu-berlin.de>, Markus Worchel <m.worchel@tu-berlin.de>, Ugo Finnendahl <finnendahl@tu-berlin.de>, Marc Alexa <marc.alexa@tu-berlin.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: licence.txt
|
|
11
|
+
Requires-Dist: torch<2.12,>=2.11
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ARAP-Regularization-for-Implicit-Surfaces
|
|
18
|
+
|
|
19
|
+
This is the source code for the paper [As-Rigid-As-Possible Regularization for Implicit Surfaces](https://diglib.eg.org/items/de832211-9ab2-431d-bb2b-cde770a5b5ca), SGP 2026.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
The only dependency is [pytorch](https://pytorch.org/) (tested with torch 2.11.0 with CUDA 12.8). For the demos that generate results similar to those in the paper (implict surface modeling and Gauss Stylization) further dependencies are required (see `demos/requirements.txt`).
|
|
24
|
+
|
|
25
|
+
````powershell
|
|
26
|
+
python -m venv .env
|
|
27
|
+
.\.venv\Scripts\Activate.ps1 # depends on your os
|
|
28
|
+
pip install git+https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces.git
|
|
29
|
+
````
|
|
30
|
+
|
|
31
|
+
## Usage (iarap.arap_energy)
|
|
32
|
+
`iarap.arap_energy` computes the batch-averaged ARAP regularization energy ($\mathcal{L}_{ARAP}$) for a deformation model $\boldsymbol f$.
|
|
33
|
+
|
|
34
|
+
* Input:
|
|
35
|
+
|
|
36
|
+
`f`: deformation function as `torch.nn.Module` $(B, d) \rightarrow (B, d)$
|
|
37
|
+
|
|
38
|
+
`samples`: points of shape $(B, d)$ (tested with $d=2$ and $d=3$)
|
|
39
|
+
|
|
40
|
+
`l_bend`: bending weight ($\lambda_{bend}$)
|
|
41
|
+
* Output:
|
|
42
|
+
scalar tensor (mean ARAP energy)
|
|
43
|
+
|
|
44
|
+
Minimal example:
|
|
45
|
+
|
|
46
|
+
````python
|
|
47
|
+
import torch
|
|
48
|
+
import iarap
|
|
49
|
+
|
|
50
|
+
class Identity(torch.nn.Module):
|
|
51
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
52
|
+
return x
|
|
53
|
+
|
|
54
|
+
f = Identity()
|
|
55
|
+
samples = torch.randn(1024, 3, device="cuda" if torch.cuda.is_available() else "cpu")
|
|
56
|
+
loss = iarap.arap_energy(f, samples, l_bend=0.1)
|
|
57
|
+
print(loss.item())
|
|
58
|
+
````
|
|
59
|
+
|
|
60
|
+
## Demos
|
|
61
|
+
|
|
62
|
+
Please see `./demos/surface_modeling.ipynb` to see how ARAP regularization can be used to model implicit surfaces with user defined handles. In `./demos/gauss_stylization.ipynb` you will find the implict variant of [Gauss Stylization](https://github.com/ugogon/gaussStylization) as described in the paper.
|
|
63
|
+
|
|
64
|
+
There are multiple pretrained signed distance functions in `./demos/sdfs`. Please note that some sdfs are based on meshes that are under CC-BY.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Citation
|
|
68
|
+
|
|
69
|
+
If you use the repository, please cite
|
|
70
|
+
```
|
|
71
|
+
@article{10.1111:cgf.70519,
|
|
72
|
+
journal = {Computer Graphics Forum},
|
|
73
|
+
title = {{As-Rigid-As-Possible Regularization for Implicit Surfaces}},
|
|
74
|
+
author = {Djuren, Tobias and
|
|
75
|
+
Worchel, Markus and
|
|
76
|
+
Finnendahl, Ugo and
|
|
77
|
+
Alexa, Marc},
|
|
78
|
+
year = {2026},
|
|
79
|
+
publisher = {The Eurographics Association and John Wiley & Sons Ltd.},
|
|
80
|
+
ISSN = {1467-8659},
|
|
81
|
+
DOI = {10.1111/cgf.70519}
|
|
82
|
+
}
|
|
83
|
+
```
|
iarap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# ARAP-Regularization-for-Implicit-Surfaces
|
|
5
|
+
|
|
6
|
+
This is the source code for the paper [As-Rigid-As-Possible Regularization for Implicit Surfaces](https://diglib.eg.org/items/de832211-9ab2-431d-bb2b-cde770a5b5ca), SGP 2026.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
The only dependency is [pytorch](https://pytorch.org/) (tested with torch 2.11.0 with CUDA 12.8). For the demos that generate results similar to those in the paper (implict surface modeling and Gauss Stylization) further dependencies are required (see `demos/requirements.txt`).
|
|
11
|
+
|
|
12
|
+
````powershell
|
|
13
|
+
python -m venv .env
|
|
14
|
+
.\.venv\Scripts\Activate.ps1 # depends on your os
|
|
15
|
+
pip install git+https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces.git
|
|
16
|
+
````
|
|
17
|
+
|
|
18
|
+
## Usage (iarap.arap_energy)
|
|
19
|
+
`iarap.arap_energy` computes the batch-averaged ARAP regularization energy ($\mathcal{L}_{ARAP}$) for a deformation model $\boldsymbol f$.
|
|
20
|
+
|
|
21
|
+
* Input:
|
|
22
|
+
|
|
23
|
+
`f`: deformation function as `torch.nn.Module` $(B, d) \rightarrow (B, d)$
|
|
24
|
+
|
|
25
|
+
`samples`: points of shape $(B, d)$ (tested with $d=2$ and $d=3$)
|
|
26
|
+
|
|
27
|
+
`l_bend`: bending weight ($\lambda_{bend}$)
|
|
28
|
+
* Output:
|
|
29
|
+
scalar tensor (mean ARAP energy)
|
|
30
|
+
|
|
31
|
+
Minimal example:
|
|
32
|
+
|
|
33
|
+
````python
|
|
34
|
+
import torch
|
|
35
|
+
import iarap
|
|
36
|
+
|
|
37
|
+
class Identity(torch.nn.Module):
|
|
38
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
39
|
+
return x
|
|
40
|
+
|
|
41
|
+
f = Identity()
|
|
42
|
+
samples = torch.randn(1024, 3, device="cuda" if torch.cuda.is_available() else "cpu")
|
|
43
|
+
loss = iarap.arap_energy(f, samples, l_bend=0.1)
|
|
44
|
+
print(loss.item())
|
|
45
|
+
````
|
|
46
|
+
|
|
47
|
+
## Demos
|
|
48
|
+
|
|
49
|
+
Please see `./demos/surface_modeling.ipynb` to see how ARAP regularization can be used to model implicit surfaces with user defined handles. In `./demos/gauss_stylization.ipynb` you will find the implict variant of [Gauss Stylization](https://github.com/ugogon/gaussStylization) as described in the paper.
|
|
50
|
+
|
|
51
|
+
There are multiple pretrained signed distance functions in `./demos/sdfs`. Please note that some sdfs are based on meshes that are under CC-BY.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Citation
|
|
55
|
+
|
|
56
|
+
If you use the repository, please cite
|
|
57
|
+
```
|
|
58
|
+
@article{10.1111:cgf.70519,
|
|
59
|
+
journal = {Computer Graphics Forum},
|
|
60
|
+
title = {{As-Rigid-As-Possible Regularization for Implicit Surfaces}},
|
|
61
|
+
author = {Djuren, Tobias and
|
|
62
|
+
Worchel, Markus and
|
|
63
|
+
Finnendahl, Ugo and
|
|
64
|
+
Alexa, Marc},
|
|
65
|
+
year = {2026},
|
|
66
|
+
publisher = {The Eurographics Association and John Wiley & Sons Ltd.},
|
|
67
|
+
ISSN = {1467-8659},
|
|
68
|
+
DOI = {10.1111/cgf.70519}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polar Decomposition with Stable Gradients for PyTorch
|
|
3
|
+
=====================================================
|
|
4
|
+
|
|
5
|
+
Computes J = R @ S where R is a proper rotation (det R = +1) and S is symmetric.
|
|
6
|
+
|
|
7
|
+
Note: This implements the "right polar decomposition with proper rotation",
|
|
8
|
+
which enforces det(R) = +1 rather than allowing improper rotations (reflections).
|
|
9
|
+
As a consequence, S is symmetric but NOT necessarily positive definite. If J has
|
|
10
|
+
a negative determinant, one eigenvalue of S will be negative (S absorbs the
|
|
11
|
+
reflection). The standard polar decomposition (where R may have det = -1) always
|
|
12
|
+
yields S positive semi-definite, but for applications in continuum mechanics and
|
|
13
|
+
geometry processing, the det = +1 convention is typically preferred so that R
|
|
14
|
+
always lives in SO(n).
|
|
15
|
+
|
|
16
|
+
The backward and forward-mode AD use the closed-form derivative of the polar
|
|
17
|
+
factor. The key equation in the eigenbasis of S:
|
|
18
|
+
|
|
19
|
+
Omega_{ab} = (T_{ab} - T_{ba}) / (sigma_a + sigma_b)
|
|
20
|
+
|
|
21
|
+
This involves only 1/(sigma_a + sigma_b), never 1/(sigma_a - sigma_b),
|
|
22
|
+
so it is perfectly stable when singular values coincide.
|
|
23
|
+
|
|
24
|
+
WARNING: When det(J) < 0, one of the sigma values is negative (by the flip
|
|
25
|
+
convention). If |sigma_a| = |sigma_b| for a pair with opposite signs, then
|
|
26
|
+
sigma_a + sigma_b = 0 and the derivative is genuinely singular. This reflects
|
|
27
|
+
a real geometric singularity: the polar factor is not differentiable there.
|
|
28
|
+
An optional `eps` parameter clamps the denominator for robustness, but this
|
|
29
|
+
introduces bias in the gradient.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import torch
|
|
33
|
+
from torch.autograd import Function
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PolarDecomposition(Function):
|
|
37
|
+
generate_vmap_rule = True
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def forward(J, eps=0.0):
|
|
41
|
+
"""
|
|
42
|
+
Compute polar decomposition J = R @ S with det(R) = +1.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
J: (..., n, n) tensor, must be invertible
|
|
46
|
+
eps: small value to clamp |sigma_a + sigma_b| away from zero in
|
|
47
|
+
the derivative. Only relevant when det(J) < 0 and two singular
|
|
48
|
+
values are nearly equal. Default 0.0 (no clamping).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
R: (..., n, n) proper rotation (orthogonal with det = +1)
|
|
52
|
+
S: (..., n, n) symmetric (positive definite if det(J) > 0,
|
|
53
|
+
otherwise one eigenvalue is negative)
|
|
54
|
+
sigma: (..., n) signed singular values (intermediate, for backward/jvp)
|
|
55
|
+
V: (..., n, n) right singular vectors (intermediate, for backward/jvp)
|
|
56
|
+
flip: (..., n) sign of the determinant of R
|
|
57
|
+
"""
|
|
58
|
+
U, sigma, Vh = torch.linalg.svd(J)
|
|
59
|
+
V = Vh.mH
|
|
60
|
+
|
|
61
|
+
# Ensure det(R) = +1
|
|
62
|
+
det = torch.det(U @ Vh)
|
|
63
|
+
flip = torch.ones_like(sigma)
|
|
64
|
+
flip[..., -1] = torch.sign(det)
|
|
65
|
+
U = U * flip.unsqueeze(-2)
|
|
66
|
+
sigma = sigma * flip
|
|
67
|
+
|
|
68
|
+
R = U @ Vh
|
|
69
|
+
S = V @ torch.diag_embed(sigma) @ Vh
|
|
70
|
+
|
|
71
|
+
return R, S, sigma, V, flip
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def setup_context(ctx, inputs, output):
|
|
75
|
+
J, eps = inputs
|
|
76
|
+
R, S, sigma, V, flip = output
|
|
77
|
+
ctx.save_for_backward(R, sigma, V)
|
|
78
|
+
ctx.eps = eps
|
|
79
|
+
# save as plain attributes for jvp (save_for_backward only
|
|
80
|
+
# populates saved_tensors for the backward path)
|
|
81
|
+
ctx.R = R
|
|
82
|
+
ctx.sigma = sigma
|
|
83
|
+
ctx.V = V
|
|
84
|
+
ctx.mark_non_differentiable(sigma, V, flip)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _safe_sig_sum(sigma, eps):
|
|
88
|
+
"""Compute sigma_a + sigma_b matrix with optional clamping."""
|
|
89
|
+
sig_sum = sigma.unsqueeze(-1) + sigma.unsqueeze(-2)
|
|
90
|
+
if eps > 0:
|
|
91
|
+
# Clamp absolute value away from zero, preserving sign.
|
|
92
|
+
# Where sig_sum is exactly zero, use +eps.
|
|
93
|
+
sign = sig_sum.sign()
|
|
94
|
+
sign = torch.where(sign == 0, torch.ones_like(sign), sign)
|
|
95
|
+
sig_sum = torch.where(sig_sum.abs() < eps, sign * eps, sig_sum)
|
|
96
|
+
return sig_sum
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def jvp(ctx, dJ, _deps):
|
|
100
|
+
"""
|
|
101
|
+
Forward-mode AD: compute tangents (dR, dS) given tangent dJ.
|
|
102
|
+
|
|
103
|
+
From J = R @ S, a perturbation dJ gives:
|
|
104
|
+
dJ = dR @ S + R @ dS
|
|
105
|
+
|
|
106
|
+
Multiplying by R^T:
|
|
107
|
+
T = R^T @ dJ = Omega @ S + dS
|
|
108
|
+
|
|
109
|
+
where Omega = R^T @ dR is antisymmetric.
|
|
110
|
+
|
|
111
|
+
In the eigenbasis of S (V-basis):
|
|
112
|
+
Omega_V_{ab} = (T_V_{ab} - T_V_{ba}) / (sigma_a + sigma_b) for a != b
|
|
113
|
+
Omega_V_{aa} = 0
|
|
114
|
+
dS_V = T_V - Omega_V @ diag(sigma) (symmetric part)
|
|
115
|
+
"""
|
|
116
|
+
R = ctx.R
|
|
117
|
+
sigma = ctx.sigma
|
|
118
|
+
V = ctx.V
|
|
119
|
+
sig_sum = PolarDecomposition._safe_sig_sum(sigma, ctx.eps)
|
|
120
|
+
|
|
121
|
+
# Transform perturbation into V-basis
|
|
122
|
+
T = R.mH @ dJ
|
|
123
|
+
T_V = V.mH @ T @ V
|
|
124
|
+
|
|
125
|
+
# Solve for Omega_V (antisymmetric)
|
|
126
|
+
Omega_V = (T_V - T_V.mH) / sig_sum
|
|
127
|
+
Omega_V = Omega_V - torch.diag_embed(torch.diagonal(Omega_V, dim1=-2, dim2=-1))
|
|
128
|
+
|
|
129
|
+
# dR = R @ V @ Omega_V @ V^T
|
|
130
|
+
Omega = V @ Omega_V @ V.mH
|
|
131
|
+
dR = R @ Omega
|
|
132
|
+
|
|
133
|
+
# dS = R^T @ dJ - Omega @ S = T - Omega @ S (symmetric)
|
|
134
|
+
S = V @ torch.diag_embed(sigma) @ V.mH
|
|
135
|
+
dS = T - Omega @ S
|
|
136
|
+
# Symmetrize to clean up floating point
|
|
137
|
+
dS = (dS + dS.mH) / 2
|
|
138
|
+
|
|
139
|
+
return dR, dS, None, None, None
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def backward(ctx, dR, dS, dsigma, dV, dflip):
|
|
143
|
+
R, sigma, V = ctx.saved_tensors
|
|
144
|
+
sig_sum = PolarDecomposition._safe_sig_sum(sigma, ctx.eps)
|
|
145
|
+
|
|
146
|
+
dJ = torch.zeros_like(R)
|
|
147
|
+
|
|
148
|
+
if dR is not None:
|
|
149
|
+
# Forward: dR = R @ V @ Omega_V @ V^T
|
|
150
|
+
# where Omega_V_{ab} = (T_V_{ab} - T_V_{ba}) / (sigma_a + sigma_b)
|
|
151
|
+
# and T_V = V^T @ R^T @ dJ @ V
|
|
152
|
+
#
|
|
153
|
+
# Adjoint: dL/dT_V_{ab} = (G_V_{ab} - G_V_{ba}) / (sigma_a + sigma_b)
|
|
154
|
+
# where G_V = V^T @ R^T @ grad_R @ V
|
|
155
|
+
# dL/dT_V_{aa} = 0
|
|
156
|
+
# dL/dJ = R @ V @ (dL/dT_V) @ V^T
|
|
157
|
+
|
|
158
|
+
G_V = V.mH @ R.mH @ dR @ V
|
|
159
|
+
Phi_V = (G_V - G_V.mH) / sig_sum
|
|
160
|
+
Phi_V = Phi_V - torch.diag_embed(torch.diagonal(Phi_V, dim1=-2, dim2=-1))
|
|
161
|
+
dJ = dJ + R @ V @ Phi_V @ V.mH
|
|
162
|
+
|
|
163
|
+
if dS is not None:
|
|
164
|
+
# Forward (V-basis): dS_V_{ab} = alpha_{ab} T_V_{ab} + beta_{ab} T_V_{ba}
|
|
165
|
+
# alpha_{ab} = sigma_a / (sigma_a + sigma_b)
|
|
166
|
+
# beta_{ab} = sigma_b / (sigma_a + sigma_b)
|
|
167
|
+
#
|
|
168
|
+
# Adjoint: dL/dT_V_{ab} = 2 * H_V_sym_{ab} * sigma_a/(sigma_a+sigma_b) for a!=b
|
|
169
|
+
# dL/dT_V_{aa} = H_V_sym_{aa}
|
|
170
|
+
|
|
171
|
+
H_V = V.mH @ dS @ V
|
|
172
|
+
H_V_sym = (H_V + H_V.mH) / 2
|
|
173
|
+
sig_a = sigma.unsqueeze(-1).expand_as(sig_sum)
|
|
174
|
+
coeff = 2 * sig_a / sig_sum # diag = 1 automatically
|
|
175
|
+
dL_dT_V = H_V_sym * coeff
|
|
176
|
+
dJ = dJ + R @ V @ dL_dT_V @ V.mH
|
|
177
|
+
|
|
178
|
+
return dJ, None # None for eps (non-tensor argument)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def polar_decomposition(J, eps=0.0):
|
|
182
|
+
"""
|
|
183
|
+
Compute polar decomposition J = R @ S with stable gradients.
|
|
184
|
+
|
|
185
|
+
This uses the det(R) = +1 convention, so R is always a proper rotation
|
|
186
|
+
in SO(n). If det(J) < 0, the reflection is absorbed into S, which will
|
|
187
|
+
then have one negative eigenvalue.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
J: (..., n, n) tensor, must be invertible
|
|
191
|
+
eps: clamp |sigma_a + sigma_b| to be at least eps in the derivative.
|
|
192
|
+
Only needed when det(J) < 0 and two singular values are nearly
|
|
193
|
+
equal. Introduces gradient bias but prevents explosion.
|
|
194
|
+
Default 0.0 (no clamping; exact gradients).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
R: (..., n, n) proper rotation (det = +1)
|
|
198
|
+
S: (..., n, n) symmetric (positive definite iff det(J) > 0)
|
|
199
|
+
sigma: (..., n) signed singular values (intermediate, for backward/jvp)
|
|
200
|
+
flip: (..., n) sign of the determinant of R
|
|
201
|
+
"""
|
|
202
|
+
R, S, _, _, flip = PolarDecomposition.apply(J, eps)
|
|
203
|
+
return R, S, flip
|
|
204
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
import torch
|
|
3
|
+
from . import __polar_decomposition
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def arap_energy(f: torch.nn.Module, samples: torch.Tensor, l_bend: float, bending_type: Literal['Chao', 'SR'] = 'Chao') -> torch.Tensor:
|
|
7
|
+
"""
|
|
8
|
+
Computes the mean ARAP regularization energy on a batch of sample points.
|
|
9
|
+
See Sec. 3.2 in the paper and demo/surface_modeling.ipynb for an example.
|
|
10
|
+
#TODO maybe not return values as mean but tensor and also return axiliaries J, R, etc.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
f: Deformation ``f(x) -> y`` as torch.nn.Module takes the samples
|
|
14
|
+
as input and returns the deformed samples as tensor with the same shape.
|
|
15
|
+
samples: Tensor of shape ``(B, d)`` containing surface points.
|
|
16
|
+
l_bend: Scalar weight for the bending term.
|
|
17
|
+
bending_type: Bending model:
|
|
18
|
+
- ``"Chao"``: use the bending energy from Chao et al. (2010).
|
|
19
|
+
- ``"SR"``: use the bending energy from Levi and Gotsman (2014).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Scalar tensor containing the batch-averaged ARAP energy.
|
|
23
|
+
|
|
24
|
+
Notes:
|
|
25
|
+
- Uses Jacobians from ``torch.func.jacrev`` and batch evaluation via
|
|
26
|
+
``torch.func.vmap``.
|
|
27
|
+
"""
|
|
28
|
+
d = samples.size(-1) # dimension
|
|
29
|
+
|
|
30
|
+
def deformation(x: torch.Tensor) -> torch.Tensor:
|
|
31
|
+
y = f(x)
|
|
32
|
+
return y, y
|
|
33
|
+
samples_jac, samples_deformed = torch.func.vmap(torch.func.jacrev(deformation, has_aux=True))(samples)
|
|
34
|
+
singular_values = torch.linalg.svdvals(samples_jac)
|
|
35
|
+
|
|
36
|
+
def rotation(samples: torch.Tensor) -> torch.Tensor:
|
|
37
|
+
J = torch.func.jacrev(lambda x: f(x))(samples)
|
|
38
|
+
R, Y, flip = __polar_decomposition.polar_decomposition(J)
|
|
39
|
+
return R, (R, Y, flip)
|
|
40
|
+
dR, (R, Y, flip) = torch.func.vmap(torch.func.jacrev(lambda x: rotation(x), has_aux=True), randomness='different')(samples)
|
|
41
|
+
|
|
42
|
+
# stretch energy
|
|
43
|
+
arap_stretch_loss = (singular_values * flip.detach() - 1).pow(2).mean()
|
|
44
|
+
|
|
45
|
+
# bending energy
|
|
46
|
+
if bending_type == 'Chao':
|
|
47
|
+
arap_bend_loss = (dR * torch.einsum('buvx,bvw->buwx', dR, Y)).mean() * d**3
|
|
48
|
+
elif bending_type == 'SR':
|
|
49
|
+
arap_bend_loss = dR.pow(2).mean() * d**3
|
|
50
|
+
return arap_stretch_loss + l_bend * arap_bend_loss
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This code is taken from Ling et al. 2025 - Uniform Sampling of Surfaces by Casting Rays
|
|
3
|
+
and modified e.g. to work with other dimensions, dtypes, devices as well as removed class structure.
|
|
4
|
+
'''
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
def sphere_trace_modified(origin: torch.Tensor, direction: torch.Tensor, sdf_fn, max_t, dim=3, eps=1e-4, step_bound=10, dtype=torch.float, device='cuda'):
|
|
10
|
+
all_pts = []
|
|
11
|
+
all_pts_hit = []
|
|
12
|
+
sdf_cost = 0
|
|
13
|
+
|
|
14
|
+
origin = origin.to(dtype=dtype, device=device)
|
|
15
|
+
direction = direction.to(dtype=dtype, device=device)
|
|
16
|
+
|
|
17
|
+
p = origin + 0*direction
|
|
18
|
+
d = sdf_fn(p).squeeze(-1)
|
|
19
|
+
t = torch.zeros(d.shape, dtype=dtype, device=device)
|
|
20
|
+
|
|
21
|
+
iter = 0
|
|
22
|
+
delta = torch.abs(d)
|
|
23
|
+
not_converged = (t < max_t) # march every ray till the end
|
|
24
|
+
while iter < 2000 and torch.any(not_converged):
|
|
25
|
+
|
|
26
|
+
# add samples near surface and step over the surface for these samples
|
|
27
|
+
p_near_surface = (t < max_t) & (delta < eps)
|
|
28
|
+
if torch.any(p_near_surface):
|
|
29
|
+
p_hits = torch.ones_like(p)
|
|
30
|
+
p_hits[p_near_surface] = p[p_near_surface]
|
|
31
|
+
all_pts.append(p_hits)
|
|
32
|
+
all_pts_hit.append(p_near_surface)
|
|
33
|
+
|
|
34
|
+
# gather samples near surface
|
|
35
|
+
p_near = p[p_near_surface]
|
|
36
|
+
delta_near = delta[p_near_surface]
|
|
37
|
+
t_near = t[p_near_surface]
|
|
38
|
+
max_t_near = max_t[p_near_surface]
|
|
39
|
+
directions_near = direction[p_near_surface]
|
|
40
|
+
|
|
41
|
+
# p_near_before = p_near.clone()
|
|
42
|
+
# Keep stepping until delta > eps
|
|
43
|
+
not_flipped = (t_near < max_t_near) & (delta_near < eps)
|
|
44
|
+
while torch.any(not_flipped):
|
|
45
|
+
|
|
46
|
+
# Take a step for the samples not flipped yet
|
|
47
|
+
p_near_not_flipped = p_near[not_flipped]
|
|
48
|
+
delta_near_not_flipped = delta_near[not_flipped]
|
|
49
|
+
t_near_not_flipped = t_near[not_flipped]
|
|
50
|
+
|
|
51
|
+
# Take a step of size max( delta_near_not_flipped, eps/2 ) so that it doesn't get stuck with too small a step.
|
|
52
|
+
p_near_not_flipped = p_near_not_flipped + torch.maximum(delta_near_not_flipped[:,None], torch.ones_like(delta_near_not_flipped)[:,None] * eps/2.0) * directions_near[not_flipped]
|
|
53
|
+
t_near_not_flipped = t_near_not_flipped + torch.maximum(delta_near_not_flipped, torch.ones_like(delta_near_not_flipped) * eps/2.0)
|
|
54
|
+
|
|
55
|
+
# Measure distance at new location
|
|
56
|
+
with torch.no_grad():
|
|
57
|
+
delta_near_not_flipped = sdf_fn(p_near_not_flipped).squeeze(-1)
|
|
58
|
+
sdf_cost += p_near_not_flipped.shape[0]
|
|
59
|
+
delta_near_not_flipped = torch.abs(delta_near_not_flipped)
|
|
60
|
+
|
|
61
|
+
# Copy back to these near surface samples
|
|
62
|
+
p_near[not_flipped] = p_near_not_flipped
|
|
63
|
+
t_near[not_flipped] = t_near_not_flipped
|
|
64
|
+
delta_near[not_flipped] = delta_near_not_flipped
|
|
65
|
+
|
|
66
|
+
# Evaluate whether every sample is flipped
|
|
67
|
+
not_flipped = (t_near < max_t_near) & (delta_near < eps)
|
|
68
|
+
|
|
69
|
+
# after flipping these samples to the other side of the surface, put those samples back with updated t and delta
|
|
70
|
+
p[p_near_surface] = p_near
|
|
71
|
+
delta[p_near_surface] = delta_near
|
|
72
|
+
t[p_near_surface] = t_near
|
|
73
|
+
|
|
74
|
+
nc_far = not_converged
|
|
75
|
+
p_far = p[nc_far,:]
|
|
76
|
+
d_far = d[nc_far]
|
|
77
|
+
delta_far = delta[nc_far]
|
|
78
|
+
t_far = t[nc_far]
|
|
79
|
+
|
|
80
|
+
# Take a step
|
|
81
|
+
p_far = p_far + delta_far[:,None]/step_bound * direction[nc_far,:]
|
|
82
|
+
t_far = t_far + delta_far/step_bound
|
|
83
|
+
|
|
84
|
+
# Measure distance at new location
|
|
85
|
+
with torch.no_grad():
|
|
86
|
+
d_far = sdf_fn(p_far).squeeze(-1)
|
|
87
|
+
|
|
88
|
+
sdf_cost += p_far.shape[0]
|
|
89
|
+
delta_far = torch.abs(d_far)
|
|
90
|
+
|
|
91
|
+
# Copy back to original tensors
|
|
92
|
+
p[nc_far,:] = p_far
|
|
93
|
+
d[nc_far] = d_far
|
|
94
|
+
delta[nc_far] = delta_far
|
|
95
|
+
t[nc_far] = t_far
|
|
96
|
+
|
|
97
|
+
iter += 1
|
|
98
|
+
not_converged = t < max_t
|
|
99
|
+
|
|
100
|
+
if len(all_pts) > 0:
|
|
101
|
+
all_pts = torch.stack(all_pts, dim=-2) # 5000, 480, 3
|
|
102
|
+
all_pts_hit = torch.stack(all_pts_hit, dim=-1) # 5000, 480
|
|
103
|
+
pts = all_pts.view(-1,dim)[all_pts_hit.view(-1)]
|
|
104
|
+
return pts, sdf_cost
|
|
105
|
+
else:
|
|
106
|
+
return torch.zeros((0,dim), dtype=torch.float, device=device), sdf_cost
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sample_rays(num_rays, dim=3):
|
|
110
|
+
# sample line directions
|
|
111
|
+
dirs = np.random.randn(num_rays,dim)
|
|
112
|
+
dirs = dirs / np.linalg.norm(dirs,axis=-1)[:,None] #[n,dim]
|
|
113
|
+
dirs = dirs[:,None,:] #[n,1,3]
|
|
114
|
+
|
|
115
|
+
# find the other normal and binormal basis
|
|
116
|
+
_,_,V = np.linalg.svd(dirs)
|
|
117
|
+
E = V[:,:,1:] #[n,3,2]
|
|
118
|
+
|
|
119
|
+
# sample random offsets in normal and binormal directions
|
|
120
|
+
dirs = dirs[:,0,:]
|
|
121
|
+
U = np.sqrt(dim) * ( np.random.uniform(0,1,(num_rays,1,dim-1)) * 2.0 - 1.0 )
|
|
122
|
+
O = np.sum(U * E, axis=-1)
|
|
123
|
+
O = O + dirs * np.sqrt(dim)
|
|
124
|
+
|
|
125
|
+
# determine if the ray O+D*t intersects the [-1,1]^dim hypercube
|
|
126
|
+
# using Slab method
|
|
127
|
+
t_low = np.zeros((num_rays,dim))
|
|
128
|
+
t_high = np.zeros((num_rays,dim))
|
|
129
|
+
t_low = (-1.0 - O)/dirs
|
|
130
|
+
t_high = (1.0 - O)/dirs
|
|
131
|
+
|
|
132
|
+
t_close = np.minimum(t_low, t_high)
|
|
133
|
+
t_far = np.maximum(t_low,t_high)
|
|
134
|
+
t_close = np.max(t_close, axis=-1)
|
|
135
|
+
t_far = np.min(t_far,axis=-1)
|
|
136
|
+
t = np.stack([t_close, t_far],axis=-1)
|
|
137
|
+
keep = t_close < t_far
|
|
138
|
+
|
|
139
|
+
dirs = dirs[keep]
|
|
140
|
+
O = O[keep]
|
|
141
|
+
t_close = t_close[keep]
|
|
142
|
+
t_far = t_far[keep]
|
|
143
|
+
O = O + dirs * t_close[:,None]
|
|
144
|
+
T = t_far - t_close
|
|
145
|
+
n_current = O.shape[0]
|
|
146
|
+
|
|
147
|
+
if (not np.any(keep)) and n_current > num_rays:
|
|
148
|
+
return np.zeros((0,3)),np.zeros((0,3)),np.zeros((0))
|
|
149
|
+
elif n_current == num_rays:
|
|
150
|
+
return O, dirs, T
|
|
151
|
+
elif n_current < num_rays:
|
|
152
|
+
O_current, D_current,T_current = sample_rays(num_rays-n_current, dim=dim)
|
|
153
|
+
O = np.concatenate([O,O_current],axis=0)
|
|
154
|
+
D = np.concatenate([dirs,D_current],axis=0)
|
|
155
|
+
T = np.concatenate([T,T_current],axis=0)
|
|
156
|
+
return O, D, T
|
|
157
|
+
|
|
158
|
+
def uniform_sample(sdf_func, num_rays, dim=3, thresh=1e-4, dtype=torch.float, device='cuda'):
|
|
159
|
+
lines_origins, lines_dirs, max_ts = sample_rays(num_rays, dim=dim)
|
|
160
|
+
lines_origins = torch.from_numpy(lines_origins).to(dtype=dtype, device=device)
|
|
161
|
+
lines_dirs = torch.from_numpy(lines_dirs).to(dtype=dtype, device=device)
|
|
162
|
+
pts, _ = sphere_trace_modified(lines_origins,
|
|
163
|
+
lines_dirs,
|
|
164
|
+
sdf_fn=sdf_func,
|
|
165
|
+
max_t=torch.from_numpy(max_ts).to(dtype=dtype, device=device),
|
|
166
|
+
dim=dim,
|
|
167
|
+
eps=thresh,
|
|
168
|
+
dtype=dtype,
|
|
169
|
+
device=device
|
|
170
|
+
)
|
|
171
|
+
return pts
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iarap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ARAP regularization for implicit surfaces
|
|
5
|
+
Author-email: Tobias Djuren <t.djuren@tu-berlin.de>, Markus Worchel <m.worchel@tu-berlin.de>, Ugo Finnendahl <finnendahl@tu-berlin.de>, Marc Alexa <marc.alexa@tu-berlin.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: licence.txt
|
|
11
|
+
Requires-Dist: torch<2.12,>=2.11
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ARAP-Regularization-for-Implicit-Surfaces
|
|
18
|
+
|
|
19
|
+
This is the source code for the paper [As-Rigid-As-Possible Regularization for Implicit Surfaces](https://diglib.eg.org/items/de832211-9ab2-431d-bb2b-cde770a5b5ca), SGP 2026.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
The only dependency is [pytorch](https://pytorch.org/) (tested with torch 2.11.0 with CUDA 12.8). For the demos that generate results similar to those in the paper (implict surface modeling and Gauss Stylization) further dependencies are required (see `demos/requirements.txt`).
|
|
24
|
+
|
|
25
|
+
````powershell
|
|
26
|
+
python -m venv .env
|
|
27
|
+
.\.venv\Scripts\Activate.ps1 # depends on your os
|
|
28
|
+
pip install git+https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces.git
|
|
29
|
+
````
|
|
30
|
+
|
|
31
|
+
## Usage (iarap.arap_energy)
|
|
32
|
+
`iarap.arap_energy` computes the batch-averaged ARAP regularization energy ($\mathcal{L}_{ARAP}$) for a deformation model $\boldsymbol f$.
|
|
33
|
+
|
|
34
|
+
* Input:
|
|
35
|
+
|
|
36
|
+
`f`: deformation function as `torch.nn.Module` $(B, d) \rightarrow (B, d)$
|
|
37
|
+
|
|
38
|
+
`samples`: points of shape $(B, d)$ (tested with $d=2$ and $d=3$)
|
|
39
|
+
|
|
40
|
+
`l_bend`: bending weight ($\lambda_{bend}$)
|
|
41
|
+
* Output:
|
|
42
|
+
scalar tensor (mean ARAP energy)
|
|
43
|
+
|
|
44
|
+
Minimal example:
|
|
45
|
+
|
|
46
|
+
````python
|
|
47
|
+
import torch
|
|
48
|
+
import iarap
|
|
49
|
+
|
|
50
|
+
class Identity(torch.nn.Module):
|
|
51
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
52
|
+
return x
|
|
53
|
+
|
|
54
|
+
f = Identity()
|
|
55
|
+
samples = torch.randn(1024, 3, device="cuda" if torch.cuda.is_available() else "cpu")
|
|
56
|
+
loss = iarap.arap_energy(f, samples, l_bend=0.1)
|
|
57
|
+
print(loss.item())
|
|
58
|
+
````
|
|
59
|
+
|
|
60
|
+
## Demos
|
|
61
|
+
|
|
62
|
+
Please see `./demos/surface_modeling.ipynb` to see how ARAP regularization can be used to model implicit surfaces with user defined handles. In `./demos/gauss_stylization.ipynb` you will find the implict variant of [Gauss Stylization](https://github.com/ugogon/gaussStylization) as described in the paper.
|
|
63
|
+
|
|
64
|
+
There are multiple pretrained signed distance functions in `./demos/sdfs`. Please note that some sdfs are based on meshes that are under CC-BY.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Citation
|
|
68
|
+
|
|
69
|
+
If you use the repository, please cite
|
|
70
|
+
```
|
|
71
|
+
@article{10.1111:cgf.70519,
|
|
72
|
+
journal = {Computer Graphics Forum},
|
|
73
|
+
title = {{As-Rigid-As-Possible Regularization for Implicit Surfaces}},
|
|
74
|
+
author = {Djuren, Tobias and
|
|
75
|
+
Worchel, Markus and
|
|
76
|
+
Finnendahl, Ugo and
|
|
77
|
+
Alexa, Marc},
|
|
78
|
+
year = {2026},
|
|
79
|
+
publisher = {The Eurographics Association and John Wiley & Sons Ltd.},
|
|
80
|
+
ISSN = {1467-8659},
|
|
81
|
+
DOI = {10.1111/cgf.70519}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
licence.txt
|
|
3
|
+
pyproject.toml
|
|
4
|
+
iarap/__init__.py
|
|
5
|
+
iarap/__polar_decomposition.py
|
|
6
|
+
iarap/energy.py
|
|
7
|
+
iarap/sampling.py
|
|
8
|
+
iarap.egg-info/PKG-INFO
|
|
9
|
+
iarap.egg-info/SOURCES.txt
|
|
10
|
+
iarap.egg-info/dependency_links.txt
|
|
11
|
+
iarap.egg-info/requires.txt
|
|
12
|
+
iarap.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
torch<2.12,>=2.11
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
iarap
|
iarap-0.1.0/licence.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iarap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Tobias Djuren", email="t.djuren@tu-berlin.de" },
|
|
10
|
+
{ name="Markus Worchel", email="m.worchel@tu-berlin.de" },
|
|
11
|
+
{ name="Ugo Finnendahl", email="finnendahl@tu-berlin.de" },
|
|
12
|
+
{ name="Marc Alexa", email="marc.alexa@tu-berlin.de" }
|
|
13
|
+
]
|
|
14
|
+
description = "ARAP regularization for implicit surfaces"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.9"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"torch>=2.11,<2.12",
|
|
19
|
+
]
|
|
20
|
+
license = "MIT"
|
|
21
|
+
license-files = ["LICEN[CS]E*"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://gitlab.com/tobidju/arap-regularization-for-implicit-surfaces"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["iarap"]
|
iarap-0.1.0/setup.cfg
ADDED