SIIM-ACR Pneumothorax Segmentation 气胸x光识别比赛数据处理

本文详细介绍了如何处理气胸X光图像数据,包括从DCM文件读取、RLE编码的mask转换、数据可视化以及将标注信息存储为JPEG图像和JSON文件。通过数据预处理,可以更好地理解和操作医学影像数据,为后续的机器学习或深度学习任务做好准备。
摘要由CSDN通过智能技术生成

比赛封面

气胸可能由胸部钝伤、潜在肺部疾病造成的损害或最可怕的原因引起——它可能完全没有明显的原因。在某些情况下,肺部塌陷可能会危及生命。

气胸通常由放射科医生通过胸部 X 光检查诊断,有时很难确认。用于检测气胸的准确 AI 算法将在许多临床场景中有用。AI 可用于对胸片进行分类以进行优先解释,或为非放射科医生提供更自信的诊断。

2019年8月,由Society for Imaging Informatics in Medicine (SIIM)举办的气胸分割识别比赛的目标就是对胸部X光存在气胸的情况进行检出。如何在气胸患者的X光片上,分割出气胸患者患病区的部位和形状。

本文主要是对此次比赛公开的气胸X光胸片数据进行分析和处理,将会依次从数据获取—>数据可视化—>数据转储,三个方面展开。力求对这批数据的组成,以及处理方式有个较为深入的了解。为后续的训练、测试等等内容,打下坚实的基础。

最终,我会将处理好的数据直接分享出来,方便下载使用,下载链接:胸片X光气胸标注处理后下载链接。下面我们就开始吧!

一、 比赛及数据介绍

比赛地址:https://www.kaggle.com/c/siim-acr-pneumothorax-segmentation
原始数据下载地址:https://www.kaggle.com/c/siim-acr-pneumothorax-segmentation/discussion/108009

图像分割的数据一共分为两部分(附加一个json部分,不需要的可以跳过阅读):

  • 训练用的图片,JPEG格式
  • 图片中需要分割的部分,称之为 mask
  • 附加检测常用的json文件

这次我们待处理的图像数据是以 DCM 文件存储,DICOM是什么呢?

DCM 是一种数位成像,广泛运用于医学领域,但并不局限于医学,DCM 本身是一种特殊的图像文件,它可以用来存储各种图像信息。

DCM 文件是遵循 DICOM 标准的一种文件,医学图像大多数都是采用这样的数据格式进行存储的。

我们的 mask 部分的数据存储在 csv 文件中,csv 文件大家都比较熟悉, 这里就不做介绍了。后面我们对csv进行解析,拿到mask标气胸注部分。

二、数据处理

2.1 导入 mask 数据

首先我们来看下存放 mask 数据的 csv 文件中的 mask 标注数据。使用 pandas 的 read_csv 接口读取 train-rle.csv 文件,一次性将整个csv内容读取到内存中。代码如下:

# 获取标注信息
rles_df = pd.read_csv(r'Z:\SIIM-ACR-Pneumothorax-Segmentation/train-rle.csv')
rles_df.columns = ['ImageId', 'EncodedPixels']
print(rles_df.head())

打印查看其中的头部5条数据,结果如下截图:

head5
可以看到这个 csv 文件中存放了两列,一列是 ImageId , 一列是 EncodedPixels 。

  • train-rle包含12955份dicom文件(站立位胸片),有气胸:无气胸 = 3577:9378
  • ImageId 这一列比较好理解,是训练数据的 id,对应的是 dcm 文件的文件名,也是DCM文件内的SOPInstanceUID,这个联系线索很重要。
  • EncodedPixels 实际存放的就是 mask 的像素数据,这些像素数据是以run-length-encoded (RLE)编码存放的。

接下来,我们需要定义一个函数来将 RLE 编码的数据还原成 mask 图片数据。这部分官方已经提供了一个函数,给我们直接使用,如下:

def rle2mask(rle, width, height):
    """
    将 RLE 编码的数据还原成 mask 图片数据
    """
    mask = np.zeros(width * height)
    array = np.asarray([int(x) for x in rle.split()])
    starts = array[0::2]
    lengths = array[1::2]

    current_position = 0
    for index, start in enumerate(starts):
        current_position += start
        mask[current_position:current_position+lengths[index]] = 255
        current_position += lengths[index]

    return mask.reshape(width, height)

2.2 导入 DCM 文件

接下来我们将DCM读入并存储到字典中,方便以后查看跟使用。我们还将之前读入的 mask 数据也合并到相应的 ImageId 的字典中。

