pair标注后保存的文件,是一个压缩文件。解压后会包含一个json文件和一个nii文件。本文就将对nii文件进行解析。完成各个类别的分拆,验证是否存在问题。
官方Pair标注结果读取说明可以通过软件获取。标记后解析nii文件后的mask图例,如下右侧图。
(因为是发现了问题,才尝试了去解决问题。下文采用倒叙方式,由处理好的结果,一步步的探究由来)。
就发现直接从nii文件读取保存后的标注文件,与在pair软件上的位置不太对。经过仔细的对比才发现,是方位发生了变化。下面就将pair标记好的mask,与原始图进行匹配。
1.标注mask匹配
经过了两次变换,分别是旋转90度和水平翻转,就发现转出来的结果就可以和pair软件匹配上了,匹配的结果如下所示:
(这块没有来得及和官方人员进行沟通,没有明白其中的思路,为什么保存的nii文件,还需要两步操作才能和pair软件显示的对应上)
# 旋转90度
mask = cv2.rotate(mask, cv2.ROTATE_90_CLOCKWISE)
# 水平翻转
right_mask = cv2.flip(mask, 1)
上述变换和图像展示的完整代码也分享出来,如下这样:
import cv2
import os
import numpy as np
mask_Dir = r'F:\Pair\data\label-all'
raw_Dir = r'F:\Pair\data\image'
mask_list = os.listdir(mask_Dir)
raw_list = os.listdir(raw_Dir)
for i in range(len(mask_list)):
maskfile_name = mask_list[i]
rawfile_name = raw_list[i]
mask_path = os.path.join(mask_Dir, maskfile_name)
raw_path = os.path.join(raw_Dir, rawfile_name)
raw_img = cv2.imread(raw_path, 1)
mask = cv2.imread(mask_path, 0)
# 旋转90度
mask = cv2.rotate(mask, cv2.ROTATE_90_CLOCKWISE)
# 水平翻转
mask = cv2.flip(mask, 1)
cv2.imshow(maskfile_name+'mask', mask)
# cv2.waitKey(1000)
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
xmin, ymin, xmax, ymax = x, y, x + w, y + h
print(xmin, ymin, xmax, ymax)
cv2.drawContours(raw_img, [contour], 0, (0, 0, 255), 1)
cv2.rectangle(raw_img, (int(xmin), int(ymin)), (int(xmax), int(ymax)), (0, 255, 0), 1)
cv2.imshow(maskfile_name+'img', raw_img)
cv2.waitKey(1000)
其中image和label文件夹内容如下所示:
到这里就要问,那label是如何获取的呢?获取方式如下:
- pair标注生成标注压缩文件
- 解析压缩文件产生json和nii.gz文件
- nii.gz文件转储为上述label文件这样的图片形式
具体的产生代码如下(这里稍微会复杂一点点,因为我想要引出pair的多标签标注的解析):
import numpy as np
import nibabel as nib
import os
from PIL import Image
def nii_2img(select=False):
label = nib.load(r"./data\17\1_2_840_113704_1_1762661706_15468_1049307570_11_1_3_2_0_62_0_62_dcm_Label.nii.gz")
#Convert them to numpy format,
label_data = label.get_fdata()
print("=======label shape=======")
print(label_data.shape)
print("=======label value=======")
print(label_data[label_data != 0])
# 挑选类别
if select:
print('change after ---')
label_data[label_data != 20.0] = 0
#extract 2D slices from 3D volume for training cases while
# e.g. slice 000
for i in range(label_data.shape[2]):
label_one = label_data[:, :, i]
label_slice000 = label_one * 255
# 旋转和翻转也可以直接放到这里,就不存在开篇对不上的问题
# 旋转90度
mask = cv2.rotate(label_slice000, cv2.ROTATE_90_CLOCKWISE)
# 水平翻转
label_slice000 = cv2.flip(mask, 1)
nozero_list = label_one[label_one != 0]
if 21.0 in nozero_list:
print("=======label shape=======")
print(label_one.shape)
print("=======label value=======")
print(label_one[label_one != 0])
np.savetxt(r"./data/txt/" + str(i) + ".txt", label_one, delimiter=',', fmt='%5s')
label = Image.fromarray(label_slice000)
label = label.convert("L")
label.save(r"./data/label/" + str(i) + "_label.png")
if __name__ == "__main__":
nii_2img(select=False)
可以看到select这个选项,这是干神马的呢?简单点就是多标签的类别解析
2.各类别拆分和mask转储
上图可以看到,此case的标注类别有两个,分别是类别20和类别21,此时的类别就变得丰富了起来,不再只是单类问题了。通过上述代码中:
np.savetxt(r"./data/txt/" + str(i) + ".txt", label_one, delimiter=',', fmt='%5s')
查看下保存的TXT文件内容,就知晓了。
同时,也为选择某一类别,提供了依据,有时标记了很多类,但是在使用阶段,可能并不需要那么多类,这就是select存在的意义。
修改下面内容,即可实现对对应类别的筛选。20就是标记序号为20多类,如果这个类的标号是31,那此事想要拿到这个类的位置信息,就需要将这里设定为21的保留下来,其余的都设为0:
# 挑选类别
if select:
print('change after ---')
label_data[label_data != 20.0] = 0
那就展示下各个类别分开后的样子,如下:
至此,大功告成,完成了pair软件标注文件的后处理。有了这个各个类别的mask图,想干点其他的事情,都方便了很多。例如转labelme的json文件等等,都可以通用了。
PS:如果你的标记是3个类别,但是你只想取其中的两个类为目标,其他类别为0展示,对select部分补充如下:
# 挑选类别
if select:
print('change after ---')
print(type(label_data))
# method select one class
# label_data[label_data != 19.0] = 0
# method select one or mult class
index = np.ones(label_data.shape)
for class_i in TB_ClassList:
index_i = label_data != class_i
index = index_i * np.array(index, dtype=bool)
print(index, type(index))
label_data[index] = 0
这样就实现了我们的想法,展示结果如下:
上述内容不仅对于pair软件试用,对于大多数语义分割的数据标注集的处理同样适用