Hausdorff 距离

ops/2025/1/19 9:17:10/

Hausdorff 距离

本文的内容主要围绕目标定位经典工作 Locating Objects Without Bounding Boxes 展开,着重于介绍 Hausdorff Distance 相关的知识。

  • 论文:https://openaccess.thecvf.com/content_CVPR_2019/html/Ribera_Locating_Objects_Without_Bounding_Boxes_CVPR_2019_paper.html
  • 代码:https://github.com/javiribera/locating-objects-without-bboxes

Hausdorff Distance

Pasted image 20220714184814.png

这是一种用于度量两个点集的距离的度量方式。其已经被广泛应用于多种任务中,包括字符识别、人脸识别、以及场景匹配等。

Ω \Omega Ω 表示所有可能点的空间。对于 2D 图像这样的二维平面可能就是 R 2 \mathbb{R}^{2} R2。对于两个包含点数可能不同的点集 X X X Y Y Y,有针对其中的点的距离度量 d ( x , y ) d(x,y) d(x,y),则具体计算形式为:

d H ( X , Y ) = max ⁡ { sup ⁡ x ∈ X inf ⁡ y ∈ Y d ( x , y ) , sup ⁡ y ∈ Y inf ⁡ x ∈ X d ( x , y ) } d_H(X,Y) = \max \{ \sup_{x \in X} \inf_{y \in Y} d(x,y), \sup_{y \in Y} \inf_{x \in X} d(x,y) \} dH(X,Y)=max{xXsupyYinfd(x,y),yYsupxXinfd(x,y)}

其中 sup ⁡ \sup sup 为上确界操作, inf ⁡ \inf inf 为下确界操作,对于我们关注的图像中的计算所对应的有限点集而言,分别可以简单理解为最大值和最小值。且两点之间距离最大为图像的对角线长度。而这也可以认为是两个点集之间 Hausdorff 距离可能的上界:

d H ( X , Y ) ≤ d m a x = max ⁡ x ∈ Ω , y ∈ Ω d ( x , y ) d_H(X,Y) \le d_{max} = \max_{x \in \Omega, y \in \Omega} d(x,y) dH(X,Y)dmax=xΩ,yΩmaxd(x,y)

这个度量的计算过程可以简单归纳为如下几步:

  • inf ⁡ y ∈ Y d ( x , y ) \inf_{y \in Y} d(x,y) infyYd(x,y):对于每个 x x x,寻找距离最近(下确界)的 y y y 所对应的距离。
  • sup ⁡ x ∈ X inf ⁡ y ∈ Y d ( x , y ) \sup_{x \in X} \inf_{y \in Y} d(x,y) supxXinfyYd(x,y):从所有的 x x x 所对应的距离下界中寻找最大值(上确界)。
  • inf ⁡ x ∈ X d ( x , y ) \inf_{x \in X} d(x,y) infxXd(x,y):对于每个 y y y,寻找距离最近(下确界)的 x x x 所对应的距离。
  • sup ⁡ y ∈ Y inf ⁡ x ∈ X d ( x , y ) \sup_{y \in Y} \inf_{x \in X} d(x,y) supyYinfxXd(x,y):从所有的 y y y 所对应的距离下界中寻找最大值(上确界)。
  • max ⁡ { … } \max \{ \dots \} max{}:对两部分计算结果选择最大值。

这一计算过程直观上可以简单理解为,如果一个点集中的每个点都非常接近于另一个点集中的一些点,那么两个点集就可以认为很接近。

对于 X , Y , Z ∈ Ω X, Y, Z \in \Omega X,Y,ZΩ,Hausdorff 距离满足:

  • d H ( X , Y ) ≥ 0 d_H(X,Y) \ge 0 dH(X,Y)0
  • d H ( X , Y ) = 0 ⇔ X = Y d_H(X,Y)=0 \Leftrightarrow X=Y dH(X,Y)=0X=Y
  • d H ( X , Y ) = d H ( Y , X ) d_H(X,Y)=d_H(Y,X) dH(X,Y)=dH(Y,X)
  • d H ( X , Y ) ≤ d H ( X , Z ) + d H ( Z , Y ) d_H(X,Y) \le d_H(X,Z)+d_H(Z,Y) dH(X,Y)dH(X,Z)+dH(Z,Y)