在训练数据中,如果胸片没有被 mask 标记,表示这个病例他并不患有气胸。通过 EncodedPixels 中的数据,将是否是气胸的患者记录到 has_pneumothorax 这一字段中。-1 即为无气胸,也无标记信息。

在后续对标注信息操作的时候,也记得对标记为-1对数据跳过,否则会报错提示。

def dicom_to_dict(dicom_data, file_path, rles_df, encoded_pixels=True):
    """
    获取dicom记录的相关信息, 以及encoded_pixels
    """
    data = {}

    # Parse fields with meaningful information
    data['patient_name'] = dicom_data.PatientName
    data['patient_id'] = dicom_data.PatientID
    data['patient_age'] = int(dicom_data.PatientAge)
    data['patient_sex'] = dicom_data.PatientSex
    data['Rows'] = dicom_data.Rows
    data['Columns'] = dicom_data.Columns
    data['pixel_spacing'] = dicom_data.PixelSpacing
    data['file_path'] = file_path
    data['id'] = dicom_data.SOPInstanceUID

    # look for annotation if enabled (train set)
    if encoded_pixels:
        encoded_pixels_list = rles_df[rles_df['ImageId']==dicom_data.SOPInstanceUID]['EncodedPixels'].values

        pneumothorax = False
        for encoded_pixels in encoded_pixels_list:
            if encoded_pixels != ' -1':
                pneumothorax = True

        data['encoded_pixels_list'] = encoded_pixels_list
        data['has_pneumothorax'] = pneumothorax
        data['encoded_pixels_count'] = len(encoded_pixels_list)

    return data

查看下读取dcm的代码,是否正常打印,代码如下:

# 获取dcm数据信息
    train_fns = sorted(glob.glob(r'Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\tmp_train/*/*/*.dcm'))
    train_metadata_df = pd.DataFrame()
    train_metadata_list = []
    for file_path in tqdm(train_fns):
        dicom_data = pydicom.dcmread(file_path)
        train_metadata = dicom_to_dict(dicom_data, file_path, rles_df)
        train_metadata_list.append(train_metadata)
    train_metadata_df = pd.DataFrame(train_metadata_list)
    print(train_metadata_df.head())

打印头部5行如下:

dcm

三、数据可视化

我们在读取完数据以后,接下来就进行数据情况的查看。打印查看的同时,也方便我们将可视化的内容,转储保存。

3.1 病例样本可视化

我们随机挑选了4个病例。我们在每个病例上打出了年龄,性别以及是否是气胸患者。可视化代码如下:

num_img = 4
subplot_count = 0
fig, ax = plt.subplots(nrows=1, ncols=num_img, sharey=True, figsize=(num_img * 10, 10))
for index, row in train_metadata_df.sample(n=num_img).iterrows():
    dataset = pydicom.dcmread(row['file_path'])
    ax[subplot_count].imshow(dataset.pixel_array, cmap=plt.cm.gray)
    # label the x-ray with information about the patient
    ax[subplot_count].text(0, 0, 'Age:{}, Sex: {}, Pneumothorax: {}'.format(row['patient_age'], row['patient_sex'],
                                                                            row['has_pneumothorax']),
                           size=26, color='white', backgroundcolor='black')
    subplot_count += 1

plt.show()
fig.savefig('data.png')
plt.pause(15)  # 显示秒数
plt.close()

展示的图像如下:

head可以看到dcm获取的影像文件可以直接被展示了,并且病人相关信息和是否是气胸,也获取到了。后面我们就开始对标注信息进一步处理,展示出来。

3.2 mask可视化

我们在看下 mask 图像在相对应的病例中的位置:

我们分两组来显示:

  • 第一组我们将原始胸片图像中用红色的框框出 mask 的最小包围盒. 然后将mask 数据部分用不同的颜色区分
  • 第二组我们直接显示原始图像

两者形成对比,便于查看我们标注的气胸位置在哪里。(注视部分包含了3.1的内容)

def bounding_box(img):
    # return max and min of a mask to draw bounding box
    rows = np.any(img, axis=1)
    cols = np.any(img, axis=0)
    rmin, rmax = np.where(rows)[0][[0, -1]]
    cmin, cmax = np.where(cols)[0][[0, -1]]

    return rmin, rmax, cmin, cmax

