经过前面:
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
之类的
建议诸位多看这种工业级别的源码,不光看着舒服,而且能学到不少东西