由于涉及到最大距离的选择,所以 Hausdorff 距离对于异常点是很敏感的。

Average Hausdorff Distance

为了避免这一点,平均 Hausdorff 距离成为了更常用的选择:

d A H = 1 ∣ X ∣ ∑ x ∈ X min ⁡ y ∈ Y d ( x , y ) + 1 ∣ Y ∣ ∑ y ∈ Y min ⁡ x ∈ X d ( x , y ) d_{AH} = \frac{1}{|X|}\sum_{x \in X} \min_{y \in Y} d(x,y) + \frac{1}{|Y|}\sum_{y \in Y} \min_{x \in X} d(x,y) dAH=X1xXyYmind(x,y)+Y1yYxXmind(x,y)

这里的两项分别对两个点集中点的数量计算了平均最短距离。

这一形式仍然满足前面四条属性中的前三条,但是不再满足第四条了。并且也因此,Hausdorff 距离对于两个集合中的任意点都是可微分的。

Y Y Y 表示包含真值点坐标的集合, X X X 作为模型的预测。理想情况下,可以会使用平均 Hausdorff 距离作为损失函数用于训练过程,但是其作为损失函数存在限制。由于 FCN 风格的模型通常使用预测图上的高的激活位置指示目标中心,通常并不会直接返回像素坐标。为了使得这样情况可以正常优化,必须保证损失函数对于模型输出时可微分的。而上面直接基于坐标的形式就不行了。

Weighted Hausdorff Distance

于是,在这篇论文中提出了一个新的改进版本的 Hausdorff 距离,即加权 Hausdorff 距离:

d W H ( p , Y ) = 1 ∑ x ∈ Ω p x + ϵ ∑ x ∈ Ω p x min ⁡ y ∈ Y d ( x , y ) + 1 ∣ Y ∣ ∑ y ∈ Y M α x ∈ Ω [ p x d ( x , y ) + ( 1 − p x ) d m a x ] M α a ∈ A [ f ( a ) ] = ( 1 ∣ A ∣ ∑ a ∈ A f α ( a ) ) 1 α \begin{align} d_{WH}(p,Y) & = \frac{1}{\sum_{x \in \Omega}p_x +\epsilon} \sum_{x \in \Omega} p_x \min_{y \in Y} d(x,y) + \frac{1}{|Y|} \sum_{y \in {Y}} \underset{x \in \Omega}{M_{\alpha}}[p_x d(x,y) + (1-p_x)d_{max}] \\ \underset{a \in A}{M_{\alpha}}[f(a)] & = (\frac{1}{|A| \sum_{a \in A} f^\alpha(a)})^\frac{1}{\alpha} \end{align} dWH(p,Y)aAMα[f(a)]=xΩpx+ϵ1xΩpxyYmind(x,y)+Y1yYxΩMα[pxd(x,y)+(1px)dmax]=(AaAfα(a)1)α1

第一部分计算了每个 x x x 与最近的 y y y 的距离,使用 x 处的预测值对平均后的结果进行加权。这里可以看做是一个加权的平均距离。

而第二部分则把针对位置 x x x 处理的距离设定为了使用 p x p_x px 加权的 d ( x , y ) d(x,y) d(x,y) 和图像中可能的最大距离 d m a x d_{max} dmax(即对角线长度)的组合。这个式子:

  • 在极端情形,即 p x = 0 p_x=0 px=0 时,此时对应的含义就成了图像对角线,因为此时距离可以理解为与任意的 y y y 都等于最大距离。而当 p x = 1 p_x=1 px=1 时,此时则仅考虑 d ( x , y ) d(x,y) d(x,y),即实际的距离。
  • 由于 p x p_x px 实际并不是二值状态,而是一个 0~1 之间的变化值,所以这可以表示一种连续集合与离散集合的距离形式。

这里特别的是 M α a ∈ A [ f ( a ) ] \underset{a \in A}{M_{\alpha}}[f(a)] aAMα[f(a)] 是广义平均,具体可见:

  • https://baike.baidu.com/item/幂平均/6685661
  • https://en.wikipedia.org/wiki/Generalized_mean