def plot_with_mask_and_bbox(file_path, mask_encoded_list,  rows, columns, figsize=(20,10)):
    pixel_array = pydicom.dcmread(file_path).pixel_array

    # use the masking function to decode RLE
    mask_decoded_list = [rle2mask(mask_encoded, rows, columns).T for mask_encoded in mask_encoded_list]
    print('mask_decoded:', mask_decoded_list)
    fig, ax = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(10,8))

    # print out the xray
    ax[0].imshow(pixel_array, cmap=plt.cm.gray)
    # print the bounding box
    for mask_decoded in mask_decoded_list:
        # print out the annotated area
        ax[0].imshow(mask_decoded, alpha=0.3, cmap="Reds")
        rmin, rmax, cmin, cmax = bounding_box(mask_decoded)
        # 绘制一些特殊的形状和路径,例如矩形
        bbox = patches.Rectangle((cmin, rmin), cmax-cmin, rmax-rmin, linewidth=1, edgecolor='r', facecolor='none')
        ax[0].add_patch(bbox)   # 将图形添加到图中
    ax[0].set_title('With Mask')
    ax[1].imshow(pixel_array, cmap=plt.cm.gray)
    ax[1].set_title('Raw')

    plt.show()
    fig.savefig('./data/mask_raw.png')
    # plt.pause(15)  # 显示秒数
    # plt.close()

def main_visual():
    # 获取标注信息
    rles_df = pd.read_csv(r'Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation/train-rle.csv')
    rles_df.columns = ['ImageId', 'EncodedPixels']
    print(rles_df.head())

    # 获取dcm数据信息
    train_fns = sorted(glob.glob(r'Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\tmp_train/*/*/*.dcm'))
    train_metadata_df = pd.DataFrame()
    train_metadata_list = []
    for file_path in tqdm(train_fns):
        dicom_data = pydicom.dcmread(file_path)
        train_metadata = dicom_to_dict(dicom_data, file_path, rles_df)
        train_metadata_list.append(train_metadata)
    train_metadata_df = pd.DataFrame(train_metadata_list)
    print(train_metadata_df.head())

    # num_img = 4
    # subplot_count = 0
    # fig, ax = plt.subplots(nrows=1, ncols=num_img, sharey=True, figsize=(num_img * 10, 10))
    # for index, row in train_metadata_df.sample(n=num_img).iterrows():
    #     dataset = pydicom.dcmread(row['file_path'])
    #     ax[subplot_count].imshow(dataset.pixel_array, cmap=plt.cm.gray)
    #     # label the x-ray with information about the patient
    #     ax[subplot_count].text(0, 0, 'Age:{}, Sex: {}, Pneumothorax: {}'.format(row['patient_age'], row['patient_sex'],
    #                                                                             row['has_pneumothorax']),
    #                            size=26, color='white', backgroundcolor='black')
    #     subplot_count += 1
    #
    # plt.show()
    # fig.savefig('data.png')
    # plt.pause(15)  # 显示秒数
    # plt.close()


    for index, row in train_metadata_df.sample(n=4).iterrows():
        file_path = row['file_path']
        print(file_path)
        rows = row['Rows']
        columns = row['Columns']
        mask_encoded_list = row['encoded_pixels_list']

        if len(mask_encoded_list) > 0:
            if mask_encoded_list[0] != '-1':
                plot_with_mask_and_bbox(file_path, mask_encoded_list, rows, columns) 

结果展示如下:
12到这里,基本上气胸相关的数据都展示完毕了。后面我们就开始动手,将标注结果和图像保存下来,便于后面的操作。

四、数据转储

标注信息存储到CSV文件,是为了便于数据传输,但是不便于查看和理解。所以,解析CSV文件,转储成我们易于观察的数据,是接下来我们要做的内容。

转储的内容包括两部分:

  • dcm转为 jepg 图像
  • rle标注mask信息,转mask图和json标注文件存储

下面一一展开介绍。

4.1 DCM转储JPEG图像

dcm文件转png文件的方式方法有很多,包括采用目前可以使用的软件进行图片转储,例如microdicom。也可以使用python中的pydicom去转图。主要思路就是将范围更广的dcm数据,转化到可以png或JPEG的0-255。

import os
import cv2
import pydicom
import numpy as np
import skimage.transform as transform

