points
triwarp.points
Unstructured point-cloud geometry: fitting, second moments and normal estimation.
No connectivity here -- everything takes a bare (n,) array of positions. Two groups:
- Whole-cloud fits.
centroid,covarianceandcentered_covariancegive the second moments;fit_lineandfit_planeread the dominant and weakest eigenvector off them.gram_matrixis the uncentered form, for callers that want to center differently. - Per-point.
estimate_normalsfits a plane to each point's k-nearest neighbourhood, which is how an unoriented cloud acquires normals before reconstruction.outlier_probabilityandstatistical_outlier_maskscore the same neighbourhood for isolation, which is how a scanned cloud loses its stragglers before the normals are fitted.plane_basisandradial_sortthen let a caller work in the tangent plane it defines. -
Cleanup and subsampling. Four masks and one selector, all returning indices or
wp.array[wp.bool]rather than a copied cloud, so a caller pays for the gather only if it wants one:point_finite_maskandpoint_duplicate_maskare the two exact predicates,radius_outlier_maskandstatistical_outlier_maskthe two density ones (an absolute floor and a cloud-relative threshold), andfarthest_point_samplepicks an exact count spread over the cloud's support. Run the exact predicates first: they are cheap and they remove the inputs the others are ill-defined on. -
Approximate hull vertices. Which points are convex-hull vertices, without building a hull. Both entry points are exact-hull-free and run in a fixed number of parallel launches, and both are one-sided -- but in opposite directions, which is what the names say:
convex_subset_mask(and its point-returning formconvex_subset) accumulates positive certificates: a direction a point is extremal along proves it is on the hull. Cheap and precise, but a hull vertex with no sampled certificate is dropped, so the result can be smaller than the hull-vertex set.convex_superset_maskaccumulates negative certificates: a tetrahedron of hull points strictly containing a point proves that point is interior. It keeps everything it cannot rule out, so the result is never smaller than the hull-vertex set -- a conservative filter, suitable as a prefilter before an exact hull.
Neither computes hull connectivity; for that use an exact hull library
(scipy.spatial.ConvexHull or the qhull-backed mesh packages).
Normals from estimate_normals are unoriented -- a plane fit
cannot pick a side. See triwarp.repair for orientation propagation.
One member here takes no cloud at all: vector_angle is the
unsigned angle between two wp.vec3 arrays. It is geometry rather than array structure, so
triwarp.array ("NumPy-style structural and elementwise ops") is no better a
home, and there is no third candidate -- so it stays, and this sentence is why.
SUPPORT_TIE_SLACK
module-attribute
SUPPORT_TIE_SLACK = wp.constant(wp.float32(1e-06))
TETRAHEDRON_FLATNESS
module-attribute
TETRAHEDRON_FLATNESS = wp.constant(wp.float32(0.001))
point_plane_distance
point_plane_distance(
points: array[vec3],
plane_normal: vec3,
plane_origin: vec3 | None = None,
) -> wp.array[wp.float32]
Signed perpendicular distance of each point to a plane.
The sign follows plane_normal: positive on the side it points to, negative on the other,
zero on the plane. An unsigned distance is wp.map(wp.abs, ...) over the result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
plane_normal
|
vec3
|
Plane normal vector as |
required |
plane_origin
|
vec3 | None
|
Point on the plane as |
None
|
Returns:
| Type | Description |
|---|---|
array[float32]
|
Length |
Source code in triwarp/points.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
half_space_mask
half_space_mask(
points: array[vec3],
plane_normal: vec3,
plane_origin: vec3 | None = None,
) -> wp.array[wp.bool]
Flag the points strictly on the normal's side of a plane.
The unbounded selection primitive: one plane cuts space in two and this is the half the normal points into. Composes into a convex region by intersecting several masks, which is what makes it worth having next to the box query — a slab, a wedge or a frustum is a handful of these, and none of them needs a spatial index because every point is tested independently.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
plane_normal
|
vec3
|
Plane normal as |
required |
plane_origin
|
vec3 | None
|
Point on the plane as |
None
|
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Examples:
upper = tw.points.half_space_mask(v, wp.vec3(0.0, 0.0, 1.0))
kept = tw.array.gather(v, tw.array.flatnonzero(upper))
Notes
The test is strict, so a point exactly on the plane is excluded and the two masks for
opposite normals are disjoint rather than overlapping -- with the plane z = 1, a point at
z = 1 is in neither half -- and it makes the pair of masks a partition of the points off
the plane.
See Also
point_plane_distance
The signed distance this thresholds, when the magnitude is wanted too.
triwarp.neighbors.query_bvh_box
The bounded counterpart: selection by a box, through a BVH.
triwarp.array.flatnonzero
Source code in triwarp/points.py
124 125 126 127 128 129 130 131 132 133 134 135 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 | |
centroid
centroid(points: array[vec3]) -> wp.array[wp.vec3]
Mean position sum(points) / n as a (1,) device wp.vec3 array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Shape |
Source code in triwarp/points.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
gram_matrix
gram_matrix(points: array[vec3]) -> wp.array[wp.mat33]
Uncentered Gram (scatter) matrix G = sum_k outer(x_k, x_k).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[mat33]
|
Shape |
Source code in triwarp/points.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
fit_line
fit_line(points: array[vec3]) -> wp.vec3
Major axis of a point set, weighted across all three singular directions.
The result is normalize(S @ V) over the SVD of the uncentered point matrix -- a sum of
all three right singular vectors weighted by their singular values. It is not the first
principal axis, and the two
part company as soon as the cloud is neither strongly elongated nor centred on the origin:
measured against the leading eigenvector of the covariance, |dot| is 1.000 on a 1000:1
needle but 0.939 on a 3:1:0.2 cloud and 0.812 once that cloud is offset from the origin.
For the principal frame, use
principal_axes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
vec3
|
Unit vector along the weighted major axis. The result is direction-only: its sign is governed by the SVD convention and is not meaningful. |
Notes
This is the quantity trimesh.points.major_axis computes. Because the weighted sum mixes
all three singular vectors, its value depends on each one's sign, which no SVD fixes -- so the
two implementations agree to 1.000 where one singular value dominates and to 0.992 on a
moderate cloud.
See Also
principal_axes
The first principal axis, and the full frame, of the centred cloud.
fit_plane
The complementary weakest direction.
trimesh.points.major_axis
Source code in triwarp/points.py
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 278 279 280 281 282 283 284 285 286 | |
centered_covariance
centered_covariance(
points: array[vec3], center: array[vec3] | None = None
) -> wp.array[wp.mat33]
Centered scatter matrix C = sum_k outer(x_k - mu, x_k - mu) (no 1/n).
Centering happens inside the outer-product loop (rather than via the
sum(x x^T) - n mu mu^T identity) to avoid float32 catastrophic
cancellation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
center
|
array[vec3] | None
|
Optional precomputed centroid as a |
None
|
Returns:
| Type | Description |
|---|---|
array[mat33]
|
Shape |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Source code in triwarp/points.py
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 | |
fit_plane
fit_plane(points: array[vec3]) -> tuple[wp.vec3, wp.vec3]
Fit a plane to a point set using SVD.
The plane origin is the centroid of the points and the normal is the singular vector with the smallest singular value of the centered covariance matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
tuple[vec3, vec3]
|
|
See Also
principal_axes
All three axes at once; this normal is its third row.
plane_basis
Two in-plane axes completing this normal into a frame.
Source code in triwarp/points.py
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | |
plane_basis
plane_basis(normal: vec3) -> tuple[wp.vec3, wp.vec3]
Right-handed orthonormal basis (u, v) spanning the plane with the given normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
vec3
|
Plane normal as |
required |
Returns:
| Type | Description |
|---|---|
tuple[vec3, vec3]
|
|
Source code in triwarp/points.py
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | |
covariance
covariance(
points: array[vec3], ddof: int = 1
) -> wp.array[wp.mat33]
Sample covariance matrix (1 / (n - ddof)) sum_k outer(x_k - mu, x_k - mu).
Matches numpy.cov(points.T, ddof=ddof) for the default ddof=1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
ddof
|
int
|
Delta degrees of freedom; the divisor is |
1
|
Returns:
| Type | Description |
|---|---|
array[mat33]
|
Shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in triwarp/points.py
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | |
principal_axes
principal_axes(
points: array[vec3],
) -> tuple[wp.mat33, wp.vec3, wp.vec3]
Principal frame of a point cloud: its three axes, spreads and centroid.
The eigenvectors of the centred covariance, ordered widest first, as the rows of a proper
rotation -- so rotation * (p - centroid) are the coordinates of p in the principal
frame, and wp.transpose(rotation) maps back. This is the fit
fit_line and fit_plane each read one
direction of: the first row is the true major axis and the third is the plane normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
rotation |
mat33
|
Principal axes as rows, in descending order of spread. A proper rotation: orthonormal with
determinant |
eigenvalues |
vec3
|
The three spreads, descending. These are the eigenvalues of the scatter matrix
|
centroid |
vec3
|
Mean position, the origin of the frame. |
Notes
Matches pyvista.principal_axes up to the sign of the first two axes, which no
eigendecomposition fixes.
Examples:
rotation, eigenvalues, centroid = tw.points.principal_axes(v)
See Also
fit_line
A different estimator, and the reason this exists; see its docstring.
fit_plane
covariance
triwarp.bounds.oriented_bounding_box
A searched box rather than a covariance fit, and tighter for it.
Source code in triwarp/points.py
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 | |
estimate_normals
estimate_normals(
points: array[vec3],
neighbor_idx: Array2dInt32,
*,
orient_reference: vec3 | None = None,
camera_location: vec3 | None = None,
) -> wp.array[wp.vec3]
Estimate per-point normals by PCA over each point's neighbourhood.
Each normal is the eigenvector of the smallest eigenvalue of the local
covariance matrix accumulated over the point's neighbours — the standard
choice, which Open3D's FastEigen3x3 also makes, so the result matches the
references up to sign. The neighbourhood is supplied by
the caller as neighbor_idx: build it with
query_nearest using a plain
k for a k-nearest (KNN) neighbourhood, or with max_radius set for a
radius-bounded (hybrid) neighbourhood — mirroring the two neighbour modes of
Open3D's estimate_normals(max_nn, radius).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
neighbor_idx
|
Array2dInt32
|
|
required |
orient_reference
|
vec3 | None
|
When given, normals are flipped to align with this fixed direction
( |
None
|
camera_location
|
vec3 | None
|
When given, normals are flipped to point toward this location
( |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length |
Raises:
| Type | Description |
|---|---|
ValueError
|
If both |
RuntimeError
|
If |
Source code in triwarp/points.py
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 | |
outlier_probability
outlier_probability(
neighbor_idx: Array2dInt32,
neighbor_distance: Array2dFloat32,
*,
scale: float = 3.0,
) -> wp.array[wp.float32]
Local Outlier Probability (LoOP) of each point, in [0, 1].
A point is an outlier to the extent that its own neighbourhood is stretched relative to the
neighbourhoods of the points in it. Following Kriegel et al., each point gets a "standard
distance" sigma (the RMS distance to its neighbours), a local factor
plof = sigma / mean(sigma over the neighbourhood) - 1, and a probability
max(0, erf(plof / (nplof * sqrt(2)))) where nplof = scale * sqrt(mean(plof^2)) over the
whole cloud. Because the normalization is cloud-wide, the score is comparable across points but
not across clouds — it is a rank, calibrated so that a threshold near 0.8 selects the
tail.
This is the measure behind MeshLab's compute_selection_point_cloud_outliers, whose own
propthreshold default is 0.8; threshold the result to reproduce a selection:
from triwarp.kernels import array as kernel_array
probability = tw.points.outlier_probability(neighbor_idx, neighbor_distance)
outlier_mask = wp.empty(probability.shape, dtype=wp.bool, device=probability.device)
wp.map(kernel_array.greater, probability, wp.float32(0.8), out=outlier_mask)
outliers = tw.array.flatnonzero(outlier_mask)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
neighbor_idx
|
Array2dInt32
|
|
required |
neighbor_distance
|
Array2dFloat32
|
|
required |
scale
|
float
|
The LoOP normalization factor |
3.0
|
Returns:
| Type | Description |
|---|---|
array[float32]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/points.py
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 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 | |
statistical_outlier_mask
statistical_outlier_mask(
neighbor_distance: Array2dFloat32,
*,
std_ratio: float = 2.0,
) -> wp.array[wp.bool]
Flag points whose mean neighbour distance exceeds mean + std_ratio * std over the cloud.
Open3D's remove_statistical_outlier criterion, and the cheaper cousin of
outlier_probability: one global threshold on a single
per-point statistic rather than a neighbourhood-relative one. It is the right choice for a
cloud of roughly uniform density and the wrong one for a cloud that is dense in places and
sparse in others, where the sparse region is entirely above a global threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
neighbor_distance
|
Array2dFloat32
|
|
required |
std_ratio
|
float
|
Multiplier on the cloud-wide standard deviation of the per-point means. Lower is stricter.
Open3D's own examples use |
2.0
|
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in triwarp/points.py
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 806 807 808 809 | |
radius_outlier_mask
radius_outlier_mask(
points: array[vec3],
radius: float,
min_neighbors: int,
*,
grid: HashGrid | None = None,
) -> wp.array[wp.bool]
Flag points with at most min_neighbors other points within radius.
Open3D's remove_radius_outlier criterion, and the local counterpart of
statistical_outlier_mask: an absolute density
floor rather than a cloud-wide threshold on a per-point statistic. That makes it the right
choice where the density is known in advance (a scanner's nominal spacing) and the wrong one
where it varies across the cloud, which is the trade the two run in opposite directions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
radius
|
float
|
Search radius, inclusive at exactly |
required |
min_neighbors
|
int
|
A point survives when it has strictly more than this many neighbours, counting
itself — Open3D's |
required |
grid
|
HashGrid | None
|
Optional pre-built hash grid over |
None
|
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Notes
The counts are exact, and the rule is the reference's — but Open3D's own
remove_radius_outlier shares one KDTreeFlann across an OpenMP loop and its radius search
is not thread-safe under that sharing, so it returns a different answer run to run: measured
three distinct keep sets (43 / 44 / 45 points) over eight repetitions of one 500-point cloud,
differing by one or two points each time. Querying the same tree serially reproduces this
function exactly on that cloud. Expect a comparison against the filter to disagree on a handful
of borderline points, and do not read that as a difference in the criterion.
Examples:
mask = tw.points.radius_outlier_mask(v, 1.0, 3)
outlier_indices = tw.array.flatnonzero(mask)
Source code in triwarp/points.py
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 | |
point_finite_mask
point_finite_mask(points: array[vec3]) -> wp.array[wp.bool]
Flag points whose three coordinates are all finite.
Open3D's remove_non_finite_points predicate. A single NaN or infinity poisons every
reduction the point enters — a bounding box, a covariance, a BVH build — so this is the first
pass over a cloud read off a scanner, before any of the fits in this module.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Examples:
finite = tw.points.point_finite_mask(v)
cleaned = tw.array.gather(v, tw.array.flatnonzero(finite))
See Also
point_duplicate_mask
The other exact-predicate cleanup pass; run this one first, since a NaN position is
never equal to itself under Open3D's rule.
triwarp.array.flatnonzero
Source code in triwarp/points.py
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 | |
point_duplicate_mask
point_duplicate_mask(
points: array[vec3],
) -> wp.array[wp.bool]
Flag every point that repeats an exactly equal position seen earlier in the cloud.
Open3D's remove_duplicated_points rule: bit-for-bit coordinate equality, keeping the
first occurrence of each distinct position. This is deliberately not
triwarp.grouping.unique_rows, whose wp.vec3 key buckets
each coordinate to about 2.4e-4 relative and so merges positions that are merely close;
reach for that one when a tolerance is what you want, and for this one when equality is.
Two injective 64-bit key rounds do it: the x and y bit patterns pack side by side into
one int64, that key's equivalence class packs against the z bits, and the second class
identifies the position exactly. Each half is exactly 32 bits, so neither round can collide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Notes
-0.0 and +0.0 merge, because IEEE-754 equality holds between them and the reference
compares with ==; the packing folds the two bit patterns together to reproduce that.
NaN is the one case that does not match the reference: NaN != NaN makes every
NaN row its own class for Open3D, where a bit pattern is a bit pattern here and identical
NaN rows merge. Run point_finite_mask first and the
question does not arise. Positions are compared at float32, so two points that differ only
below float32 resolution are one position here and two for a float64 reference.
Examples:
duplicates = tw.points.point_duplicate_mask(v)
n_distinct = int(v.shape[0]) - int(tw.reduce.sum(duplicates))
See Also
point_finite_mask
triwarp.grouping.unique_rows
The tolerance-bucketed sibling, and the right choice when exact equality is too strict.
triwarp.grouping.first_occurrence_indices
Source code in triwarp/points.py
957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 | |
farthest_point_sample
farthest_point_sample(
points: array[vec3], count: int, *, start: int = 0
) -> wp.array[wp.int32]
Greedily pick count points, each as far as possible from those already picked.
Open3D's farthest_point_down_sample: the classic maximin subsample, which spreads its
output over the cloud's support rather than its density — so unlike
triwarp.voxels.voxel_down_sample it returns an exact
count, and unlike a random subsample it cannot leave a hole. The greedy choice makes it
inherently sequential in count, and the whole sweep runs as one persistent block on the
device: each round folds the newest sample into every point's running distance and takes the
global arg-max as a block reduction, so nothing is launched or read back per round.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
count
|
int
|
Number of points to select; must satisfy |
required |
start
|
int
|
Index of the first sample, Open3D's |
0
|
Returns:
| Type | Description |
|---|---|
array[int32]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Ties are broken towards the lower index, matching the reference's strict > arg-max, and that
is what makes the answer reproducible: the per-iteration arg-max is a single wp.atomic_max
over a key that packs the squared distance against the complemented index, so the winner does
not depend on the order the atomics land in. Distances are compared in float32 where Open3D
uses float64, so a cloud with two points at nearly equal distance from the chosen set can
diverge from that reference at the point where the tie is resolved — and then, being greedy, for
the rest of the sequence.
Examples:
indices = tw.points.farthest_point_sample(v, 4)
spread = tw.array.gather(v, indices)
See Also
triwarp.sample.sample_surface_blue_noise
The same "well-spread points" goal from a mesh, by dart throwing rather than greedily.
triwarp.voxels.voxel_down_sample
triwarp.neighbors.nearest_neighbor_distance
Source code in triwarp/points.py
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 | |
convex_subset_mask
convex_subset_mask(
points: array[vec3],
n_directions: int = 128,
tolerance: float = 1e-06,
) -> wp.array[wp.bool]
Approximate the convex-hull vertices of a point cloud as a boolean mask.
For any direction n, the point maximizing ⟨n, p⟩ is a vertex of the
convex hull, and the point minimizing it is the hull vertex farthest along
-n. A single dot-product sweep over the points therefore yields the two
hull vertices supporting +n and -n. Directions are drawn on the
positive-z hemisphere with the deterministic Fibonacci spiral
(sample_fibonacci_hemisphere);
because the hemisphere and its reflection tile the full sphere, taking both
the max and min per direction covers all 2 * n_directions antipodal
orientations at half the dot-product cost of sampling the full sphere.
The extrema are computed by one thread per (direction, point slice), each reducing a
strided slice of ITEMS_PER_SLICE_CUDA points
(ITEMS_PER_SLICE_CPU on the CPU device) and committing one atomic, then a second pass marks
the maximizers and minimizers. At most roughly 2 * n_directions points (plus ties) can be
marked.
The result is an inner approximation, not a superset
Marking a point requires a certificate -- a sampled direction it is extremal
along -- so a hull vertex with no such direction is dropped. The mask is
therefore a subset of the hull boundary, never a conservative superset of the
hull vertices; see Notes for the exact guarantee and how to raise recall. When
losing a hull vertex is unacceptable, use
convex_superset_mask, which errs the
other way by construction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
n_directions
|
int
|
Number of Fibonacci hemisphere directions. Each covers two antipodal
orientations, so the effective coverage is |
128
|
tolerance
|
float
|
Relative slack on the support test; a point is marked when its dot product
with a direction is within |
1e-06
|
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Notes
What is guaranteed. Every marked point lies on the convex-hull boundary, to
within the tolerance slack. That is the invariant that holds for every input.
The stronger statement -- every marked point is a hull vertex -- holds only when
no support direction ties, which is the generic case for a cloud in general
position but fails on structured data: on a 5x5 grid over each face of a cube
(98 points, 8 hull vertices) the mask returns 14 points at any n_directions,
the 8 corners plus 6 face-edge midpoints that tie with a corner along a face
normal. Raising tolerance widens that effect deliberately.
What is not guaranteed. A hull vertex is recovered only when a sampled
direction falls inside its normal cone, so recall degrades with the flatness of
the hull around a vertex, not with the point count. On 500 standard-normal points
(31 hull vertices) the recovered fraction measures 0.61 at n_directions=32,
0.81 at 128, 0.87 at 256, 0.94 at 512 and 1.00 at 16384; the four vertices missed
at 256 have normal cones spanning 3e-5 to 2e-3 of the sphere, so 512 antipodal
orientations are expected to hit them 0.02 to 1.1 times. Increasing
n_directions is the only knob that raises recall -- tolerance trades
precision for it and is not a substitute. When all hull vertices are required,
use an exact hull (scipy.spatial.ConvexHull and the qhull-backed mesh
libraries): no setting of these parameters makes this function conservative.
See Also
convex_subset
convex_superset_mask
sample_fibonacci_hemisphere
flatnonzero
scipy.spatial.ConvexHull
Source code in triwarp/points.py
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 | |
convex_subset
convex_subset(
points: array[vec3],
n_directions: int = 128,
tolerance: float = 1e-06,
) -> wp.array[wp.vec3]
Approximate the convex-hull vertices of a point cloud as a point subset.
Convenience wrapper around
convex_subset_mask that returns the
selected points directly, gathered from points in ascending index order. The
accuracy is the mask's: the returned subset lies on the hull boundary but can omit
hull vertices, and is not a conservative superset of them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
n_directions
|
int
|
Number of Fibonacci hemisphere directions (see
|
128
|
tolerance
|
float
|
Relative slack on the support test (see
|
1e-06
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
The subset of |
Source code in triwarp/points.py
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 | |
convex_superset_mask
convex_superset_mask(
points: array[vec3],
subdivisions: int = 2,
margin: float = 1e-05,
) -> wp.array[wp.bool]
Conservatively discard interior points, keeping a superset of the convex-hull vertices.
Unlike convex_subset_mask, which keeps only points it can
prove are on the hull, this keeps every point it cannot prove is interior. The certificate
is a tetrahedron: the support point of the cloud along each direction of an
icosphere is a hull vertex, so for every triangle (a, b, c)
of the icosphere the tetrahedron (centroid, s_a, s_b, s_c) has all four corners in the hull
and therefore lies inside it. Any point strictly inside such a tetrahedron is strictly inside
the hull and cannot be a hull vertex; every other point is kept.
Because a hull vertex lies on the hull boundary, it is in no tetrahedron's strict interior, so
no hull vertex is ever discarded -- for any input, any subdivisions, and any margin.
The parameters trade only how many interior points survive. That makes this a prefilter: run it,
then hand the survivors to an exact hull, which then does its superlinear work on a small
fraction of the cloud.
Cost is one support sweep (n_points times the 10 * 4 ** subdivisions + 2 icosphere
directions) plus one pass testing each point against the 20 * 4 ** subdivisions
tetrahedra, whose precomputed face planes every thread reads in lockstep.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
subdivisions
|
int
|
Icosphere refinement level for the direction set, which also fixes the tetrahedron count. Higher values wrap the hull more tightly and so discard more interior points, at a proportionally higher cost; the guarantee is unaffected. |
2
|
margin
|
float
|
Distance, as a fraction of the shell radius, by which a point must clear all four faces of a tetrahedron to count as strictly inside it. Raising it keeps more points, so it can only weaken the filter, never the guarantee. The default is set by float32 arithmetic rather than by taste: a point lying exactly on a face must not be read as inside. See Notes. |
1e-05
|
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Notes
Selectivity depends strongly on the cloud's shape: a near-spherical cloud keeps a small fraction of its points (a triangulated inner shell hugs it tightly), while a flat-faced cloud keeps far more, because the shell cannot hug a plane and the survivors form a thin slab under each face.
The guarantee is exact in real arithmetic; in float32 it rests on margin covering the error
in the plane evaluation. Values of margin below 1e-6 can discard a true hull vertex --
always a point lying essentially exactly on a tetrahedron face, where the computed distance
straddles zero -- while 1e-6 and above are clean; the 1e-5 default sits well above that
boundary, at a small cost in selectivity. TETRAHEDRON_FLATNESS is the other half of the
same protection: it discards sliver tetrahedra whose face normals are too ill-conditioned to
trust, and without it no margin in this range is safe.
Degenerate input is handled by the same conservative logic rather than by a special case. A
coplanar or collinear cloud makes every tetrahedron flat; flat tetrahedra are rejected as
ill-conditioned, so nothing is certified interior and every point is kept, which is a valid
(if useless) superset. Fewer than four points returns an all-True mask directly.
Source code in triwarp/points.py
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 | |
vector_angle
vector_angle(
a: array[vec3], b: array[vec3]
) -> wp.array[wp.float32]
Unsigned angle in radians between pairs of vectors.
For each index i, computes atan2(norm(cross(a[i], b[i])), dot(a[i], b[i])), which lies
in [0, pi]. Matches trimesh.geometry.vector_angle on stacked pairs, and agrees with
its arccos(dot) to round-off on unit input while staying accurate for nearly parallel or
nearly antiparallel pairs, where arccos loses most of its digits. Being scale-free, it also
accepts unnormalized vectors, and pairs where either vector is zero read 0.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
array[vec3]
|
Length- |
required |
b
|
array[vec3]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
array[float32]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
Source code in triwarp/points.py
1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 | |
radial_sort
radial_sort(
points: array[vec3],
origin: vec3,
normal: vec3,
start: vec3 | None = None,
) -> wp.array[wp.vec3]
Sort points radially (by angle) around an axis and return them reordered.
Points are projected onto two axes perpendicular to normal and ordered by
the angle atan2 of the projection, in descending order (matching
trimesh.points.radial_sort).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
origin
|
vec3
|
Point to sort around as |
required |
normal
|
vec3
|
Axis to sort around as |
required |
start
|
vec3 | None
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in triwarp/points.py
1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 | |