广义平均在特殊参数的设定下可以实现对于最大和最小函数的逼近。但是广义平均本身却是可微的。

代码解析

代码来自:https://github.com/javiribera/locating-objects-without-bboxes/blob/master/object-locator/losses.py

先定义一些基本计算函数,包括计算成对欧氏距离的 cdist 和计算广义平均的 generaliz_mean

def _assert_no_grad(variables):for var in variables:assert not var.requires_grad, \"nn criterions don't compute the gradient w.r.t. targets - please " \"mark these variables as volatile or not requiring gradients"def cdist(x, y):"""Compute distance between each pair of the two collections of inputs.:param x: Nxd Tensor:param y: Mxd Tensor:res: NxM matrix where dist[i,j] is the norm between x[i,:] and y[j,:],i.e. dist[i,j] = ||x[i,:]-y[j,:]||"""differences = x.unsqueeze(1) - y.unsqueeze(0)distances = torch.sum(differences**2, -1).sqrt()return distancesdef generaliz_mean(tensor, dim, p=-9, keepdim=False):"""The generalized mean. It corresponds to the minimum when p = -inf.https://en.wikipedia.org/wiki/Generalized_mean:param tensor: Tensor of any dimension.:param dim: (int or tuple of ints) The dimension or dimensions to reduce.:param keepdim: (bool) Whether the output tensor has dim retained or not.:param p: (float<0)."""assert p < 0res= torch.mean((tensor + 1e-6)**p, dim, keepdim=keepdim)**(1./p)return res

Average Hausdorff Distance

def averaged_hausdorff_distance(set1, set2, max_ahd=np.inf):"""Compute the Averaged Hausdorff Distance functionbetween two unordered sets of points (the function is symmetric).Batches are not supported, so squeeze your inputs first!:param set1: Array/list where each row/element is an N-dimensional point.:param set2: Array/list where each row/element is an N-dimensional point.:param max_ahd: Maximum AHD possible to return if any set is empty. Default: inf.:return: The Averaged Hausdorff Distance between set1 and set2."""if len(set1) == 0 or len(set2) == 0:return max_ahdset1 = np.array(set1)set2 = np.array(set2)assert set1.ndim == 2, 'got %s' % set1.ndimassert set2.ndim == 2, 'got %s' % set2.ndimassert set1.shape[1] == set2.shape[1], \'The points in both sets must have the same number of dimensions, got %s and %s.'\% (set2.shape[1], set2.shape[1])d2_matrix = pairwise_distances(set1, set2, metric='euclidean')res = np.average(np.min(d2_matrix, axis=0)) + \np.average(np.min(d2_matrix, axis=1))return resclass AveragedHausdorffLoss(nn.Module):def __init__(self):super(nn.Module, self).__init__()def forward(self, set1, set2):"""Compute the Averaged Hausdorff Distance function between two unordered sets of points (the function is symmetric).Batches are not supported, so squeeze your inputs first!:param set1: Tensor where each row is an N-dimensional point.:param set2: Tensor where each row is an N-dimensional point.:return: The Averaged Hausdorff Distance between set1 and set2."""assert set1.ndimension() == 2, 'got %s' % set1.ndimension()assert set2.ndimension() == 2, 'got %s' % set2.ndimension()assert set1.size()[1] == set2.size()[1], \'The points in both sets must have the same number of dimensions, got %s and %s.'\% (set2.size()[1], set2.size()[1])d2_matrix = cdist(set1, set2)# Modified Chamfer Lossterm_1 = torch.mean(torch.min(d2_matrix, 1)[0])term_2 = torch.mean(torch.min(d2_matrix, 0)[0])res = term_1 + term_2return res

平均形式的实现方式非常简单,关键在于计算点之间的成对距离矩阵,之后沿着两个轴(以不同的点集作为基准)计算取最小和均值操作。最终的加和即为最终的距离。

注意这里的实现里没有为 batch 形式提供支持。而且考虑到不同样本对应的点集中点数量的差异,导致无法直接将不同的数据堆叠到一起。

Werighted Hausdorff Distance