def dcm2jpeg(file, dst_path):
    print('FIle:', file)
    ds = pydicom.dcmread(file, force=True)
    # ds.file_meta.TransferSyntaxUID =
    # ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian
    ori_img = np.array(ds.pixel_array)

    sharp = ori_img.shape
    _h = sharp[0]
    _w = sharp[1]
    if len(sharp) == 3:
        ori_img = ori_img[:, :, 0]
    img = transform.resize(ori_img, (_h, _w))

    start = img.min()
    end = img.max()

    img[img < start] = start
    img[img > end] = end
    img = np.array((img - start) * 255.0 / (end - start))
    if hasattr(ds, 'PhotometricInterpretation'):
        if ds.PhotometricInterpretation == 'MONOCHROME1':
            img = 255 - img
    jpeg_name = os.path.basename(file).replace('.dcm', '.jpeg')
    save_path = os.path.join(dst_path, jpeg_name)
    print(save_path)

    img = img.astype(np.uint8)
    cv2.imwrite(save_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
    print('save ok')
    return jpeg_name

def do_convert(file_path, png_folder):
    try:
         jpeg_path = dcm2jpeg(file_path, png_folder)

    except Exception as e:
        print('main process has error:%s' % e)

def main():
    ini_folder = r'E:\temp\dcm'
    jpeg_folder = r'E:\temp\jpeg'
    for root, dirs, files in os.walk(ini_folder):
        for file in files:
            print(file)
            file_path = os.path.join(root, file)

            print('_pro' in file)
            if '_pro' in file:
                continue
            if file.lower().endswith('dcm') or file.lower().endswith('dicom'):
                do_convert(file_path, jpeg_folder)
                print('ok')

if __name__ == '__main__':
    main()

以一个转图前后的对比,做个展示,如下:

4.2 转储mask

4.2.1 预保存mask

其实前面我们已经将CSV中标注的气胸mask信息,已经提取出来进行了展示。后面的工作也上述可视化部分存在交集,新增添的就是存储为mask图片的部分。保存mask成图片的完整代码,如下:

save_mask = r'Z:\SIIM-ACR-Pneumothorax-Segmentation\mask'
def save_maskImage(file_path, mask_encoded_list, rows, columns):
    # use the masking function to decode RLE
    mask_decoded_list = [rle2mask(mask_encoded, rows, columns).T for mask_encoded in mask_encoded_list]

    print('mask_decoded_list:', mask_decoded_list)
		
	# 判断是否多个mask信息,简单直接存储,后面再添加处理进行mask合并(有能力的建议自我修改合并)
    if len(mask_decoded_list)>1:
        n=1
        for mask_decoded in mask_decoded_list:
            cv2.imwrite(save_mask + '/' + os.path.basename(file_path).split('.dcm')[0]+'_'+str(n)+'.png', mask_decoded)
            n+=1
    else:
        for mask_decoded in mask_decoded_list:
            cv2.imwrite(save_mask + '/'+os.path.basename(file_path).replace('.dcm', '.png'), mask_decoded)

这里我们懒省劲,直接将每一个mask都直接存储下来,如果mask的数量多于1个,则采用加序号后缀的形式进行保存,保存结果如左图。下面我们在将分开的mask合到一起,合成结果如右图。如下:

mask2mask

4.2.2 mask合并

这里的原因再4.2.1中介绍清楚了,建议小伙伴可以在这个基础上做修改,避免麻烦的数据间的操作。

我是因为这套流程和代码我都有了,所以就直接这样操作,比较快。合并到mask文件夹的图像,也就是最后转出来气胸标记的图像了。

记得需要先将原本属于一个mask的多个mask图,放到一个文件夹内,然后依次遍历所有文件夹。合并思路是将多个mask取并集,这样就都绘制到一起了,最终我们也是一个图像对应到一个mask图。

import os
import numpy as np
import cv2

def cv_imread(file_path):
    cv_img = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), 1)
    return cv_img

raw_dir = r'Z:\seg_mask\mask_all'

patient_list = os.listdir(raw_dir)
postProcessed_list = []
for patient in patient_list:
    mask_path = os.path.join(raw_dir, patient)
    mask_list = os.listdir(mask_path)

    for i, name_m in enumerate(mask_list):
        # print(name_m)
        Instance_num = name_m.replace('.png', '').split("_")[1]

        mask = cv_imread(mask_path + "/" + name_m)
        mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
        w, h = mask.shape

        if patient not in postProcessed_list:
            print('newLoad')
            mask_temp = np.zeros((h, w))
            postProcessed_list.append(patient)
        else:
            print('reLoad')
            mask_temp = cv_imread(r"Z\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation/seg_mask/mask/"+name_m.split("_")[0]+".png")

        try:
            _, contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        except:
            contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        flag = False
        for contour in contours:
            x, y, w, h = cv2.boundingRect(contour)
            area = cv2.contourArea(contour)
            if area > 1:     # 去除<3mm的结节(一个坐标点)
                flag = True
                cv2.drawContours(mask_temp, [contour], 0, (255, 255, 255), cv2.FILLED)  # 连续绘制,类似于取并集

        if flag:
            cv2.imwrite(r"Z:\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\seg_mask/mask/"+name_m.split("_")[0]+".png", mask_temp)


    print(patient, len(postProcessed_list))

