先给numpy版本的代码,注意 图片的 shape 都是(w, h)
# General util function to get the boundary of a binary mask.
# 该函数用于获取二进制 mask 的边界
def mask_to_boundary(mask, dilation_ratio=0.02):
"""
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)
"""
h, w = mask.shape
img_diag = np.sqrt(h ** 2 + w ** 2) # 计算图像对角线长度
dilation = int(round(dilation_ratio * img_diag))
if dilation < 1:
dilation = 1
mask = mask.astype(np.uint8)
# 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 和 dt 的 shape 不一样
# gt = gt[0, 0]
# dt = dt[0]
# 这里为了让 gt 和 dt 变为 (h, w) 而不是 (1, h, w) 或者 (1, 1, h, w)
# 注意这里的类别转换主要是为了后边计算边界
# gt = gt.numpy().astype(np.uint8)
# dt = dt.numpy().astype(np.uint8)
gt = gt.astype(np.uint8)
dt = dt.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)
Boundary IoU
说白了就是计算 预测图片的边界 和 GT图片的边界 的 IoU
就是酱紫的两个边界计算 IoU 就好
问题是怎么计算上图这样的边界呢? ,用Canny边缘检测之类的? 算个梯度? 可是要边界啊,在分割的区域内部也有可能有边缘啊,这个不是边界吧
我们可以采用Boundary IoU原文的计算方式
这是原图:
我们可以让原图缩小一圈,就像这样
然后原图减去缩小版的图,就得到了边界
那怎么才能得到缩小版的原图呢? 这就请出了主角:腐蚀
可以参考这里:
OpenCV 图像处理之膨胀与腐蚀
腐蚀操作和膨胀操作相反,也就是将毛刺消除,判断方法为:在卷积核大小中对图片进行卷积。
取图像中(3 * 3)区域内的最小值。由于我们是二值图像,也就是取0(黑色)。 总结: 只要原图片3 * 3范围内有黑的,该像素点就是黑的。
接下来直接看看代码吧,代码见 Reference:
# GitHub repo: https://github.com/bowenc0221/boundary-iou-api
# Reference: https://gist.github.com/bowenc0221/71f7a02afee92646ca05efeeb14d687d
import cv2
import numpy as np
import matplotlib.pyplot as plt
# General util function to get the boundary of a binary mask.
# 该函数用于获取二进制 mask 的边界
def mask_to_boundary(mask, dilation_ratio=0.02):
"""
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)
"""
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.02):
"""
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_boundary = mask_to_boundary(gt, dilation_ratio)
dt_boundary = mask_to_boundary(dt, dilation_ratio)
intersection = ((gt_boundary * dt_boundary) > 0).sum()
union = ((gt_boundary + dt_boundary) > 0).sum()
boundary_iou = intersection / union
return boundary_iou
mask_to_boundary
函数用于计算边界的 mask,而 boundary_iou
用于计算 boundary_iou
,boundary_iou
中会调用 mask_to_boundary
.
这一行用于给原图的四周添加0, 这样连边界区域的目标像素也会被腐蚀掉
# 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)
以下是 cv2.copyMakeBorder
操作的示意图,其实直接叫 Padding 就好了
这两行用于给图像做腐蚀操作,kernel size 是(3, 3)
kernel = np.ones((3, 3), dtype=np.uint8)
new_mask_erode = cv2.erode(new_mask, kernel, iterations=dilation) # iterations 指的是腐蚀的次数
再来看下 dilation 的计算:
h, w = mask.shape
img_diag = np.sqrt(h ** 2 + w ** 2) # 计算图像对角线长度
dilation = int(round(dilation_ratio * img_diag))
if dilation < 1:
dilation = 1
腐蚀的次数与对角线的长度成正比,如果小于1则直接给1,dilation_ratio
是函数的参数
再看最后一步:
# 因为之前向四周填充了0, 故而这里不再需要四周
mask_erode = new_mask_erode[1 : h + 1, 1 : w + 1]
将周边的padding像素去掉,之后再将二者减掉就可:
return mask - mask_erode
最后就得到这个图:
boundary_iou
的计算方式和一般的 IoU 计算方式一样,有个问题就是,如果交集onion==0
时,可能存在除0错误的问题,他这个代码里没有
所以应该改为:
def boundary_iou(gt, dt, dilation_ratio=0.02):
"""
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_boundary = mask_to_boundary(gt, dilation_ratio)
dt_boundary = mask_to_boundary(dt, dilation_ratio)
intersection = ((gt_boundary * dt_boundary) > 0).sum()
union = ((gt_boundary + dt_boundary) > 0).sum()
if union < 1:
return 0
boundary_iou = intersection / union
return boundary_iou
如果你需要计算多类的 Boundary IoU,加个for循环就好:
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 和 dt 的 shape 不一样
gt = gt[0, 0]
dt = dt[0]
# 这里为了让 gt 和 dt 变为 (h, w) 而不是 (1, h, w) 或者 (1, 1, h, w)
# 注意这里的类别转换主要是为了后边计算边界
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)