class WeightedHausdorffDistance(nn.Module):def __init__(self,resized_height, resized_width,p=-9,return_2_terms=False,device=torch.device('cpu')):""":param resized_height: Number of rows in the image.:param resized_width: Number of columns in the image.:param p: Exponent in the generalized mean. -inf makes it the minimum.:param return_2_terms: Whether to return the 2 termsof the WHD instead of their sum.Default: False.:param device: Device where all Tensors will reside."""super(nn.Module, self).__init__()# Prepare all possible (row, col) locations in the imageself.height, self.width = resized_height, resized_widthself.resized_size = torch.tensor([resized_height,resized_width],dtype=torch.get_default_dtype(),device=device)self.max_dist = math.sqrt(resized_height**2 + resized_width**2)self.n_pixels = resized_height * resized_widthself.all_img_locations = torch.from_numpy(cartesian([np.arange(resized_height),np.arange(resized_width)]))# Convert to appropiate typeself.all_img_locations = self.all_img_locations.to(device=device,dtype=torch.get_default_dtype())self.return_2_terms = return_2_termsself.p = pdef forward(self, prob_map, gt, orig_sizes):"""Compute the Weighted Hausdorff Distance functionbetween the estimated probability map and ground truth points.The output is the WHD averaged through all the batch.:param prob_map: (B x H x W) Tensor of the probability map of the estimation.B is batch size, H is height and W is width.Values must be between 0 and 1.:param gt: List of Tensors of the Ground Truth points.Must be of size B as in prob_map.Each element in the list must be a 2D Tensor,where each row is the (y, x), i.e, (row, col) of a GT point.:param orig_sizes: Bx2 Tensor containing the size of the original images.B is batch size.The size must be in (height, width) format.:return: Single-scalar Tensor with the Weighted Hausdorff Distance.If self.return_2_terms=True, then return a tuple containingthe two terms of the Weighted Hausdorff Distance."""_assert_no_grad(gt)assert prob_map.dim() == 3, 'The probability map must be (B x H x W)'assert prob_map.size()[1:3] == (self.height, self.width), \'You must configure the WeightedHausdorffDistance with the height and width of the ' \'probability map that you are using, got a probability map of size %s'\% str(prob_map.size())batch_size = prob_map.shape[0]assert batch_size == len(gt)terms_1 = []terms_2 = []for b in range(batch_size):# One by oneprob_map_b = prob_map[b, :, :]gt_b = gt[b]orig_size_b = orig_sizes[b, :]norm_factor = (orig_size_b/self.resized_size).unsqueeze(0)n_gt_pts = gt_b.size()[0]# Corner case: no GT pointsif gt_b.ndimension() == 1 and (gt_b < 0).all().item() == 0:terms_1.append(torch.tensor([0],dtype=torch.get_default_dtype()))terms_2.append(torch.tensor([self.max_dist],dtype=torch.get_default_dtype()))continue# Pairwise distances between all possible locations and the GTed locationsn_gt_pts = gt_b.size()[0]normalized_x = norm_factor.repeat(self.n_pixels, 1) *\self.all_img_locationsnormalized_y = norm_factor.repeat(len(gt_b), 1)*gt_bd_matrix = cdist(normalized_x, normalized_y)  # HWxN# Reshape probability map as a long column vector,# and prepare it for multiplicationp = prob_map_b.view(prob_map_b.nelement())n_est_pts = p.sum()p_replicated = p.view(-1, 1).repeat(1, n_gt_pts)# Weighted Hausdorff Distanceterm_1 = (1 / (n_est_pts + 1e-6)) * \torch.sum(p * torch.min(d_matrix, 1)[0])  # HWxN -> HW -> 1weighted_d_matrix = (1 - p_replicated)*self.max_dist + p_replicated*d_matrixminn = generaliz_mean(weighted_d_matrix,p=self.p,dim=0, keepdim=False)  # HWxN -> Nterm_2 = torch.mean(minn)terms_1.append(term_1)terms_2.append(term_2)terms_1 = torch.stack(terms_1)terms_2 = torch.stack(terms_2)if self.return_2_terms:res = terms_1.mean(), terms_2.mean()else:res = terms_1.mean() + terms_2.mean()return res

