Skip to content

metrics

triwarp.metrics

Chamfer and Hausdorff distances between point clouds and meshes.

All metrics are fully GPU-resident: they compose the nearest-neighbor and point-to-surface primitives in proximity with the tiled reductions in reduce, and never move per-element data to the host.

Chamfer distances follow the pytorch3d convention and are built on squared Euclidean distances. Hausdorff distances follow libigl's igl::hausdorff and reduce the (already Euclidean) per-element distances with a maximum, so no squaring or final square root is needed.

One place the convention does not carry over, because the two libraries answer different questions: pytorch3d.loss.point_mesh_face_distance sums point-to-triangle with face-to-point, where chamfer_points_to_mesh's backward direction is mesh-vertex to nearest query. The forward halves agree, but the whole scalars differ by construction and are not comparable.

Two families of geometry are supported and can be mixed:

  • points_to_points -- both sides are point clouds (nearest neighbor via query_nearest).
  • points_to_mesh -- a point cloud versus a triangle mesh (point-to-surface via closest_point_on_mesh one way, mesh-vertex nearest neighbor the other way).
  • mesh_to_mesh -- both sides are triangle meshes (vertex-to-surface both directions).

chamfer_points_to_points

chamfer_points_to_points(
    x: array[vec3],
    y: array[vec3],
    *,
    point_reduction: _PointReduction = ...,
    single_directional: bool = ...,
) -> float
chamfer_points_to_points(
    x: array[vec3],
    y: array[vec3],
    *,
    point_reduction: None,
    single_directional: bool = ...,
) -> _UnreducedChamfer
chamfer_points_to_points(
    x: array[vec3],
    y: array[vec3],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer

Chamfer distance between two point clouds.

For each point in x the squared Euclidean distance to its nearest neighbor in y is accumulated (and symmetrically for y into x unless single_directional), following the pytorch3d convention, whose single_directional keyword maps onto this one exactly.

Parameters:

Name Type Description Default
x array[vec3]

(n,) query point cloud as wp.vec3.

required
y array[vec3]

(m,) target point cloud as wp.vec3.

required
point_reduction _PointReduction | None

How to reduce the per-point squared distances of each direction: "mean" (default), "sum", "max", or None to return the per-point squared distances unreduced. "max" yields a directed Hausdorff-like value.

'mean'
single_directional bool

If True, only the x -> y term is computed.

False

Returns:

Type Description
float or array[float32] or tuple

The reduced Chamfer distance as a Python float when point_reduction is not None. Otherwise the per-point squared distances: a single (n,) array when single_directional, else an ((n,), (m,)) tuple. Empty inputs yield 0.0 (reduced) or empty arrays.

Raises:

Type Description
RuntimeError

If x and y are not all on one device.

ValueError

If point_reduction is not "mean", "sum", "max" or None.

See Also

chamfer_points_to_mesh chamfer_mesh_to_mesh query_nearest

Source code in triwarp/metrics.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 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
122
123
124
125
126
127
128
129
130
131
132
133
134
def chamfer_points_to_points(
    x: wp.array[wp.vec3],
    y: wp.array[wp.vec3],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer:
    """
    Chamfer distance between two point clouds.

    For each point in ``x`` the squared Euclidean distance to its nearest neighbor
    in ``y`` is accumulated (and symmetrically for ``y`` into ``x`` unless
    ``single_directional``), following the ``pytorch3d`` convention, whose ``single_directional``
    keyword maps onto this one exactly.

    Parameters
    ----------
    x
        ``(n,)`` query point cloud as ``wp.vec3``.
    y
        ``(m,)`` target point cloud as ``wp.vec3``.
    point_reduction
        How to reduce the per-point squared distances of each direction:
        ``"mean"`` (default), ``"sum"``, ``"max"``, or ``None`` to return the
        per-point squared distances unreduced. ``"max"`` yields a directed
        Hausdorff-like value.
    single_directional
        If ``True``, only the ``x -> y`` term is computed.

    Returns
    -------
    float or wp.array[wp.float32] or tuple
        The reduced Chamfer distance as a Python ``float`` when ``point_reduction``
        is not ``None``. Otherwise the per-point squared distances: a single
        ``(n,)`` array when ``single_directional``, else an ``((n,), (m,))`` tuple.
        Empty inputs yield ``0.0`` (reduced) or empty arrays.

    Raises
    ------
    RuntimeError
        If ``x`` and ``y`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"``, ``"sum"``, ``"max"`` or ``None``.

    See Also
    --------
    [`chamfer_points_to_mesh`][triwarp.metrics.chamfer_points_to_mesh]
    [`chamfer_mesh_to_mesh`][triwarp.metrics.chamfer_mesh_to_mesh]
    [`query_nearest`][triwarp.neighbors.query_nearest]
    """
    require_same_device(x=x, y=y)
    _validate_point_reduction(point_reduction)
    distances = _distances_points_to_points(x, y, single_directional)
    if distances is None:
        return _empty_chamfer(point_reduction, single_directional, x.device)
    return _chamfer(*distances, point_reduction, single_directional)

chamfer_points_to_mesh

chamfer_points_to_mesh(
    points: array[vec3],
    vertices: array[vec3],
    faces: array[int32],
    *,
    point_reduction: _PointReduction = ...,
    single_directional: bool = ...,
) -> float
chamfer_points_to_mesh(
    points: array[vec3],
    vertices: array[vec3],
    faces: array[int32],
    *,
    point_reduction: None,
    single_directional: bool = ...,
) -> _UnreducedChamfer
chamfer_points_to_mesh(
    points: array[vec3],
    vertices: array[vec3],
    faces: array[int32],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer

Chamfer distance between a point cloud and a triangle mesh.

The forward term is the squared distance from each point to the mesh surface (exact, via closest_point_on_mesh); the backward term is the squared distance from each mesh vertex to its nearest point in the cloud.

Parameters:

Name Type Description Default
points array[vec3]

(n,) query point cloud as wp.vec3.

required
vertices array[vec3]

(v,) mesh vertex positions as wp.vec3.

required
faces array[int32]

(f * 3,) flat triangle index array as wp.int32.

required
point_reduction _PointReduction | None

"mean" (default), "sum", "max", or None. See chamfer_points_to_points.

'mean'
single_directional bool

If True, only the points -> mesh surface term is computed.

False

Returns:

Type Description
float or array[float32] or tuple

Same layout as chamfer_points_to_points. The forward array has length n and the backward array length v, except when points, vertices or faces is empty -- a mesh with no faces counts as empty here even when vertices is not, since there is no surface for the forward term to measure against -- in which case both arrays are length 0, matching chamfer_points_to_points's own convention.

Raises:

Type Description
RuntimeError

If points, vertices and faces are not all on one device.

ValueError

If point_reduction is not "mean", "sum", "max" or None.

See Also

chamfer_points_to_points chamfer_mesh_to_mesh closest_point_on_mesh

Source code in triwarp/metrics.py
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def chamfer_points_to_mesh(
    points: wp.array[wp.vec3],
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer:
    """
    Chamfer distance between a point cloud and a triangle mesh.

    The forward term is the squared distance from each point to the mesh surface
    (exact, via [`closest_point_on_mesh`][triwarp.proximity.closest_point_on_mesh]);
    the backward term is the squared distance from each mesh vertex to its nearest
    point in the cloud.

    Parameters
    ----------
    points
        ``(n,)`` query point cloud as ``wp.vec3``.
    vertices
        ``(v,)`` mesh vertex positions as ``wp.vec3``.
    faces
        ``(f * 3,)`` flat triangle index array as ``wp.int32``.
    point_reduction
        ``"mean"`` (default), ``"sum"``, ``"max"``, or ``None``. See
        [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points].
    single_directional
        If ``True``, only the ``points -> mesh surface`` term is computed.

    Returns
    -------
    float or wp.array[wp.float32] or tuple
        Same layout as [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points].
        The forward array has length ``n`` and the backward array length ``v``, except when
        ``points``, ``vertices`` or ``faces`` is empty -- a mesh with no faces counts as empty here
        even when ``vertices`` is not, since there is no surface for the forward term to measure
        against -- in which case both arrays are length 0, matching
        [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points]'s own convention.

    Raises
    ------
    RuntimeError
        If ``points``, ``vertices`` and ``faces`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"``, ``"sum"``, ``"max"`` or ``None``.

    See Also
    --------
    [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points]
    [`chamfer_mesh_to_mesh`][triwarp.metrics.chamfer_mesh_to_mesh]
    [`closest_point_on_mesh`][triwarp.proximity.closest_point_on_mesh]
    """
    require_same_device(points=points, vertices=vertices, faces=faces)
    _validate_point_reduction(point_reduction)
    distances = _distances_points_to_mesh(points, vertices, faces, single_directional)
    if distances is None:
        return _empty_chamfer(point_reduction, single_directional, points.device)
    return _chamfer(*distances, point_reduction, single_directional)

chamfer_mesh_to_mesh

chamfer_mesh_to_mesh(
    vertices_a: array[vec3],
    faces_a: array[int32],
    vertices_b: array[vec3],
    faces_b: array[int32],
    *,
    point_reduction: _PointReduction = ...,
    single_directional: bool = ...,
) -> float
chamfer_mesh_to_mesh(
    vertices_a: array[vec3],
    faces_a: array[int32],
    vertices_b: array[vec3],
    faces_b: array[int32],
    *,
    point_reduction: None,
    single_directional: bool = ...,
) -> _UnreducedChamfer
chamfer_mesh_to_mesh(
    vertices_a: array[vec3],
    faces_a: array[int32],
    vertices_b: array[vec3],
    faces_b: array[int32],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer

Chamfer distance between two triangle meshes (vertex-to-surface).

The forward term is the squared distance from each vertex of mesh A to the surface of mesh B; the backward term is the reverse. This mirrors the vertex-based approach of libigl while accumulating (mean/sum) instead of maximizing.

Parameters:

Name Type Description Default
vertices_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
faces_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
vertices_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
faces_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
point_reduction _PointReduction | None

"mean" (default), "sum", "max", or None. See chamfer_points_to_points.

'mean'
single_directional bool

If True, only the A -> surface(B) term is computed.

False

Returns:

Type Description
float or array[float32] or tuple

Same layout as chamfer_points_to_points. The forward array has length va and the backward array length vb, except when any of vertices_a, faces_a, vertices_b or faces_b is empty -- a mesh with no faces counts as empty here even when its vertices are not -- in which case both arrays are length 0.

Raises:

Type Description
RuntimeError

If vertices_a, faces_a, vertices_b and faces_b are not all on one device.

ValueError

If point_reduction is not "mean", "sum", "max" or None.

See Also

chamfer_points_to_points chamfer_points_to_mesh hausdorff_mesh_to_mesh

Source code in triwarp/metrics.py
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
287
288
289
290
291
292
293
294
295
def chamfer_mesh_to_mesh(
    vertices_a: wp.array[wp.vec3],
    faces_a: wp.array[wp.int32],
    vertices_b: wp.array[wp.vec3],
    faces_b: wp.array[wp.int32],
    *,
    point_reduction: _PointReduction | None = "mean",
    single_directional: bool = False,
) -> float | _UnreducedChamfer:
    """
    Chamfer distance between two triangle meshes (vertex-to-surface).

    The forward term is the squared distance from each vertex of mesh ``A`` to the
    surface of mesh ``B``; the backward term is the reverse. This mirrors the
    vertex-based approach of libigl while accumulating (mean/sum) instead of maximizing.

    Parameters
    ----------
    vertices_a, faces_a
        Vertices ``(va,)`` (``wp.vec3``) and flat faces ``(fa * 3,)`` (``wp.int32``) of mesh ``A``.
    vertices_b, faces_b
        Vertices ``(vb,)`` and flat faces ``(fb * 3,)`` of mesh ``B``.
    point_reduction
        ``"mean"`` (default), ``"sum"``, ``"max"``, or ``None``. See
        [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points].
    single_directional
        If ``True``, only the ``A -> surface(B)`` term is computed.

    Returns
    -------
    float or wp.array[wp.float32] or tuple
        Same layout as [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points].
        The forward array has length ``va`` and the backward array length ``vb``, except when any
        of ``vertices_a``, ``faces_a``, ``vertices_b`` or ``faces_b`` is empty -- a mesh with no
        faces counts as empty here even when its vertices are not -- in which case both arrays are
        length 0.

    Raises
    ------
    RuntimeError
        If ``vertices_a``, ``faces_a``, ``vertices_b`` and ``faces_b`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"``, ``"sum"``, ``"max"`` or ``None``.

    See Also
    --------
    [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points]
    [`chamfer_points_to_mesh`][triwarp.metrics.chamfer_points_to_mesh]
    [`hausdorff_mesh_to_mesh`][triwarp.metrics.hausdorff_mesh_to_mesh]
    """
    require_same_device(
        vertices_a=vertices_a, faces_a=faces_a, vertices_b=vertices_b, faces_b=faces_b
    )
    _validate_point_reduction(point_reduction)
    distances = _distances_mesh_to_mesh(
        vertices_a, faces_a, vertices_b, faces_b, single_directional
    )
    if distances is None:
        return _empty_chamfer(point_reduction, single_directional, vertices_a.device)
    return _chamfer(*distances, point_reduction, single_directional)

chamfer_points_to_points_loss

chamfer_points_to_points_loss(
    x: array[vec3],
    y: array[vec3],
    *,
    tape: Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32

Differentiable Chamfer loss between two point clouds.

Squared-distance analogue of chamfer_points_to_points that returns a length-1 wp.float32 device array instead of a host float, so the loss can be back-propagated to x and y through a caller-owned wp.Tape.

The nearest-neighbor assignment (via query_nearest) is computed outside tape and held constant during the backward pass, matching pytorch3d's chamfer_distance gradient.

Parameters:

Name Type Description Default
x array[vec3]

(n,) query point cloud as wp.vec3. Set requires_grad=True for gradients.

required
y array[vec3]

(m,) target point cloud as wp.vec3. Set requires_grad=True for gradients.

required
tape Tape | None

Caller-owned wp.Tape into which the differentiable kernels are recorded. When None the loss is still computed but no operations are taped (no gradient).

None
point_reduction _DiffReduction

"mean" (default) or "sum"; reduces the per-point squared distances of each direction. "max" and None are not supported (see chamfer_points_to_points).

'mean'
single_directional bool

If True, only the x -> y term is accumulated.

False

Returns:

Type Description
array[float32]

Length-1 device array holding the Chamfer loss (requires_grad=True). Empty inputs yield a length-1 zero array.

Raises:

Type Description
RuntimeError

If x and y are not all on one device.

ValueError

If point_reduction is not "mean" or "sum".

See Also

chamfer_points_to_points chamfer_points_to_mesh_loss chamfer_mesh_to_mesh_loss

Source code in triwarp/metrics.py
325
326
327
328
329
330
331
332
333
334
335
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
391
392
393
394
395
396
397
398
399
400
401
402
403
def chamfer_points_to_points_loss(
    x: wp.array[wp.vec3],
    y: wp.array[wp.vec3],
    *,
    tape: wp.Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32:
    """
    Differentiable Chamfer loss between two point clouds.

    Squared-distance analogue of
    [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points] that returns
    a length-1 ``wp.float32`` device array instead of a host ``float``, so the loss can be
    back-propagated to ``x`` and ``y`` through a caller-owned ``wp.Tape``.

    The nearest-neighbor assignment (via
    [`query_nearest`][triwarp.neighbors.query_nearest]) is computed
    outside ``tape`` and held constant during the backward pass, matching pytorch3d's
    ``chamfer_distance`` gradient.

    Parameters
    ----------
    x
        ``(n,)`` query point cloud as ``wp.vec3``. Set ``requires_grad=True`` for gradients.
    y
        ``(m,)`` target point cloud as ``wp.vec3``. Set ``requires_grad=True`` for gradients.
    tape
        Caller-owned ``wp.Tape`` into which the differentiable kernels are recorded. When
        ``None`` the loss is still computed but no operations are taped (no gradient).
    point_reduction
        ``"mean"`` (default) or ``"sum"``; reduces the per-point squared distances of each
        direction. ``"max"`` and ``None`` are not supported (see
        [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points]).
    single_directional
        If ``True``, only the ``x -> y`` term is accumulated.

    Returns
    -------
    wp.array[wp.float32]
        Length-1 device array holding the Chamfer loss (``requires_grad=True``). Empty
        inputs yield a length-1 zero array.

    Raises
    ------
    RuntimeError
        If ``x`` and ``y`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"`` or ``"sum"``.

    See Also
    --------
    [`chamfer_points_to_points`][triwarp.metrics.chamfer_points_to_points]
    [`chamfer_points_to_mesh_loss`][triwarp.metrics.chamfer_points_to_mesh_loss]
    [`chamfer_mesh_to_mesh_loss`][triwarp.metrics.chamfer_mesh_to_mesh_loss]
    """
    require_same_device(x=x, y=y)
    _validate_diff_reduction(point_reduction)
    device = x.device
    n = int(x.shape[0])
    m = int(y.shape[0])
    loss = _zero_loss(device)
    if n == 0 or m == 0:
        return loss

    # Non-differentiable nearest-neighbor indices (computed outside the tape). The backward search
    # is seeded from the forward one's own distances -- see `_backward_radius`.
    nearest_xy, dist_xy = tw.neighbors.query_nearest(y, x, k=1)
    terms = [lambda: _launch_nn_term(x, y, nearest_xy, _reduction_scale(point_reduction, n), loss)]
    if not single_directional:
        nearest_yx = tw.neighbors.query_nearest(
            x, y, k=1, initial_radius=_backward_radius(dist_xy)
        )[0]
        terms.append(
            lambda: _launch_nn_term(y, x, nearest_yx, _reduction_scale(point_reduction, m), loss)
        )

    _accumulate_chamfer_terms(tape, terms)
    return loss

chamfer_points_to_mesh_loss

chamfer_points_to_mesh_loss(
    points: array[vec3],
    vertices: array[vec3],
    faces: array[int32],
    *,
    tape: Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32

Differentiable Chamfer loss between a point cloud and a triangle mesh.

Squared-distance analogue of chamfer_points_to_mesh. The forward term is the exact point-to-surface squared distance (closest triangle held constant during backprop); the backward term is the squared distance from each mesh vertex to its nearest point in the cloud. Gradients flow to points and vertices.

Parameters:

Name Type Description Default
points array[vec3]

(n,) query point cloud as wp.vec3.

required
vertices array[vec3]

(v,) mesh vertex positions as wp.vec3.

required
faces array[int32]

(f * 3,) flat triangle index array as wp.int32.

required
tape Tape | None

Caller-owned wp.Tape for the backward pass, or None to skip taping.

None
point_reduction _DiffReduction

"mean" (default) or "sum".

'mean'
single_directional bool

If True, only the points -> mesh surface term is accumulated.

False

Returns:

Type Description
array[float32]

Length-1 device array holding the Chamfer loss (requires_grad=True).

Raises:

Type Description
RuntimeError

If points, vertices and faces are not all on one device.

ValueError

If point_reduction is not "mean" or "sum".

See Also

chamfer_points_to_mesh chamfer_points_to_points_loss chamfer_mesh_to_mesh_loss

Source code in triwarp/metrics.py
406
407
408
409
410
411
412
413
414
415
416
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
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
def chamfer_points_to_mesh_loss(
    points: wp.array[wp.vec3],
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    *,
    tape: wp.Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32:
    """
    Differentiable Chamfer loss between a point cloud and a triangle mesh.

    Squared-distance analogue of
    [`chamfer_points_to_mesh`][triwarp.metrics.chamfer_points_to_mesh]. The forward term
    is the exact point-to-surface squared distance (closest triangle held constant during
    backprop); the backward term is the squared distance from each mesh vertex to its
    nearest point in the cloud. Gradients flow to ``points`` **and** ``vertices``.

    Parameters
    ----------
    points
        ``(n,)`` query point cloud as ``wp.vec3``.
    vertices
        ``(v,)`` mesh vertex positions as ``wp.vec3``.
    faces
        ``(f * 3,)`` flat triangle index array as ``wp.int32``.
    tape
        Caller-owned ``wp.Tape`` for the backward pass, or ``None`` to skip taping.
    point_reduction
        ``"mean"`` (default) or ``"sum"``.
    single_directional
        If ``True``, only the ``points -> mesh surface`` term is accumulated.

    Returns
    -------
    wp.array[wp.float32]
        Length-1 device array holding the Chamfer loss (``requires_grad=True``).

    Raises
    ------
    RuntimeError
        If ``points``, ``vertices`` and ``faces`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"`` or ``"sum"``.

    See Also
    --------
    [`chamfer_points_to_mesh`][triwarp.metrics.chamfer_points_to_mesh]
    [`chamfer_points_to_points_loss`][triwarp.metrics.chamfer_points_to_points_loss]
    [`chamfer_mesh_to_mesh_loss`][triwarp.metrics.chamfer_mesh_to_mesh_loss]
    """
    require_same_device(points=points, vertices=vertices, faces=faces)
    _validate_diff_reduction(point_reduction)
    device = points.device
    n = int(points.shape[0])
    v = int(vertices.shape[0])
    n_faces = int(faces.shape[0]) // 3
    loss = _zero_loss(device)
    if n == 0 or v == 0 or n_faces == 0:
        return loss

    # Non-differentiable closest-face and nearest-neighbor assignment (outside the tape). The
    # backward search is seeded from the forward one's own distances -- see `_backward_radius`.
    _, dist_forward, face_id = tw.proximity.closest_point_on_mesh(vertices, faces, points)
    terms = [
        lambda: _launch_surface_term(
            points, vertices, faces, face_id, _reduction_scale(point_reduction, n), loss
        )
    ]
    if not single_directional:
        nearest_vp = tw.neighbors.query_nearest(
            points, vertices, k=1, initial_radius=_backward_radius(dist_forward)
        )[0]
        terms.append(
            lambda: _launch_nn_term(
                vertices, points, nearest_vp, _reduction_scale(point_reduction, v), loss
            )
        )

    _accumulate_chamfer_terms(tape, terms)
    return loss

chamfer_mesh_to_mesh_loss

chamfer_mesh_to_mesh_loss(
    vertices_a: array[vec3],
    faces_a: array[int32],
    vertices_b: array[vec3],
    faces_b: array[int32],
    *,
    tape: Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32

Differentiable Chamfer loss between two triangle meshes (vertex-to-surface).

Squared-distance analogue of chamfer_mesh_to_mesh. The forward term is the squared distance from each vertex of mesh A to the surface of mesh B (with the closest triangle held constant during backprop); the backward term is the reverse. Gradients flow to both meshes' vertices.

Parameters:

Name Type Description Default
vertices_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
faces_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
vertices_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
faces_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
tape Tape | None

Caller-owned wp.Tape for the backward pass, or None to skip taping.

None
point_reduction _DiffReduction

"mean" (default) or "sum".

'mean'
single_directional bool

If True, only the A -> surface(B) term is accumulated.

False

Returns:

Type Description
array[float32]

Length-1 device array holding the Chamfer loss (requires_grad=True).

Raises:

Type Description
RuntimeError

If vertices_a, faces_a, vertices_b and faces_b are not all on one device.

ValueError

If point_reduction is not "mean" or "sum".

See Also

chamfer_mesh_to_mesh chamfer_points_to_points_loss chamfer_points_to_mesh_loss

Source code in triwarp/metrics.py
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
def chamfer_mesh_to_mesh_loss(
    vertices_a: wp.array[wp.vec3],
    faces_a: wp.array[wp.int32],
    vertices_b: wp.array[wp.vec3],
    faces_b: wp.array[wp.int32],
    *,
    tape: wp.Tape | None = None,
    point_reduction: _DiffReduction = "mean",
    single_directional: bool = False,
) -> twt.Array1dFloat32:
    """
    Differentiable Chamfer loss between two triangle meshes (vertex-to-surface).

    Squared-distance analogue of
    [`chamfer_mesh_to_mesh`][triwarp.metrics.chamfer_mesh_to_mesh]. The forward term is
    the squared distance from each vertex of mesh ``A`` to the surface of mesh ``B`` (with
    the closest triangle held constant during backprop); the backward term is the reverse.
    Gradients flow to both meshes' vertices.

    Parameters
    ----------
    vertices_a, faces_a
        Vertices ``(va,)`` (``wp.vec3``) and flat faces ``(fa * 3,)`` (``wp.int32``) of mesh ``A``.
    vertices_b, faces_b
        Vertices ``(vb,)`` and flat faces ``(fb * 3,)`` of mesh ``B``.
    tape
        Caller-owned ``wp.Tape`` for the backward pass, or ``None`` to skip taping.
    point_reduction
        ``"mean"`` (default) or ``"sum"``.
    single_directional
        If ``True``, only the ``A -> surface(B)`` term is accumulated.

    Returns
    -------
    wp.array[wp.float32]
        Length-1 device array holding the Chamfer loss (``requires_grad=True``).

    Raises
    ------
    RuntimeError
        If ``vertices_a``, ``faces_a``, ``vertices_b`` and ``faces_b`` are not all on one device.
    ValueError
        If ``point_reduction`` is not ``"mean"`` or ``"sum"``.

    See Also
    --------
    [`chamfer_mesh_to_mesh`][triwarp.metrics.chamfer_mesh_to_mesh]
    [`chamfer_points_to_points_loss`][triwarp.metrics.chamfer_points_to_points_loss]
    [`chamfer_points_to_mesh_loss`][triwarp.metrics.chamfer_points_to_mesh_loss]
    """
    require_same_device(
        vertices_a=vertices_a, faces_a=faces_a, vertices_b=vertices_b, faces_b=faces_b
    )
    _validate_diff_reduction(point_reduction)
    device = vertices_a.device
    va = int(vertices_a.shape[0])
    vb = int(vertices_b.shape[0])
    n_faces_a = int(faces_a.shape[0]) // 3
    n_faces_b = int(faces_b.shape[0]) // 3
    loss = _zero_loss(device)
    if va == 0 or vb == 0 or n_faces_a == 0 or n_faces_b == 0:
        return loss

    # Non-differentiable closest-face assignment for each direction (outside the tape).
    face_id_ab = tw.proximity.closest_point_on_mesh(vertices_b, faces_b, vertices_a)[2]
    terms = [
        lambda: _launch_surface_term(
            vertices_a, vertices_b, faces_b, face_id_ab, _reduction_scale(point_reduction, va), loss
        )
    ]
    if not single_directional:
        face_id_ba = tw.proximity.closest_point_on_mesh(vertices_a, faces_a, vertices_b)[2]
        terms.append(
            lambda: _launch_surface_term(
                vertices_b,
                vertices_a,
                faces_a,
                face_id_ba,
                _reduction_scale(point_reduction, vb),
                loss,
            )
        )

    _accumulate_chamfer_terms(tape, terms)
    return loss

hausdorff_points_to_points

hausdorff_points_to_points(
    x: array[vec3],
    y: array[vec3],
    *,
    single_directional: bool = False,
) -> float

Hausdorff distance between two point clouds.

The directed distance d(x, y) = max_i min_j ||x_i - y_j|| is computed (and symmetrized with d(y, x) unless single_directional). Equivalent to symmetrizing scipy.spatial.distance.directed_hausdorff.

Parameters:

Name Type Description Default
x array[vec3]

(n,) query point cloud as wp.vec3.

required
y array[vec3]

(m,) target point cloud as wp.vec3.

required
single_directional bool

If True, return only the directed distance d(x, y).

False

Returns:

Type Description
float

The (symmetric or directed) Hausdorff distance. 0.0 for empty inputs.

Raises:

Type Description
RuntimeError

If x and y are not all on one device.

See Also

hausdorff_points_to_mesh hausdorff_mesh_to_mesh scipy.spatial.distance.directed_hausdorff

Source code in triwarp/metrics.py
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
def hausdorff_points_to_points(
    x: wp.array[wp.vec3], y: wp.array[wp.vec3], *, single_directional: bool = False
) -> float:
    """
    Hausdorff distance between two point clouds.

    The directed distance ``d(x, y) = max_i min_j ||x_i - y_j||`` is computed (and
    symmetrized with ``d(y, x)`` unless ``single_directional``). Equivalent to
    symmetrizing [`scipy.spatial.distance.directed_hausdorff`][].

    Parameters
    ----------
    x
        ``(n,)`` query point cloud as ``wp.vec3``.
    y
        ``(m,)`` target point cloud as ``wp.vec3``.
    single_directional
        If ``True``, return only the directed distance ``d(x, y)``.

    Returns
    -------
    float
        The (symmetric or directed) Hausdorff distance. ``0.0`` for empty inputs.

    Raises
    ------
    RuntimeError
        If ``x`` and ``y`` are not all on one device.

    See Also
    --------
    [`hausdorff_points_to_mesh`][triwarp.metrics.hausdorff_points_to_mesh]
    [`hausdorff_mesh_to_mesh`][triwarp.metrics.hausdorff_mesh_to_mesh]
    [`scipy.spatial.distance.directed_hausdorff`][]
    """
    require_same_device(x=x, y=y)
    distances = _distances_points_to_points(x, y, single_directional)
    if distances is None:
        return 0.0
    return _hausdorff(*distances, single_directional)

hausdorff_points_to_mesh

hausdorff_points_to_mesh(
    points: array[vec3],
    vertices: array[vec3],
    faces: array[int32],
    *,
    single_directional: bool = False,
) -> float

Hausdorff distance between a point cloud and a triangle mesh.

The forward distance is the maximum over points of the distance to the mesh surface; the backward distance is the maximum over mesh vertices of the distance to the nearest point in the cloud.

Parameters:

Name Type Description Default
points array[vec3]

(n,) query point cloud as wp.vec3.

required
vertices array[vec3]

(v,) mesh vertex positions as wp.vec3.

required
faces array[int32]

(f * 3,) flat triangle index array as wp.int32.

required
single_directional bool

If True, return only the directed points -> mesh surface distance.

False

Returns:

Type Description
float

The (symmetric or directed) Hausdorff distance. 0.0 for empty inputs.

Raises:

Type Description
RuntimeError

If points, vertices and faces are not all on one device.

See Also

hausdorff_points_to_points hausdorff_mesh_to_mesh

Source code in triwarp/metrics.py
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
def hausdorff_points_to_mesh(
    points: wp.array[wp.vec3],
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    *,
    single_directional: bool = False,
) -> float:
    """
    Hausdorff distance between a point cloud and a triangle mesh.

    The forward distance is the maximum over points of the distance to the mesh
    surface; the backward distance is the maximum over mesh vertices of the distance
    to the nearest point in the cloud.

    Parameters
    ----------
    points
        ``(n,)`` query point cloud as ``wp.vec3``.
    vertices
        ``(v,)`` mesh vertex positions as ``wp.vec3``.
    faces
        ``(f * 3,)`` flat triangle index array as ``wp.int32``.
    single_directional
        If ``True``, return only the directed ``points -> mesh surface`` distance.

    Returns
    -------
    float
        The (symmetric or directed) Hausdorff distance. ``0.0`` for empty inputs.

    Raises
    ------
    RuntimeError
        If ``points``, ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`hausdorff_points_to_points`][triwarp.metrics.hausdorff_points_to_points]
    [`hausdorff_mesh_to_mesh`][triwarp.metrics.hausdorff_mesh_to_mesh]
    """
    require_same_device(points=points, vertices=vertices, faces=faces)
    distances = _distances_points_to_mesh(points, vertices, faces, single_directional)
    if distances is None:
        return 0.0
    return _hausdorff(*distances, single_directional)

hausdorff_mesh_to_mesh

hausdorff_mesh_to_mesh(
    vertices_a: array[vec3],
    faces_a: array[int32],
    vertices_b: array[vec3],
    faces_b: array[int32],
    *,
    single_directional: bool = False,
) -> float

Hausdorff distance between two triangle meshes.

Direct port of libigl's igl::hausdorff (vertex-based overload): the distance from every vertex of one mesh to the surface of the other is computed in both directions and the overall maximum is returned. This equals sqrt(max(d_ba, d_ab)) in libigl, where d_* are squared distances.

Parameters:

Name Type Description Default
vertices_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
faces_a array[vec3]

Vertices (va,) (wp.vec3) and flat faces (fa * 3,) (wp.int32) of mesh A.

required
vertices_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
faces_b array[vec3]

Vertices (vb,) and flat faces (fb * 3,) of mesh B.

required
single_directional bool

If True, return only the directed A -> surface(B) distance.

False

Returns:

Type Description
float

The (symmetric or directed) Hausdorff distance. 0.0 for empty inputs.

Raises:

Type Description
RuntimeError

If vertices_a, faces_a, vertices_b and faces_b are not all on one device.

See Also

chamfer_mesh_to_mesh hausdorff_points_to_mesh

Source code in triwarp/metrics.py
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
717
718
def hausdorff_mesh_to_mesh(
    vertices_a: wp.array[wp.vec3],
    faces_a: wp.array[wp.int32],
    vertices_b: wp.array[wp.vec3],
    faces_b: wp.array[wp.int32],
    *,
    single_directional: bool = False,
) -> float:
    """
    Hausdorff distance between two triangle meshes.

    Direct port of libigl's ``igl::hausdorff`` (vertex-based overload): the distance
    from every vertex of one mesh to the surface of the other is computed in both
    directions and the overall maximum is returned. This equals
    ``sqrt(max(d_ba, d_ab))`` in libigl, where ``d_*`` are squared distances.

    Parameters
    ----------
    vertices_a, faces_a
        Vertices ``(va,)`` (``wp.vec3``) and flat faces ``(fa * 3,)`` (``wp.int32``) of mesh ``A``.
    vertices_b, faces_b
        Vertices ``(vb,)`` and flat faces ``(fb * 3,)`` of mesh ``B``.
    single_directional
        If ``True``, return only the directed ``A -> surface(B)`` distance.

    Returns
    -------
    float
        The (symmetric or directed) Hausdorff distance. ``0.0`` for empty inputs.

    Raises
    ------
    RuntimeError
        If ``vertices_a``, ``faces_a``, ``vertices_b`` and ``faces_b`` are not all on one device.

    See Also
    --------
    [`chamfer_mesh_to_mesh`][triwarp.metrics.chamfer_mesh_to_mesh]
    [`hausdorff_points_to_mesh`][triwarp.metrics.hausdorff_points_to_mesh]
    """
    require_same_device(
        vertices_a=vertices_a, faces_a=faces_a, vertices_b=vertices_b, faces_b=faces_b
    )
    distances = _distances_mesh_to_mesh(
        vertices_a, faces_a, vertices_b, faces_b, single_directional
    )
    if distances is None:
        return 0.0
    return _hausdorff(*distances, single_directional)