# 手把手用Python实现Boundary IoU:从理论公式到实战落地的深度解析
在图像分割这个技术密集的领域,评估指标就像一把尺子,它决定了我们衡量模型好坏的“度量衡”。长久以来,Mask IoU(交并比)一直是分割任务中无可争议的“金标准”。然而,随着模型精度的不断提升和应用场景的日益复杂,开发者们逐渐发现这把“尺子”在某些维度上存在盲区——它对大物体边界上的细微误差反应迟钝,却又对小物体的微小偏差过于严苛。这就像用一把刻度粗糙的尺子去测量精密零件,结果难免失真。
正是在这样的背景下,Boundary IoU应运而生。它并非要完全取代Mask IoU,而是提供了一种**互补的、对边界质量高度敏感的评估视角**。对于致力于提升分割模型边缘保真度的开发者,或是正在撰写相关论文、需要更细致评估结果的研究者而言,深入理解并亲手实现Boundary IoU,已经成为一项极具价值的技能。本文将彻底抛开对原始论文的简单复述,从一个实践者的角度,带你从最底层的公式推导开始,一步步用NumPy和PyTorch构建出高效、可靠的Boundary IoU计算模块,并深入探讨其在LVIS、COCO等真实数据集评估流程中的整合技巧与参数调优经验。
## 1. 重新审视分割评估:为何需要Boundary IoU?
在跳入代码之前,我们有必要先厘清一个根本问题:现有的Mask IoU到底“漏”掉了什么?理解这一点,是掌握Boundary IoU精髓的关键。
想象一下,你有一个分割模型,其任务是精确勾勒出图像中每个物体的轮廓。Mask IoU的计算方式是预测掩码(Prediction Mask)与真实掩码(Ground Truth Mask)交集与并集的比值。这个公式非常直观,但它隐含了一个重要的特性:**所有像素在计算中的权重是相等的**。
对于一个面积很大的物体(比如占据图像一半的建筑物),其内部像素数量随着物体尺寸呈**平方级**增长,而边界像素的数量只呈**线性**增长。在计算IoU时,内部海量的、极易分类正确的像素会“淹没”边界上那些相对稀少但分类困难的像素所贡献的误差。结果就是,即使模型的边界预测得歪歪扭扭,只要内部区域预测准确,整体Mask IoU依然可以保持在一个很高的水平。这就是Mask IoU对**大物体边界不敏感**的根源。
反之,对于一个小物体(比如远处的一个行人),其总像素数很少,边界像素占比相对较高。此时,边界上哪怕几个像素的偏差,也会导致IoU值急剧下降。这种对小错误的过度惩罚,使得Mask IoU在评估小物体分割质量时显得过于严苛,甚至可能无法公平地比较不同模型在小物体上的表现。
> **提示**:这种特性并非Bug,而是Mask IoU作为面积比指标的固有属性。它衡量的是“区域”的重合度,而非“轮廓”的贴合度。许多下游任务,如医疗影像中的病灶提取、自动驾驶中的障碍物边缘感知,恰恰对边界精度有着极高的要求。
Boundary IoU的设计哲学,正是将评估的焦点从“面”转移到“线”。它不再关心物体内部的所有像素,而是**只关注真实轮廓和预测轮廓附近一个狭窄带状区域内的像素重合情况**。这个带状区域的宽度由一个关键参数 `d` 来控制。通过这种方式,无论物体大小,评估的“注意力”都平等地放在了边界质量上。
## 2. 核心公式拆解与NumPy实现
理解了“为什么”之后,我们来看“是什么”。Boundary IoU的公式是其实践的基石,我们将逐部分拆解,并用最基础的NumPy库来实现它,确保你对每一个计算步骤都了然于胸。
### 2.1 公式的直观理解
Boundary IoU的核心公式如下:
**Boundary-IoU(G, P) = |(G_d ∩ G) ∩ (P_d ∩ P)| / |(G_d ∩ G) ∪ (P_d ∩ P)|**
初看有些复杂,我们将其分解:
- **G** 和 **P**:分别代表真实掩码(Ground Truth)和预测掩码(Prediction),通常是二值图像(0为背景,1为前景)。
- **G_d** 和 **P_d**:代表分别以G和P的轮廓为中心,向外(和向内)扩张 `d` 个像素后形成的“带状”区域。注意,这是一个**形态学膨胀**操作。
- **G_d ∩ G**:这个交集操作是Boundary IoU区别于类似指标(如Trimap IoU)的精妙之处。它意味着我们只取**膨胀后区域中,仍然位于原始真实物体内部**的那部分。这确保了带状区域是“贴”在真实物体边界内侧的,避免了将背景区域纳入计算。
- 同理,**P_d ∩ P** 是预测掩码边界内侧的带状区域。
- 分子是这两个“内侧带状区域”的交集,分母是它们的并集。
简单来说,**Boundary IoU计算的是“真实物体边界内侧d像素带”与“预测物体边界内侧d像素带”这两个区域之间的IoU**。
### 2.2 关键步骤的NumPy实现
我们将实现过程分解为几个可复用的函数。首先,我们需要一个函数来计算二值掩码的边界距离图(Distance Transform),这是高效获取 `G_d ∩ G` 区域的基础。
```python
import numpy as np
from scipy import ndimage
import cv2
def compute_boundary_region(mask, distance):
"""
计算掩码边界内侧指定距离的区域 (即 mask_d ∩ mask)。
参数:
mask: 二维二值数组 (H, W), 前景为1,背景为0。
distance: 带状区域的宽度(像素)。
返回:
boundary_region: 二维二值数组,边界内侧区域为1,其余为0。
"""
# 1. 计算掩码的欧氏距离变换
# 距离变换计算每个前景像素到最近背景像素的距离
dist_transform = ndimage.distance_transform_edt(mask)
# 2. 找出距离小于等于d的像素,这些像素位于物体内部且靠近边界
# 注意:这里我们取 `dist_transform > 0` 确保像素在物体内部,
# 同时 `dist_transform <= distance` 确保像素距离边界不超过d
boundary_region = np.logical_and(dist_transform > 0, dist_transform <= distance)
# 将布尔数组转换为整数 (0或1)
return boundary_region.astype(np.uint8)
```
接下来,我们实现完整的Boundary IoU计算函数。这里假设输入是单个物体的掩码。
```python
def boundary_iou_single(gt_mask, pred_mask, d):
"""
计算单个实例的Boundary IoU。
参数:
gt_mask: 真实掩码,二维二值数组 (H, W)。
pred_mask: 预测掩码,二维二值数组 (H, W)。
d: 边界带状区域的宽度(像素)。
返回:
biou: Boundary IoU值,标量。
"""
# 确保输入是二值掩码
gt_binary = (gt_mask > 0).astype(np.uint8)
pred_binary = (pred_mask > 0).astype(np.uint8)
# 计算边界内侧区域
gt_boundary = compute_boundary_region(gt_binary, d)
pred_boundary = compute_boundary_region(pred_binary, d)
# 计算交集和并集
intersection = np.logical_and(gt_boundary, pred_boundary).sum()
union = np.logical_or(gt_boundary, pred_boundary).sum()
# 避免除零错误
if union == 0:
return 0.0
biou = intersection / union
return biou
```
### 2.3 处理多实例与批数据
在实际数据集中,一张图像通常包含多个实例。我们需要一个能够批量处理、并自动匹配预测和真实实例的函数。这里我们采用一种常见的基于IoU的贪婪匹配策略。
```python
def compute_boundary_iou_for_image(gt_masks, pred_masks, d, iou_threshold=0.5):
"""
计算一张图片上所有匹配实例对的Boundary IoU。
参数:
gt_masks: 列表,包含多个真实实例掩码,每个掩码为二维数组。
pred_masks: 列表,包含多个预测实例掩码,每个掩码为二维数组。
d: 边界距离参数。
iou_threshold: 用于匹配实例的Mask IoU阈值。
返回:
matched_biou_list: 列表,包含所有成功匹配的实例对的Boundary IoU值。
match_pairs: 列表,包含匹配的(gt_idx, pred_idx)对。
"""
num_gt = len(gt_masks)
num_pred = len(pred_masks)
if num_gt == 0 or num_pred == 0:
return [], []
# 计算所有GT和Pred之间的Mask IoU矩阵
iou_matrix = np.zeros((num_gt, num_pred))
for i in range(num_gt):
for j in range(num_pred):
# 简单的Mask IoU计算
intersection = np.logical_and(gt_masks[i], pred_masks[j]).sum()
union = np.logical_or(gt_masks[i], pred_masks[j]).sum()
iou_matrix[i, j] = intersection / union if union > 0 else 0
# 贪婪匹配
matched_biou_list = []
match_pairs = []
# 按IoU从高到低匹配
while True:
max_iou = iou_matrix.max()
if max_iou < iou_threshold:
break
gt_idx, pred_idx = np.unravel_index(iou_matrix.argmax(), iou_matrix.shape)
# 计算这对匹配的Boundary IoU
biou = boundary_iou_single(gt_masks[gt_idx], pred_masks[pred_idx], d)
matched_biou_list.append(biou)
match_pairs.append((gt_idx, pred_idx))
# 将已匹配的行和列置为-1,避免重复匹配
iou_matrix[gt_idx, :] = -1
iou_matrix[:, pred_idx] = -1
return matched_biou_list, match_pairs
```
这个实现为我们提供了一个坚实的基础。然而,当处理大规模数据集或需要与深度学习框架深度集成时,纯NumPy的实现可能在效率上遇到瓶颈。接下来,我们将探索如何利用PyTorch的向量化操作和GPU加速,打造一个更高效的版本。
## 3. 高性能PyTorch实现与GPU加速
对于需要处理成千上万张图片、数万个实例的模型评估,计算速度至关重要。PyTorch不仅提供了GPU加速能力,其张量操作也便于我们进行批处理和向量化计算。我们将重构上述逻辑,使其完全在PyTorch环境中运行。
### 3.1 利用PyTorch实现距离变换与边界区域计算
PyTorch本身没有直接的距离变换函数,但我们可以利用卷积操作进行近似,或者结合`scipy`在CPU上计算后再移至GPU。为了追求极致的GPU利用率,这里介绍一种基于二值形态学膨胀的近似方法,它对于较小的 `d` 值非常高效。
```python
import torch
import torch.nn.functional as F
def compute_boundary_region_torch(mask_batch, d):
"""
使用PyTorch计算一批掩码的边界内侧区域。
采用形态学膨胀近似法,适用于d较小的情况。
参数:
mask_batch: 四维张量 (B, 1, H, W),二值掩码。
d: 边界距离。
返回:
boundary_region: 四维张量 (B, 1, H, W),边界内侧区域。
"""
B, C, H, W = mask_batch.shape
device = mask_batch.device
# 创建一个半径为d的圆形结构元素(卷积核)
# 这里简化处理,创建一个(2d+1) x (2d+1)的全1矩阵作为近似
kernel_size = 2 * d + 1
kernel = torch.ones((1, 1, kernel_size, kernel_size), device=device) / (kernel_size ** 2)
# 对掩码进行膨胀操作:通过卷积判断邻居中是否有前景像素
# 由于是二值图像,我们可以用最大池化来模拟膨胀,但更准确的方式是使用自定义卷积
# 这里使用一个技巧:计算掩码的“距离场”近似
# 步骤1: 计算掩码的补集(背景)
background = 1 - mask_batch
# 步骤2: 计算背景的距离变换近似(通过多次膨胀/腐蚀的迭代实现较慢,此处用另一种方法)
# 一个更实用的方法是直接在CPU上用scipy计算,然后传回GPU。为了示例,我们展示一个简化版本:
# 假设d很小,我们直接使用一个dilation操作来获取边界区域的外缘,然后与原始掩码求差。
# 膨胀操作(使用最大池化模拟)
padded = F.pad(mask_batch, (d, d, d, d), mode='constant', value=0)
# 使用unfold进行滑动窗口操作,检查窗口中是否有前景
unfolded = F.unfold(padded, kernel_size=(kernel_size, kernel_size))
dilated = (unfolded.max(dim=1, keepdim=True)[0] > 0).float()
# 重新折叠回图像形状
dilated = F.fold(dilated, output_size=(H, W), kernel_size=(1, 1))
# 边界区域 = 原始掩码 AND (膨胀后的掩码)
# 但我们需要的是“内侧”区域,所以应该是:原始掩码 AND (NOT(腐蚀后的掩码))
# 腐蚀操作
eroded = (F.unfold(padded, kernel_size=(kernel_size, kernel_size)).min(dim=1, keepdim=True)[0] > 0).float()
eroded = F.fold(eroded, output_size=(H, W), kernel_size=(1, 1))
# 内侧边界区域:在原始掩码内,但不在腐蚀后的掩码内(即距离边界d以内的区域)
boundary_region = mask_batch * (1 - eroded)
return boundary_region
```
> **注意**:上述基于膨胀/腐蚀的近似方法在 `d` 较大时可能不够精确,且计算开销随 `d` 增大而增加。在生产环境中,如果追求精确性,一种常见的做法是将掩码数据在CPU上使用`scipy.ndimage.distance_transform_edt`计算距离变换,然后将结果转换为PyTorch张量移至GPU进行后续交集并集运算。这实现了精度和灵活性的平衡。
### 3.2 批量化Boundary IoU计算
有了边界区域计算函数,我们可以实现一个完全向量化的批处理Boundary IoU计算。
```python
def batch_boundary_iou(gt_masks, pred_masks, d):
"""
批量计算Boundary IoU。
假设gt_masks和pred_masks已经是一一匹配好的,形状为 (B, 1, H, W)。
参数:
gt_masks: 真实掩码张量。
pred_masks: 预测掩码张量。
d: 边界距离。
返回:
biou_scores: 形状为 (B,) 的Boundary IoU分数张量。
"""
# 计算边界区域
gt_boundary = compute_boundary_region_torch(gt_masks, d)
pred_boundary = compute_boundary_region_torch(pred_masks, d)
# 计算交集和并集
intersection = (gt_boundary * pred_boundary).flatten(start_dim=1).sum(dim=1)
union = ((gt_boundary + pred_boundary) > 0).flatten(start_dim=1).sum(dim=1).float()
# 避免除零
biou_scores = intersection / (union + 1e-7)
return biou_scores
```
### 3.3 与现有评估流程的整合:以Mask R-CNN为例
许多开发者使用像`detectron2`或`mmdetection`这样的框架进行实例分割。我们需要将Boundary IoU的计算无缝嵌入到现有的评估循环中。以下是一个概念性的整合示例:
假设我们有一个在COCO数据集上训练的Mask R-CNN模型,我们想在验证集上同时计算标准的AP(基于Mask IoU)和Boundary AP。
```python
# 伪代码,展示整合思路
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
import numpy as np
class BoundaryCOCOEvaluator:
def __init__(self, coco_gt, d=15):
self.coco_gt = coco_gt
self.d = d # 设置边界距离参数
self.biou_scores = [] # 存储所有匹配实例的Boundary IoU
def accumulate(self, predictions):
"""
累积单张图片的预测结果。
predictions: 列表,每个元素是一个字典,包含'bbox', 'segmentation', 'score', 'category_id'等。
"""
img_id = predictions[0]['image_id'] if predictions else None
gt_ann_ids = self.coco_gt.getAnnIds(imgIds=img_id)
gt_anns = self.coco_gt.loadAnns(gt_ann_ids)
# 将COCO格式的RLE掩码解码为二值图像
gt_masks = [self.coco_gt.annToMask(ann) for ann in gt_anns]
pred_masks = [self._decode_rle(pred['segmentation']) for pred in predictions]
# 使用基于IoU的匹配(可以复用之前的compute_boundary_iou_for_image逻辑)
matched_biou_list, _ = compute_boundary_iou_for_image(gt_masks, pred_masks, self.d)
self.biou_scores.extend(matched_biou_list)
def summarize(self):
"""计算并返回Boundary AP等指标。"""
if not self.biou_scores:
return {}
biou_array = np.array(self.biou_scores)
# 这里需要根据你的评估协议来计算AP
# 例如,可以设定多个IoU阈值(如0.5:0.05:0.95),统计Boundary IoU超过阈值的比例
# 以下是一个简化示例,计算平均Boundary IoU
mean_biou = biou_array.mean()
return {
'Boundary-mIoU': mean_biou,
# 可以添加更多统计量,如在不同物体大小区域上的平均BIoU
}
def _decode_rle(self, segmentation):
# 将RLE或多边形解码为二值掩码
# 具体实现取决于你的预测格式
pass
```
在实际操作中,你可能需要修改评估脚本,在标准的COCO评估循环中同时调用这个累积器,最后输出两套指标:传统的AP和基于Boundary IoU的新指标。
## 4. 关键参数 `d` 的调优与实践经验
参数 `d` 定义了边界带的宽度,是Boundary IoU的“灵敏度旋钮”。选择不当的 `d` 会导致指标失去意义。原始论文建议将其设置为图像对角线长度的约2%(对于COCO/LVIS这类约640x480的图像,`d≈15`像素),对于Cityscapes等高分辨率图像,则建议固定像素值(如15像素)或对角线长度的0.5%。
### 4.1 如何为你的数据集确定 `d`
盲目照搬论文参数可能不适用于你的特定数据。以下是确定合适 `d` 值的实践流程:
1. **分析标注一致性**:从数据集中随机抽取一批有重复标注的图片(如果存在)。计算同一物体不同标注之间的Boundary IoU,绘制其随 `d` 变化的曲线。曲线开始趋于平缓的 `d` 值,可以视为标注本身固有噪声的下界。`d` 不应小于这个值,否则指标会过度惩罚合理的标注差异。
2. **考察模型性能范围**:在验证集上运行你的基准模型,计算预测与真实标注之间的Boundary IoU分布。选择一个 `d`,使得模型预测的**中位数Boundary IoU**落在0.7到0.9之间。如果中位数太低(如<0.5),说明 `d` 太小或模型边界分割质量太差;如果中位数太高(如>0.95),说明 `d` 太大,指标可能已退化为对内部像素不敏感的普通IoU。
3. **进行敏感性测试**:模拟论文中的误差类型(如边界高斯噪声、整体偏移),观察Boundary IoU值随误差程度的变化是否平滑、敏感。一个好的 `d` 应该能在误差较小时给出梯度变化,在误差较大时趋于0。
下表展示了在不同 `d` 值下,Boundary IoU行为的变化趋势,可以帮助你进行定性判断:
| `d` 值选择 | 对边界误差的敏感性 | 对小物体的惩罚 | 对大物体边界的关注度 | 潜在问题 |
|------------|-------------------|----------------|---------------------|----------|
| **过小 (如 d=3)** | 极高 | 非常严苛,易受标注噪声影响 | 极高 | 数值不稳定,波动大,可能无法区分中等质量的预测 |
| **适中 (如 d=15)** | 高 | 合理,与Mask IoU对小物体的评估接近 | 高,能有效暴露大物体边界问题 | 需要根据图像分辨率调整 |
| **过大 (如 d=50)** | 低 | 很弱,几乎不惩罚小物体边界误差 | 低,逐渐退化为Mask IoU | 失去对边界质量的特异性评估能力 |
### 4.2 与Mask IoU的组合使用策略
Boundary IoU并非完美无缺。其一个已知的局限性是:它只评估边界附近 `d` 像素内的区域。考虑一个极端情况:预测是一个紧贴真实边界的**细环**,而真实掩码是**实心圆**。此时,Boundary IoU可能很高,因为边界带重合得很好,但Mask IoU会很低,因为内部区域完全丢失。这种预测显然是错误的。
因此,论文作者建议将Boundary IoU与Mask IoU**结合使用**,取两者的**最小值**作为最终的分割质量评分(记为 `min(IoU, BIoU)`)。这种组合方式确保了评估同时兼顾了区域整体重合度和边界局部精度。
```python
def combined_segmentation_quality(gt_mask, pred_mask, d):
"""
计算结合了Mask IoU和Boundary IoU的分割质量分数。
"""
# 计算Mask IoU
intersection = np.logical_and(gt_mask, pred_mask).sum()
union = np.logical_or(gt_mask, pred_mask).sum()
mask_iou = intersection / union if union > 0 else 0
# 计算Boundary IoU
boundary_iou = boundary_iou_single(gt_mask, pred_mask, d)
# 返回两者中的较小值
return min(mask_iou, boundary_iou)
```
在计算AP时,就用这个组合分数去判断一个预测是TP(True Positive)还是FP(False Positive)。这种策略被用于计算 **Boundary AP** 和 **Boundary PQ**。
## 5. 在LVIS和COCO数据集上的实战技巧与坑点指南
将理论应用于大规模标准数据集时,会遇到一些具体的工程挑战。这里分享在LVIS和COCO数据集上应用Boundary IoU评估时积累的一些经验。
### 5.1 数据预处理:掩码格式与分辨率统一
COCO和LVIS数据集提供的标注格式是**RLE(Run-Length Encoding)** 或**多边形(Polygon)**。而我们的计算函数需要二值掩码数组。
* **解码效率**:使用`pycocotools`的`decode`函数将RLE解码为二值掩码是标准做法。但要注意,对于非常多的实例,逐条解码可能成为性能瓶颈。可以考虑批量解码或使用更高效的库。
* **分辨率对齐**:确保预测掩码和真实掩码具有相同的空间分辨率。如果你的模型输出是原图分辨率,那很好。如果输出是其他分辨率(如RoIAlign后的固定大小),则需要通过插值将其**上采样(Upsample)** 到原图大小。**双线性插值**后接阈值化(如0.5)是常用方法,但这会引入平滑效应,轻微影响边界锐度。
```python
import torch.nn.functional as F
def resize_and_binarize_mask(mask_tensor, target_size):
"""
将掩码张量调整到目标大小并二值化。
参数:
mask_tensor: 输入掩码,(1, H, W) 或 (H, W),值在[0,1]之间。
target_size: 目标尺寸 (height, width)。
返回:
binary_mask: 二值化的目标尺寸掩码。
"""
# 调整大小
resized = F.interpolate(mask_tensor.unsqueeze(0), size=target_size, mode='bilinear', align_corners=False)
# 二值化
binary_mask = (resized.squeeze(0) > 0.5).float()
return binary_mask
```
### 5.2 处理小物体与极端情况
LVIS数据集包含大量小物体,这给Boundary IoU计算带来了特殊挑战。
* **`d` 值可能超过物体半径**:对于非常小的物体,设定的 `d` 值可能大于物体本身的半径。此时,`compute_boundary_region`函数计算出的 `boundary_region` 可能会覆盖整个物体(因为 `dist_transform <= d` 的条件对物体内所有像素都成立)。在这种情况下,Boundary IoU会退化为Mask IoU。**这是符合设计预期的**,意味着对于极小物体,我们不再苛求其边界精度,而是回退到区域重合度的评估。
* **空掩码处理**:无论是预测还是真实标注,都可能出现空掩码(全零)。在计算前必须过滤掉这些情况,否则会导致除零错误或无意义的匹配。
* **计算稳定性**:当并集区域 (`union`) 极小时(例如,两个都非常小的边界带刚好错开),IoU值会剧烈波动。在代码中加入一个极小的epsilon(如`1e-7`)来稳定除法运算是良好的实践。
### 5.3 评估流程的加速与优化
当在完整验证集上运行时,计算Boundary IoU可能比Mask IoU慢一个数量级,主要耗时在距离变换和边界区域的计算上。
* **并行化**:利用PyTorch的批处理能力,一次性处理多个实例,甚至多张图片的实例。
* **近似计算**:对于追求速度的场景,可以考虑用**形态学腐蚀(Erosion)** 来近似“边界内侧区域”。`boundary_region ≈ mask - erosion(mask, d)`。其中`erosion`可以使用`torch.nn.functional.max_pool2d`(对二值图像求局部最小值)来实现,这在GPU上非常快。
* **缓存真实标注的边界区域**:在评估过程中,真实标注 (`gt_masks`) 是不变的。你可以预先为整个数据集的每个真实实例计算好 `gt_boundary` 并缓存起来,在评估每个模型时只需计算 `pred_boundary`,可以节省近一半的计算量。
```python
# 使用腐蚀近似边界区域 (PyTorch GPU版本)
def compute_boundary_region_approx(mask_batch, d):
B, C, H, W = mask_batch.shape
# 使用最大池化进行腐蚀(因为背景为0,前景为1,取最小值即为腐蚀)
# 需要先将mask取反,使前景为0,背景为1,这样最大值池化就是腐蚀
inverted = 1 - mask_batch
# 填充以适应边界
pad = d
inverted_padded = F.pad(inverted, (pad, pad, pad, pad), mode='constant', value=1) # 用背景值填充
# 使用avg_pool2d模拟腐蚀(求局部最小值),通过一个大的kernel
# 更准确的做法是使用min pooling,但PyTorch没有直接的min_pool2d。
# 一个替代方案是使用unfold + min
kernel_size = 2*d + 1
unfolded = F.unfold(inverted_padded, kernel_size=kernel_size, stride=1, padding=0)
eroded_inverted = unfolded.min(dim=1, keepdim=True)[0]
eroded_inverted = F.fold(eroded_inverted, output_size=(H, W), kernel_size=(1,1))
eroded = 1 - eroded_inverted
# 边界区域 = 原始掩码 - 腐蚀后的掩码
boundary_region = mask_batch - eroded
boundary_region = torch.clamp(boundary_region, 0, 1) # 确保值在0-1
return boundary_region
```
经过这些优化,Boundary IoU的计算可以变得足够高效,从而能够常规地纳入你的模型验证和对比实验流程中,为你提供传统Mask IoU无法揭示的、关于模型边界分割能力的深刻洞察。