Point Cloud Metrics
Point Cloud Utils has functions to compute a number of commonly used metrics between point clouds.
Chamfer Distance
The Chamfer distance between two point clouds \(P_1 = \{x_i \in \mathbb{R}^3\}_{i=1}^n\) and \(P_2 = \{x_j \in \mathbb{R}^3\}_{j=1}^m\) is defined as the average distance between pairs of nearest neighbors between \(P_1\) and \(P_2\) i.e. $$ \text{chamfer}(P_1, P_2) = \frac{1}{2n} \sum_{i=1}^n |x_i - \text{NN}(x_i, P_2)| + \frac{1}{2m} \sum_{j=1}^n |x_j - \text{NN}(x_j, P_1)| $$ and \(\text{NN}(x, P) = \text{argmin}_{x' \in P} \|x - x'\|\) is the nearest neighbor function.
The following code computes the Chamfer distance between two point clouds:
import point_cloud_utils as pcu
# p1 is an (n, 3)-shaped numpy array containing one point per row
p1 = pcu.load_mesh_v("point_cloud_1.ply")
# p2 is an (m, 3)-shaped numpy array containing one point per row
p2 = pcu.load_mesh_v("point_cloud_2.ply")
# Compute the chamfer distance between p1 and p2
cd = pcu.chamfer_distance(p1, p2)
Hausdorff distance
The Hausdorff distance between two point clouds \(P_1 = \{x_i \in \mathbb{R}^3\}_{i=1}^n\) and \(P_2 = \{x_j \in \mathbb{R}^3\}_{j=1}^m\) is defined as the maxmimum distance between any pair of nearest neighbors between \(P_1\) and \(P_2\) i.e. $$ \text{hausdorff}(P_1, P_2) = \frac{1}{2} \max_{x \in P_1} |x - \text{NN}(x, P_2)| + \frac{1}{2} \max_{x' \in P_2} |x' - \text{NN}(x', P_1)| $$ and \(\text{NN}(x, P) = \text{argmin}_{x' \in P} \|x - x'\|\) is the nearest neighbor function.
The following code computes the Hausdorff distance between two point clouds:
import point_cloud_utils as pcu
# p1 is an (n, 3)-shaped numpy array containing one point per row
p1 = pcu.load_mesh_v("point_cloud_1.ply")
# p2 is an (m, 3)-shaped numpy array containing one point per row
p2 = pcu.load_mesh_v("point_cloud_2.ply")
# Compute the chamfer distance between p1 and p2
hd = pcu.hausdorff_distance(p1, p2)
One sided Hausdorff distance
In some applications, one only needs the one-sided Hausdorff distance between \(P_1\) and \(P_2\), i.e.
The following code computes the one-sided Hausdorff distance between two point clouds:
import point_cloud_utils as pcu
# p1 is an (n, 3)-shaped numpy array containing one point per row
p1 = pcu.load_mesh_v("point_cloud_1.ply")
# p2 is an (m, 3)-shaped numpy array containing one point per row
p2 = pcu.load_mesh_v("point_cloud_2.ply")
# Compute the chamfer distance between p1 and p2
hd_p1_to_p2 = pcu.one_sided_hausdorff_distance(p1, p2)
Note
To get the \(P_2 \rightarrow P_1\) Hausdorff distance, just swap the arguments to pcu.one_sided_hausdorff_distance
Earth-Mover's (Sinkhorn) distance
The Earth Mover's distance between two point clouds \(P = \{p_i \in \mathbb{R}^3\}_{i=1}^n\) and \(Q = \{q_j \in \mathbb{R}^3\}_{j=1}^m\) is computed as the average distance between pairs of points according to an optimal correspondence \(\pi \in \Pi(P, Q)\), where \(\Pi(P, Q)\) is the set of \(n \times m\) matrices where the rows and columns sum to one. The assignment \(\pi\) is thus a matrix where \(\Pi_{i,j}\) is a number between \(0\) and \(1\) denoting how much point \(p_i\) and \(q_j\) correspond. We can write the EMD formally as: $$ \text{EMD}(P, Q) = \min_{\pi \in \Pi(P, Q)} \sum_{i = 1}^n \sum_{j = 1}^m \pi_{i,j} |p_i - q_j| % \langle \pi, D \rangle \qquad D_{ij} = |p_i - q_j| $$
Point Cloud Utils implements the sinkhorn algorithm for computing the (approximate) Earth Mover's Distance. To compute the EMD, run:
import point_cloud_utils as pcu
# p1 is an (n, 3)-shaped numpy array containing one point per row
p1 = pcu.load_mesh_v("point_cloud_1.ply")
# p2 is an (m, 3)-shaped numpy array containing one point per row
p2 = pcu.load_mesh_v("point_cloud_2.ply")
# Compute the chamfer distance between p1 and p2
emd, pi = pcu.earth_movers_distance(p1, p2)