[PaddleSeg源码阅读] PaddleSeg Validation 中添加 Boundary IoU的计算(3)——添加Boundary IoU

经过前面:
PaddleSeg Validation 中添加 Boundary IoU的计算(1)——val.py文件细节提示
PaddleSeg Validation 中添加 Boundary IoU的计算(2)——inference 部分

相信诸位已经对 PaddleSeg 进行预测已经有了简单的理解,本来如果是随便加的话,直接在inference那个函数后边直接加就行,但是为了优雅一些
在这里插入图片描述
咱们首先打开 PaddleSeg 计算指标的函数文件:
paddleseg/utils/metrics.py

参考这篇 boundary IoU 的计算方式

在文件结尾直接添加:


# GitHub repo: https://github.com/bowenc0221/boundary-iou-api
# Reference: https://gist.github.com/bowenc0221/71f7a02afee92646ca05efeeb14d687d


# General util function to get the boundary of a binary mask.
# 该函数用于获取二进制 mask 的边界
def mask_to_boundary(mask, dilation_ratio=0.05):
    """
    Convert binary mask to boundary mask.
    :param mask (numpy array, uint8): binary mask
    :param dilation_ratio (float): ratio to calculate dilation = dilation_ratio * image_diagonal
    :return: boundary mask (numpy array)
    """
    mask = mask.astype(np.uint8)
    
    h, w = mask.shape
    img_diag = np.sqrt(h ** 2 + w ** 2) # 计算图像对角线长度
    dilation = int(round(dilation_ratio * img_diag))
    if dilation < 1:
        dilation = 1
    # Pad image so mask truncated by the image border is also considered as boundary.
    new_mask = cv2.copyMakeBorder(mask, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=0)
    kernel = np.ones((3, 3), dtype=np.uint8)
    new_mask_erode = cv2.erode(new_mask, kernel, iterations=dilation)
    
    # 因为之前向四周填充了0, 故而这里不再需要四周
    mask_erode = new_mask_erode[1 : h + 1, 1 : w + 1]
    
    # G_d intersects G in the paper.
    return mask - mask_erode




def boundary_iou(gt, dt, dilation_ratio=0.005, cls_num=2):
    """
    Compute boundary iou between two binary masks.
    :param gt (numpy array, uint8): binary mask
    :param dt (numpy array, uint8): binary mask
    :param dilation_ratio (float): ratio to calculate dilation = dilation_ratio * image_diagonal
    :return: boundary iou (float)
    """
    gt = gt[0, 0]
    dt = dt[0]
    gt = gt.numpy().astype(np.uint8)
    dt = dt.numpy().astype(np.uint8)
    
    boundary_iou_list = []
    for i in range(cls_num):
        
        gt_i = (gt == i)
        dt_i = (dt == i)
        gt_boundary = mask_to_boundary(gt_i, dilation_ratio)
        dt_boundary = mask_to_boundary(dt_i, dilation_ratio)
        intersection = ((gt_boundary * dt_boundary) > 0).sum()
        union = ((gt_boundary + dt_boundary) > 0).sum()
		if union < 1:
            boundary_iou_list.append(0)
            continue
        boundary_iou = intersection / union
        boundary_iou_list.append( boundary_iou )
        
    return np.array(boundary_iou_list)

因为这个是函数,不是以下的组件,所以无需添加装饰器:

manager.MODELS
manager.BACKBONES
manager.DATASETS
manager.TRANSFORMS
manager.LOSSES

paddleseg/utils/__init__.py 中是这样导入的

from . import metrics

所以也无需在某个地方

from metrics import boundary_iou

然后就是在:

# paddleseg/core/val.py

# 这里有 metrics 即可
from paddleseg.utils import metrics, TimeAverager, calculate_eta, logger, progbar

直接用函数 boundary_iou 即可,用之前我们再看一下这个类 TimeAverager, 因为我们要用其来计算每一幅图 Boundary IoU 的均值,别看他名字是给时间做均值的,但是Python有一种哲学:“只要你长得像鸭子,那你就是鸭子”
原话是酱紫:

If it walks like a duck and quacks like a duck, it must be a duck

来看 TimeAverager 的源代码:

位置:paddleseg/utils/timer.py

class TimeAverager(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self._cnt = 0             # 一共添加了多少个元素
        self._total_time = 0      # 总共多长时间
        self._total_samples = 0   # 采了多少样? 这个用法其实不明确

    def record(self, usetime, num_samples=None):
        self._cnt += 1                # 每次记录,总数得加1吧
        self._total_time += usetime   # 记录得把总数加上吧
        if num_samples:
        	# 如果碰到了需要的样本,则把样本数加上
            self._total_samples += num_samples

    def get_average(self):
        if self._cnt == 0:
        	# 防止除0错误,直接返回0
            return 0
        return self._total_time / float(self._cnt) # 返回值就是总数 / 总记录数 (均值)

    def get_ips_average(self):
        if not self._total_samples or self._cnt == 0:
        	# 还没开始记录或者采样数为0,直接给0,防止除0错误
            return 0
        # 总采样数 / 总时间 (每多长时间可以采一个样)
        return float(self._total_samples) / self._total_time

粗看一下我们只需要直接将Boundary IoU 视为 usetime 传入 self.record就好了, 同时不使用num_samples,接下里 self.get_average 来计算均值即可

但是,Boundary IoU 可能用不了,因为 boundary_iou 函数 返回的是 np.ndarray,而这里有一个原地操作:

self._total_time += usetime
# 而 self._total_time 初始化为 int 0
self._total_time = 0

但是你当你用的时候,你发现,竟然可以运行,得益于Python是动态的,而且int和np.ndarray可以直接相加的多态hhh

所以,添加一个记录Boundary IoU的记录器:

progbar_val = progbar.Progbar(
  target=total_iters, verbose=1 if nranks < 2 else 2)
reader_cost_averager = TimeAverager()
batch_cost_averager = TimeAverager()
inference_averager = TimeAverager()  # 仅仅用于计算推理时间  # < -------------------- 
biou_averager = TimeAverager()       # 用于记录 BIoU

在你计算推理的代码之后,添加计算 Boundary IoU 的代码:

pred, logits = infer.inference(
     model,
     im,
     ori_shape=ori_shape,
     transforms=eval_dataset.transforms.transforms,
     # transforms=[],
     is_slide=is_slide,
     stride=stride,
     crop_size=crop_size)
 inference_averager.record(
     time.time() - infer_start, num_samples=len(label))
 
 # 计算 BIoU
 biou = metrics.boundary_iou(pred, label)    # <------------ 这里计算
 biou_averager.record(biou)                  # <------------ 这里添加记录

最后添加一行打印结果的记录:

if print_detail:
    infor = "[EVAL] #Images: {} mIoU: {:.4f} Acc: {:.4f} Kappa: {:.4f} Dice: {:.4f}".format(
        len(eval_dataset), miou, acc, kappa, mdice)
    infor = infor + auc_infor if auc_roc else infor
    logger.info(infor)
    logger.info("[EVAL] Class IoU: \n" + str(np.round(class_iou, 4)))
    logger.info("[EVAL] Class Precision: \n" + str(
        np.round(class_precision, 4)))
    logger.info("[EVAL] Class Recall: \n" + str(np.round(class_recall, 4)))
    mean_biou = biou_averager.get_average()        # <---------------- 这里这里计算全部的均值
    logger.info("[EVAL] Boundary IoU: \n" + str(np.round(mean_biou, 4))) # 打印

OK了,终于可以执行了

本来是没有下一篇了,因为 Boundary IoU 已经添加完毕了,但是我又发现,还有些其他的细节

于是在下一篇补充一些其他的细节,如 reverse_transform 之类的


建议诸位多看这种工业级别的源码,不光看着舒服,而且能学到不少东西

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值