parametrization
triwarp.parametrization
Flatten a mesh into the plane: harmonic, Tutte, ARAP and LSCM parametrizations.
Four maps, differing in what they hold fixed and what they minimize. The first three pin the
boundary and solve for the interior:
harmonic minimizes Dirichlet energy with cotangent weights,
tutte is the same solve with uniform weights (guaranteeing an
injective map for a convex boundary), and arap alternates local
rotation fits with a global solve to trade conformality for low area distortion.
lscm instead pins only two vertices and lets the boundary find its
own shape, minimizing conformal rather than Dirichlet energy.
map_vertices_to_circle supplies the boundary
condition the fixed-boundary three need, and
face_flipped_indices is the diagnostic that
says whether a result is actually injective. Ports of the corresponding igl:: routines; every
solve is conjugate-gradient.
face_flipped_mask
face_flipped_mask(
vertices: array[vec2], faces: array[int32]
) -> wp.array[wp.bool]
Per-face flag: whether a triangle is inverted (negative 2D signed area) in the parametrization.
For each triangle the 2D signed area of its three UV vertices is computed; a face is flagged
True when that area is strictly negative, i.e. the triangle has folded over (flipped
orientation) in the 2D domain. Mirrors libigl's flipped_triangles per-triangle test
(determinant of the homogeneous 3 x 3 vertex matrix < 0). Degenerate (zero-area)
triangles are not flagged, matching the strict < 0 comparison.
face_flipped_indices is the index form of
this mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec2]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Notes
Equivalent to the per-triangle predicate behind libigl flipped_triangles: the 2D cross
product (v1 - v0) x (v2 - v0) equals det([[x0, x1, x2], [y0, y1, y2], [1, 1, 1]]), so a
True entry corresponds exactly to a triangle libigl would list as flipped.
This is a different flip from
face_flip_mask, which is about the 3D winding: this
mask flags a triangle whose UV image has folded over, a property of the parametrization that
says nothing about the surface, while that one flags a triangle whose index order must be
reversed to agree with its patch. A mesh can be consistently wound and still have flipped UVs,
and vice versa.
Source code in triwarp/parametrization.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
face_flipped_indices
face_flipped_indices(
vertices: array[vec2], faces: array[int32]
) -> wp.array[wp.int32]
Return the indices of triangles inverted (negative 2D signed area) in the parametrization.
Convenience wrapper returning flatnonzero of
face_flipped_mask: the indices into faces
of triangles whose 2D signed area is strictly negative (folded over in the UV domain). Matches
libigl's flipped_triangles, which returns the same list of flipped-triangle indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec2]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
array[int32]
|
Ascending face indices of the flipped triangles on |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Source code in triwarp/parametrization.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
map_vertices_to_circle
map_vertices_to_circle(
vertices: array[vec3], boundary: array[int32]
) -> wp.array[wp.vec2]
Map an ordered boundary loop onto the unit circle by arc length.
Places boundary vertex i at angle 2*pi * len[i] / total, where len[i] is the
cumulative edge length from boundary[0] to boundary[i] along the loop and total is
the full perimeter (closing over the wrap edge). The result is in loop order: row i is the
position of boundary[i]. Feed it as boundary_uv to
tutte or harmonic for a
disk parametrization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
boundary
|
array[int32]
|
Ordered boundary-loop vertex indices, e.g. from
|
required |
Returns:
| Type | Description |
|---|---|
array[vec2]
|
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Notes
The name and the arc-length placement follow igl::map_vertices_to_circle.
See Also
Source code in triwarp/parametrization.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
harmonic
harmonic(
vertices: array[vec3],
faces: array[int32],
boundary_indices: array[int32],
boundary_uv: array[vec2],
k: int = 1,
) -> wp.array[wp.vec2]
Harmonic parametrization with fixed boundary.
Minimizes the k-harmonic energy built from the cotangent Laplacian
cotmatrix subject to the boundary vertices being pinned to
boundary_uv. For k == 1 this is the harmonic map (each interior UV is the
cotangent-weighted average of its neighbors); k == 2 is the biharmonic map, and so on. The
interior system is solved with conjugate gradient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
boundary_indices
|
array[int32]
|
|
required |
boundary_uv
|
array[vec2]
|
|
required |
k
|
int
|
Harmonic power ( |
1
|
Returns:
| Type | Description |
|---|---|
array[vec2]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
tutte
k_harmonic
The operator this minimizes, as opposed to this map -- the two are the only two
"harmonic" names in the package and they are not interchangeable.
cotmatrix
map_vertices_to_circle
Notes
Matches igl::harmonic, k included.
The interior system is solved iteratively (conjugate gradient) rather than by direct
factorization, with an automatic multigrid preconditioner
(multigrid_preconditioner) engaged for k >= 2,
since squaring the operator squares its condition number. For k == 1 a simple Jacobi
preconditioner is used instead.
Source code in triwarp/parametrization.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
tutte
tutte(
vertices: array[vec3],
faces: array[int32],
boundary_indices: array[int32],
boundary_uv: array[vec2],
k: int = 1,
) -> wp.array[wp.vec2]
Tutte embedding with fixed boundary (uniform-Laplacian parametrization).
Identical to harmonic except the operator is the
combinatorial graph_laplacian instead of the
cotangent one — for k == 1 that Laplacian is the only difference. Because the uniform
Laplacian's free-free block is a diagonally dominant M-matrix, the Tutte embedding of a mesh
with a convex boundary is guaranteed bijective (fold-free), unlike the harmonic/conformal maps.
For
k > 1 the mass matrix is the identity (matching libigl's speye graph-Laplacian variant).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
boundary_indices
|
array[int32]
|
|
required |
boundary_uv
|
array[vec2]
|
|
required |
k
|
int
|
Laplacian power ( |
1
|
Returns:
| Type | Description |
|---|---|
array[vec2]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/parametrization.py
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | |
arap
arap(
vertices: array[vec3],
faces: array[int32],
fixed_indices: array[int32],
fixed_uv: array[vec2],
uv_init: array[vec2],
max_iterations: int = 10,
tolerance: float = 1e-07,
) -> wp.array[wp.vec2]
As-rigid-as-possible (ARAP) parametrization with fixed vertices.
Minimizes the ARAP energy of the 2D parametrization by local/global alternation, starting from
uv_init and keeping fixed_indices pinned to fixed_uv at every iteration. The local
step fits, per triangle, the closest rotation between the isometrically flattened rest triangle
and its current UV image (closed-form 2D polar decomposition, reflections forbidden); the global
step solves the cotangent-Laplacian Poisson system (-L)_uu U_u = (K R)_u - (-L)_ub bc for
each UV column with conjugate gradient.
A good uv_init matters: ARAP is non-convex, so feed a fold-free initial map such as
harmonic or tutte, with
the boundary placed by
map_vertices_to_circle. Inspect the result
for inverted triangles with
face_flipped_indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
fixed_indices
|
array[int32]
|
|
required |
fixed_uv
|
array[vec2]
|
|
required |
uv_init
|
array[vec2]
|
|
required |
max_iterations
|
int
|
Number of local/global iterations ( |
10
|
tolerance
|
float
|
Relative residual tolerance of the inner conjugate-gradient solve ( |
1e-07
|
Returns:
| Type | Description |
|---|---|
array[vec2]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Notes
Uses the elements ARAP energy (one rotation per triangle), libigl's default for the flat
dim = 2 parametrization case; the covariance scatter is built per corner of the flattened
mesh, which is why the SPOKES / SPOKES_AND_RIMS (per-vertex) energies do not apply here.
The half-cotangent weights c_e come from
cotmatrix_entries with no clamping (matching libigl),
and the closest 2D rotation is the closed form theta = atan2(S10 - S01, S00 + S11) of
igl::fit_rotations_planar (libigl's scale-invariant S /= max|S| normalization is
unnecessary for atan2 and is skipped). The cotangent operator and the two conjugate-gradient
solves run in float64 for determinism while the UV field is stored float32 between
iterations (module convention); the ~1e-7/iteration drift is well under the pinned-boundary
tolerance for the default iteration count.
Why tolerance defaults to 1e-7 and not 1e-8. Unlike
harmonic or lscm, whose
single solve is the answer, ARAP's global solves are inner steps of a truncated outer
iteration, so solving more accurately than the outer iteration's own truncation error wastes
work without changing the result. Pass tolerance=1e-8 for stricter per-iteration solves;
going looser than the default risks the inner error exceeding the outer truncation error on
large meshes, which is why 1e-7 rather than something looser is the default.
Source code in triwarp/parametrization.py
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 | |
lscm
lscm(
vertices: array[vec3],
faces: array[int32],
pinned_indices: array[int32],
pinned_uv: array[vec2],
) -> wp.array[wp.vec2]
Constrained least-squares conformal map.
Computes the conformal (angle-preserving) parametrization that minimizes the LSCM (Levy)
conformal energy subject to a set of pinned vertices, by solving a single quadratic program
over the stacked [u; v] vector of 2 * n_vertices unknowns with the LSCM Hessian
lscm_hessian as the operator. Unlike
harmonic / tutte (which
pin the whole boundary and solve two independent columns), LSCM couples u and v through
the boundary vector-area term, so it needs only a few pins — typically two — to fix the
remaining similarity-transform (rotation + scale + translation) degree of freedom.
The interior system is solved with conjugate gradient. Closed meshes are valid input: the
boundary vector-area matrix is then zero and the
Hessian reduces to -repdiag(L, 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
pinned_indices
|
array[int32]
|
|
required |
pinned_uv
|
array[vec2]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[vec2]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than two vertices are pinned (and the mesh has at least two vertices), or if
|
RuntimeError
|
If |
Notes
The unknowns are stacked [u; v] (all u DOFs then all v DOFs), matching igl's
lscm: pin i fixes DOF i (u) and DOF i + n_vertices (v). The returned
Q of igl.lscm equals -repdiag(L, 2) - 2 A exactly (see
lscm_hessian).
Source code in triwarp/parametrization.py
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 | |