在LIDC-IDRI
的数据集中,对于同一个案例,存在多个医生标注的结果。这就导致下面几种情况的出现:
- A医生标注的结节区域,B医生并不一定会标;
- B医生标注的结节,C医生也标注了,但是范围大小存在着交集关系;
- 同时标记,给的特征等级也不一定相同。
此时,就需要对一个案例标注的结节进行处理。可以根据标注次数进行选择,也可以简单粗暴的直接取并集。本文就直接取并集,比较的简单。如果要考虑标记次数,可以参考这篇文章:【3D 图像分割】基于 Pytorch 的 3D 图像分割6(数据预处理之LIDC-IDRI 标签 xml 标签转储及标记次数统计 )。
本文的目标,就是在上一节处理得到的PKL
文件的基础上,获取一个个结节坐标信息,和对应需要处理的某个特征的信息,比如良恶性,比如钙化程度。最终得到一个合并后汇总版的坐标,和对应的等级,具体步骤如下:
- 获取一个个结节信息,包括坐标和等级;
- 对这些结节,根据
IOU
,分成一个个小堆,等着合并用; - 一个堆,一个堆的合并在一起,等级是根据投票少数服从多数;
- 最后把合并后的坐标和等级存储下来,供后续裁剪使用。
一、具体实施 ❤️
实施上述内容的主调用函数如下,定义了pkl
文件的路径、存储服务器托管网的路径、取什么特征。这样,在main
里面就实施上述步骤,也会在本节中,分小节一一展开介绍。
def main(pkl_dir, mainKey, save_dir):
pkl_list = os.listdir(pkl_dir)
for pkl in pkl_list:
name = pkl.split('_')[0]
pkl_path = os.path.join(pkl_dir, pkl)
print('pkl_path:', pkl_path)
boxes, characteres = getPkl_info(pkl_path, mainKey=mainKey) # 读取pkl文件,获取标注的信息
print('boxes:', boxes, characteres)
if boxes:
cluster_l = make_one_cluster(boxes, iout=0.1)
boxes_combine, level_combine = combine_bbox_level(cluster_l, boxes, characteres)
print(boxes_combine)
print(level_combine)
print()
np.save(os.path.join(save_dir, '%s_boxes.npy' % (name)), boxes_combine)
np.save(os.path.join(save_dir, '%s_level.npy' % (name)), level_combine)
if __name__=='__main__':
pkl_dir = r'./pkl_file'
save_dir = r'./nodule_cls_info'
mainKey = 'nodule_calcification'
# 实性 部分实性 磨玻璃影
cls = ['solidNodule', 'solidNodulePart', 'GGO']
main(pkl_dir, mainKey, save_dir)
1.1、读取PKL文件,获取信息
PKL文件的读取,在之前的文章中都有简单的介绍,感兴趣的可以去看看本专栏之前的文章,会对pkl
文件的读取和存储留下更深的印象。
在pkl
文件中:
- 一个结节一层的记录信息如下所示,
- 一个结节不同层的信息,会组成一个列表
- 不同结节,会组成一个更大的列表
对于下面一个结节一层的记录信息,就不一逐一展开介绍了,这块介绍参考上一篇文章:【3D 图像分类】基于 Pytorch 的 3D 立体图像分类3(LIDC-IDRI 肺结节 XML 特征标签 PKL 转储)
{
'pixels': [
[355, 278],
[354, 279],
[354, 280],
[354, 281],
[354, 282],
[354, 283],
[354, 284],
[354, 285],
[355, 286],
[356, 286],
[357, 286],
[358, 285],
[359, 285],
[360, 284],
[361, 284],
[362, 283],
[363, 282],
[363, 281],
[362, 280],
[361, 279],
[360, 279],
[359, 278],
[358, 278],
[357, 278],
[356, 278],
[355, 278]
],
'sop_uid': '1.3.6.1.4.1.14519.5.2.1.6279.6001.265463834573905158752543199468',
'sop_Instance_num': 57,
'nodule_id': 'Nodule 001',
'nodule_malignancy': 3,
'nodule_subtlety': 4,
'nodule_internal_struct': 1,
'nodule_calcification': 6,
'nodule_sphericity': 4,
'nodule_margin': 5,
'nodule_lobulation': 1,
'nodule_spiculation': 1,
'nodule_texture': 5
},
获取pkl
文件信息的完整定义代码如下,这里使用了一个外接立体框,表示一个结节的坐标,包括了zmin, ymin, xmin, zmax, ymax, xmax
,去除掉了就只标记一层的结节。(这里你也可以不去掉,那最后就会留下这个,因为算IOU
时候,它与其他的框的值比较低)
def getPkl_info(pkl_path, mainKey='nodule_malignancy'):
boxes, character_l = [], []
with open(pkl_path, "rb") as f:
pkl_data = pickle.load(f)
print(pkl_data)
for rad_annotationID in pkl_data['nodules']:
z_l, y_l, x_l = [], [], []
for one_nodule in rad_annotationID:
sop_Instance_num = one_nodule['sop_Instance_num']
pixel_array = np.array(one_nodule['pixels'])
character = one_nodule[mainKey]
y1, x1, y2, x2 = np.min(pixel_array[:, 1]), np.min(pixel_array[:, 0]), np.max(
pixel_array[:, 1]), np.max(pixel_array[:, 0])
z_l.append(sop_Instance_num)
y_l.append(y1), y_l.append(y2)
x_l.append(x1), x_l.append(x2)
zmin, zmax = min(z_l), max(z_l)
ymin, ymax = min(y_l), max(y_l)
xmin, xmax = min(x_l), max(x_l)
if zmax > zmin: # 去除掉只标记了一层的
boxes.append([zmin, ymin, xmin, zmax, ymax, xmax])
character_l.append(character)
return boxes, character_l
1.2、根据IOU
,分成小堆
在判断同一次检查,不同的结节之间的关系,采用了立体框之间的IOU
作为判断标准,其中:
-
IOU
低于阈值的,两个立体框之间是相离更大的,归为不同的结节 -
IOU
高于阈值的,两个立体框之间是相交更大的,他们两个需要划到一个堆里面,供后续合并操作
def iou_3d(cubes_a, cubes_b):
# cubes_a:[zi.min(), yi.min(), xi.min(),
# zi.max(), yi.max(), xi.max()]
cubes_a = np.expand_dims(cubes_a, axis=1)
cubes_b = np.expand_dims(cubes_b, axis=0)
# np.maximum逐元素比较两个array的大小,取出大的值
overlap = np.maximum(0.0,
np.minimum(cubes_a[..., 3:], cubes_b[..., 3:]) - # 大大,求最小
np.maxi服务器托管网mum(cubes_a[..., :3], cubes_b[..., :3]) # 小小,求最大
)
overlap = np.prod(overlap, axis=-1) # np.prod:计算数组中所有元素的乘积
area_a = np.prod(cubes_a[..., 3:] - cubes_a[..., :3], axis=-1) # 最大坐标减去最小坐标
area_b = np.prod(cubes_b[..., 3:] - cubes_b[..., :3], axis=-1)
iou = overlap / (area_a + area_b - overlap+1e-5)
return iou
def make_one_cluster(boxes, iout=0.1):
iou = iou_3d(boxes, boxes)
n, m = iou.shape
iou[np.tril_indices_from(iou)] = 0
print(iou, iou.shape, type(iou))
# iou_l = iou.tolist()
cluster_l = []
all_l = []
for i in range(n):
res = np.where(iou[i] > iout)[0]
print(i, res, type(res))
match_index = res.tolist()
if i not in all_l:
match_index.append(i)
cluster_l.append(match_index)
all_l.extend(match_index)
print(cluster_l)
print(all_l)
return cluster_l
上面代码中,除了计算框与框之间的IOU
外,还需要去除对角线及其下三角形(tril
)的值,这里都置为0。除此之外,1匹配到3,那就不能再3匹配到1了,所以增加了一个去重的判断,已经纳入了,就不在归入列表了。
1.3、合并在一起
合并在一起就比较的简单了,这里取的是并集,所以对于一个堆的不同医生标注的结节,zyx的最大值,最小的取最小,最大的取最大就可以了。
当然,你也可以取交集。但是,对于最后分类阶段对结节像素区域裁剪一般不构成影响的。因为结节的区域是比较小的,crop后的patch一般会比这个结节大很多的,所以,这点影响不大。
下面是合并的代码。
def combine_bbox_level(cluster_l, boxes, characteres):
boxes_combine, level_combine = [], []
for oneCluster in cluster_l:
level_list, bbox_list = [], []
for i in oneCluster:
level_list.append(characteres[i])
bbox_list.append(boxes[i])
print('level_list:', level_list)
print('bbox_list:', bbox_list)
bbox_array = np.array(bbox_list)
zmin, zmax = min(bbox_array[:, 0]), max(bbox_array[:, 3])
ymin, ymax = min(bbox_array[:, 1]), max(bbox_array[:, 4])
xmin, xmax = min(bbox_array[:, 2]), max(bbox_array[:, 5])
characteristicLeval = max(level_list, key=level_list.count)
boxes_combine.append([zmin, ymin, xmin, zmax, ymax, xmax])
level_combine.append(characteristicLeval)
return boxes_combine, level_combine
1.4、存储下来
通过前面一系列的处理,将多人标注的结节进行了汇总,多相应的等级进行了投票处理,得到了立体框的坐标,以及对应特征的等级。此时,将这两个数据临时存储下来,供后续裁剪等操作,提供数据。
存储的方式如下:
np.save(os.path.join(save_dir, '%s_boxes.npy' % (name)), boxes_combine)
np.save(os.path.join(save_dir, '%s_level.npy' % (name)), level_combine)
这样一次检查,就得到了一个_boxes.npy
的文件,和一个_level.npy
为文件。
打开npy文件查看,_boxes.npy
存放的内容如下:
[[ 54 276 351 58 288 364]
[ 49 360 285 51 370 297]
[ 40 258 324 43 268 335]
[ 21 330 316 22 340 328]
[ 23 232 292 26 243 302]
[ 15 290 155 16 295 162]
[ 50 199 109 51 208 116]]
_level.npy
存放的内容如下:
[6 6 6 6 6 6 6]
二、总结 ❤️
本文就没有延伸太多无关的内容,主要就是对本系列一二两个动手实操内容的一个数据处理的补充。通过本文之后,你就得到了一个结节具体的坐标位置,以及对应特征类别的等级。有了这两个信息,无论你是做检测,还是扣成patch
进行分类,都非常的简单了。
最后这个扣patch
的操作,会在下一节给出,期待。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
相关推荐: LLMs之HFKR:HFKR(基于大语言模型实现异构知识融合的推荐算法)的简介、原理、性能、实现步骤、案例应用之详细攻略
LLMs之HFKR:HFKR服务器托管网(基于大语言模型实现异构知识融合的推荐算法)的简介、原理、性能、实现步骤、案例应用之详细攻略 目录 HFKR的简介 服务器托管网异构知识融合:一种基于LLM的个性化推荐新方法 服务器托管,北京服务器托管,服务器租用 ht…