到这里,其实开头地方我们提到的几个关键数据都得到了。包括:

  • dcm转JPEG
  • 标记信息mask

PS: 本人目前已经参加工作,可能无法及时回复大家的问题,下面附上处理好的气胸标签和图像数据下载链接,希望大家打赏一点幸苦费~~

下载地址气胸X光处理后下载数据

上述资源中包括4个部分,分别是:

  • img:气胸的胸片JPEG图像
  • mask:气胸标注mask图像,2669张
  • json:气胸标注mask保存到json标签,可labelme标注软件打开审核
  • 代码部分:包括本文前面的所有代码,气胸标签处理和转图等代码

在这里插入图片描述
如有遇到任何问题,都欢迎在本博客下方留言,或直接私信,我看到后会第一时间回复,谢谢。

4.3 mask2json

前面几乎已经完成了。后面做一些补充。保存好mask后,对mask图,生成我们需要的json文件,用于后面的标注审核(如果你没有二次审核这一步,无需要下面生成json的部分),代码如下:

################
# 有时候没法直接获取标注的json文件,只有黑白色的mask图像
# 这里需要将mask,转为labelme标注的json文件
##################

import os
import cv2
import json
import base64
from PIL import Image
import io

def base64encode_img(image_path):
    src_image = Image.open(image_path)
    output_buffer = io.BytesIO()
    src_image.save(output_buffer, format='JPEG')
    byte_data = output_buffer.getvalue()
    base64_str = base64.b64encode(byte_data).decode('utf-8')
    return base64_str

def mask2json():
    mask_path = r"Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\seg_mask\mask"
    img_path = r"Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\jpegs_train"
    save_json_path = r"Z:\mult_DR\kagglePublic_data\SIIM-ACR-Pneumothorax-Segmentation\Pneumothorax_json/"
    label = "气胸"

    for (path, dirs, files) in os.walk(mask_path):
        for filename in files:
            A = dict()
            listbigoption=[]

            prefile_path = os.path.join(path, filename)

            pred_seged = cv2.imread(prefile_path, 0)
            raw_h, raw_w = pred_seged.shape
            _, pred_seged = cv2.threshold(pred_seged, 127, 255, cv2.THRESH_BINARY)

            try:
                _, contours, _ = cv2.findContours(pred_seged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
            except:
                contours, _ = cv2.findContours(pred_seged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

            if len(contours) != 0:
                for contour in contours:
                    # print(len(contours), len(contour))
                    if len(contour) >= 3:
                        listobject = dict()
                        listxy = []

                        if len(contour) > 300:
                            merge = 20
                        elif len(contour) > 200:
                            merge = 15
                        elif len(contour) > 100:
                            merge = 10
                        elif len(contour) > 50:
                            merge = 6
                        elif len(contour) > 20:
                            merge = 3
                        else:
                            merge = 1

                        for i in range(0, len(contour), merge):
                            e=contour[i]
                            listxy.append(e[0].tolist())

                        if len(listxy) <=3:
                            print(prefile_path)
                            print('len(listxy):', len(listxy), listxy)
                            continue
                        listobject['points'] = listxy
                        listobject['line_color'] = 'null'
                        listobject['label'] = label
                        listobject['shape_type'] = 'polygon'
                        listobject['fill_color'] = 'null'
                        listbigoption.append(listobject)

            A['lineColor'] = [0, 255, 0, 128]
            raw_file = filename.replace('.png', '.jpeg')
            A['imageData'] = base64encode_img(os.path.join(img_path, raw_file))
            A['fillColor'] = [255, 0, 0, 128]
            A['imagePath'] = raw_file
            A['shapes'] = listbigoption
            A['imageHeight'] = raw_h
            A['imageWidth'] = raw_w
            A['flags'] = {}
            with open(save_json_path + filename.replace(".png", ".json"), 'w', encoding='utf-8') as f:
                json.dump(A, f, indent=2, ensure_ascii=False)
if __name__=="__main__":
    mask2json()

保存好的json数据,也一并保存到上述公开的下载资料中了,自行点击链接去下载哦。如果对你有用,多多关注哦(利用周六找个咖啡书店,把他给干完了,发布手工)

在这里插入图片描述

参考链接:

参考链接1:https://www.cnblogs.com/matpool/p/12357392.html
参考链接2:https://blog.csdn.net/iizhuzhu/article/details/104012715

  • 2
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钱多多先森

你的鼓励,是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值