用深度学习方法得到的分割结果,会有一些假阳性区域。通过去除这些假阳性区域,可以提高分割结果。
比如说做肾分割,大家都知道,肾只有左右两边有,如果分割结果出现了三个区域,则可以根据常识,去除那个假阳性区域。
用到的方法就是 连通成分分析Connected-Components
。
这里提供两种方法:
1. opencv-python 提供的方法
安装: pip install opencv-python
cv2.connectedComponents & cv2.connectedComponentsWithStats
1.1 cv2.connectedComponents
实例:
import cv2
import numpy as np
img = np.array([[0, 255, 0, 0],
[0, 0, 255, 255],
[0, 0, 0, 255],
[255, 0, 0, 0]], np.uint8)
res, labels = cv2.connectedComponents(img, connectivity=4)
res
Out[5]: 4
labels
Out[6]:
array([[0, 1, 0, 0],
[0, 0, 2, 2],
[0, 0, 0, 2],
[3, 0, 0, 0]], dtype=int32)
假设 img 是分割结果,值为255的是目标区域,可以发现,目标区域有3块。res代表区域的数量 一共有4块。
labels为经过连通区域分析后的标记,把 4邻域 内相同的值标记为一个类别。这样一共产生了4个区域,分别用【0,1,2,3】表示。
上述例子例子中,参数 connectivity=4
表示在4邻域范围内查找元素,也是可以改成8邻域对比一下
4邻域:A点的上下左右中,假设存在B点和它的值一样,就表示 AB 点属于同一区域。
这样标记后,如果我们觉得3那个区域是假阳性,那我们就可以把3那个区域的值变为0,其余区域的值标记为255,这样就消除掉3这个区域的阳性值了。
label = np.where(labels > 2, 0, labels)
# 把labels中,大于2的值,赋值为0, 其余的就是labels原来的值。这样就剩下了两个区域
print(label)
结果如下:
[[0 1 0 0]
[0 0 2 2]
[0 0 0 2]
[0 0 0 0]]
1.2 cv2.connectedComponentsWithStats
stats 是 bounding box 的信息,N*5的矩阵,行对应每个label,五列分别为 [x0, y0, width, height, area]
总结:该方法对二维图像去除假阳性区域很好用,但是无法对三维图像进行操作。
2 cc3d 提供的方法
cc3d 提供了二维和三维的方法实现连通成分分析
- 3D 方法: 提供 26、18 或 6 个连通邻域划分区域
- 2D 方法: 提供 4 和 8 个连通域分析。
如何安装
pip install connected-components-3d
测试
比如二维图像:
原始图像有 [0, 31, 199] 3个值 背景是0,绿色是31, 199是紫色。
img = np.array(Image.open('./testing_img/test2d.png'))[:,:,0]
print(np.unique(img))
labels = cc3d.connected_components(labels, connectivity=4)
使用4邻域后,图片多了很多种颜色,每种颜色都代表一个区域,一共有78个区域。
划分成不同区域了,自然能提取出想要的区域。比如把 区域像素<阈值的置为0,从而去除假阳性。或者只保留前两个最大的区域,其余置为0.
最后,附上我真实处理三维数据的代码
import cc3d
import nibabel as nib
from pathlib2 import Path
from tqdm import tqdm
import numpy as np
import os
def main(data, output):
data = Path(data).resolve()
output = Path(output).resolve()
assert data != output, f'postprocess data will replace original data, use another output path'
if not output.exists():
output.mkdir(parents=True)
predictions = sorted(data.glob('*_seg.nii.gz'))
for pred in tqdm(predictions):
if not pred.name.startswith('.'):
vol_nii = nib.load(str(pred))
affine = vol_nii.affine
vol = vol_nii.get_fdata()
vol = post_processing(vol)
vol_nii = nib.Nifti1Image(vol, affine)
vol_nii_filename = output / pred.name
vol_nii.to_filename(str(vol_nii_filename))
def post_processing(vol):
vol_ = vol.copy()
vol_[vol_ > 0] = 1
vol_ = vol_.astype(np.int64)
vol_cc = cc3d.connected_components(vol_)
cc_sum = [(i, vol_cc[vol_cc == i].shape[0]) for i in range(vol_cc.max() + 1)]
cc_sum.sort(key=lambda x: x[1], reverse=True)
cc_sum.pop(0) # remove background
reduce_cc = [cc_sum[i][0] for i in range(1, len(cc_sum)) if cc_sum[i][1] < cc_sum[0][1] * 0.1]
for i in reduce_cc:
vol[vol_cc == i] = 0
return vol
if __name__ == '__main__':
data = 'output/' # 分割结果地址,图像为nii.gz
output = 'output_remove/' # 移除假阳性后保存地址
if not os.path.exists(output):
os.makedirs(output)
main(data, output)