由于这里不同样本对应的点集不同,所以无法直接利用 batch 形式的计算,需要对每个样本单独计算,最后整体平均。这里同时考虑了实际模型输入输出中图像形状的变化,为了将距离关系对应于原图,所以这里利用放缩后的尺寸和原始尺寸之间计算了一个坐标放缩因子用于调整输出图中坐标和真值坐标。

之后针对两项分别进行计算,对于第一项,其计算方式和 Average Hausdorff Distance 一致,而第二项则根据公式进行了变换。这类时候首先计算了加权形式的距离矩阵,利用不同位置上的预测值对最大距离(预测图的对角线长度)和前面计算的距离矩阵进行加权求和。之后对加权距离矩阵计算广义平均,利用负指数,获得近似的最小值。最终对所有真值点对应的近似平均最小距离。

参考资料

  • 快速计算图像之间的 Hausdorff 距离,通过引入距离图而避免了双重遍历的问题:https://cs.stackexchange.com/questions/117989/hausdorff-distance-between-two-binary-images-according-to-distance-maps
  • 计算距离图的例子:https://docs.monai.io/en/stable/_modules/monai/metrics/utils.html#get_surface_distance

http://www.ppmy.cn/ops/151339.html

相关文章

使用 ChatGPT 生成和改进你的论文

文章目录 零、前言一、操作引导二、 生成段落或文章片段三、重写段落四、扩展内容五、生成大纲内容六、提高清晰度和精准度七、解决特定的写作挑战八、感受 零、前言 我是虚竹哥&#xff0c;目标是带十万人玩转ChatGPT。 ChatGPT 是一个非常有用的工具&#xff0c;可以帮助你…

详解Rust 中 String 和 str 的用途与区别

文章目录 1. 基本定义1.1 String1.2 str 2. 存储位置与内存模型2.1 String2.2 str 3. 用法与区别4. 使用场景4.1 使用 String 的场景4.2 使用 str 的场景 5. String 和 str 的关系6. 代码示例分析6.1 从 &str 创建 String6.2 从 String 获取 &str6.3 拼接字符串6.4 静态…

接口测试自动化实战(超详细的)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 前言 自从看到阿里云性能测试 PTS 接口测试开启免费公测&#xff0c;就想着跟大家分享交流一下如何实现高效的接口测试为出发点&#xff0c;本文包含了我在接口测…

零基础入门uniapp Vue3组合式API版本

前言&#xff1a;小程序学习笔记&#xff0c;课程来源up主咸虾米_。仅记录笔记&#xff0c;大家想学习可以去关注他。 1.已安装HBuider X&#xff08;目前是4.36版本&#xff09;&#xff0c;微信开发者工具&#xff08;但还没注册小程序码&#xff09;&#xff0c;相关配置OK…

HTML5 Canvas实现的跨年烟花源代码

以下是一份基于HTML5 Canvas实现的跨年烟花源代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml">…

mongodb详解二:基础操作

基础操作 数据库操作collection操作查看表插入数据查找数据 数据库操作 1.创建数据库 use test_db;如果没有数据库&#xff0c;use命令会新建一个&#xff1b;有的话&#xff0c;会切换到这个数据库 2.查看数据库 show dbs;collection操作 查看表 show tables;插入数据 …

模块化架构与微服务架构,哪种更适合桌面软件开发?

前言 在现代软件开发中&#xff0c;架构设计扮演着至关重要的角色。两种常见的架构设计方法是模块化架构与微服务架构。它们各自有独特的优势和适用场景&#xff0c;尤其在C#桌面软件开发领域&#xff0c;模块化架构往往更加具有实践性。本文将对这两种架构进行对比&#xff0…

ubuntu18.04开发环境下samba服务器的搭建

嵌入式linux的发展很快&#xff0c;最近准备在一个新项目上采用新一代的linux核心板&#xff0c;发现linux内核的版本已经更新到5.4以上甚至6.0以上&#xff1b;之前常用的linux内核版本是2.6.4&#xff0c;虽然在某些项目上还能用但是明显跟不上时代的步伐了&#xff0c;所以要…