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 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
+ ![](/assets/teaser.jpg)
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
+ ![](/assets/teaser.jpg)
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,3 @@
1
+ from . import sampling
2
+ from .energy import arap_energy
3
+ __all__ = ['arap_energy', 'sampling']
@@ -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
+ ![](/assets/teaser.jpg)
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
+ torch<2.12,>=2.11
@@ -0,0 +1 @@
1
+ iarap
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+