kaggle 入门系列翻译(三) RSNA 肺炎预测

https://www.kaggle.com/peterchang77/exploratory-data-analysis

概述

 

比赛主要用来识别二维高分辨率图像的胸片中是否存在肺炎的区域。肺炎只是导致胸片显示出问题的一种可能,且每幅图可能有数个肺炎区域或没有肺炎区域。

文章由一个放射科医师和机器学习双重专家编写,介绍该数据集的底层结构、成像结构和标签类型

首先导入依赖库:

import glob, pylab, pandas as pd
import pydicom, numpy as np
 

数据集

数据集由多个文件和目录组成。在kaggle内核中,该数据集将被加载至:../input 目录中。(大多数比赛都会在这个目录)

!ls ../input
GCP Credits Request Link - RSNA.txt  stage_1_test_images
stage_1_detailed_class_info.csv      stage_1_train_images
stage_1_sample_submission.csv	     stage_1_train_labels.csv

主要文件如下

  • stage_1_train_labels.csv: 包含特征和标签的训练集(包含边框)
  • stage_1_detailed_class_info.csv: 包含细节标签的CSV (进一步探索可以)
  • stage_1_train_images/: 包含训练集的原始图片文件。

先看下第一个csv文件

df = pd.read_csv('../input/stage_1_train_labels.csv')
print(df.iloc[0])
patientId    0004cfab-14fd-4e49-80ba-63a80b6bddd6
x                                             NaN
y                                             NaN
width                                         NaN
height                                        NaN
Target                                          0
Name: 0, dtype: object

每一列包含一个病人id号patientId,一个是否生病的target,以及如果生病的话,相应矩阵的x,y,width,height。可以看到上面是一个没有生病的病人,所以矩阵数据都为NaN。

print(df.iloc[4])
patientId    00436515-870c-4b36-a041-de91049b9ab4
x                                             264
y                                             152
width                                         213
height                                        379
Target                                          1
Name: 4, dtype: object

一个重要关注点是一个病人可能由多个病灶,也就有多个矩阵了。这样同一个用户在表中就有多条对应的列表。每个target为1。即不管病人有几个病灶,该csv文件中target最多为1,而是将多个病灶拆分为多条数据。

图片原始信息

医学图像以一种称为DICOM文件(*.dcm)的特殊格式存储。它们包含头元数据以及像素数据的底层原始图像数组的组合。在Python中,访问和操作DICOM文件的一个流行库是pydicom模块。要使用pydicom库,首先在stage_1_train_images/文件夹中查找匹配的文件,找到给定patientId的DICOM文件,然后使用pydicom.read_file()方法加载数据:

patientId = df['patientId'][0]
dcm_file = '../input/stage_1_train_images/%s.dcm' % patientId
dcm_data = pydicom.read_file(dcm_file)

print(dcm_data)
(0008, 0005) Specific Character Set              CS: 'ISO_IR 100'
(0008, 0016) SOP Class UID                       UI: Secondary Capture Image Storage
(0008, 0018) SOP Instance UID                    UI: 1.2.276.0.7230010.3.1.4.8323329.28530.1517874485.775526
(0008, 0020) Study Date                          DA: '19010101'
(0008, 0030) Study Time                          TM: '000000.00'
(0008, 0050) Accession Number                    SH: ''
(0008, 0060) Modality                            CS: 'CR'
(0008, 0064) Conversion Type                     CS: 'WSD'
(0008, 0090) Referring Physician's Name          PN: ''
(0008, 103e) Series Description                  LO: 'view: PA'
(0010, 0010) Patient's Name                      PN: '0004cfab-14fd-4e49-80ba-63a80b6bddd6'
(0010, 0020) Patient ID                          LO: '0004cfab-14fd-4e49-80ba-63a80b6bddd6'
(0010, 0030) Patient's Birth Date                DA: ''
(0010, 0040) Patient's Sex                       CS: 'F'
(0010, 1010) Patient's Age                       AS: '51'
(0018, 0015) Body Part Examined                  CS: 'CHEST'
(0018, 5101) View Position                       CS: 'PA'
(0020, 000d) Study Instance UID                  UI: 1.2.276.0.7230010.3.1.2.8323329.28530.1517874485.775525
(0020, 000e) Series Instance UID                 UI: 1.2.276.0.7230010.3.1.3.8323329.28530.1517874485.775524
(0020, 0010) Study ID                            SH: ''
(0020, 0011) Series Number                       IS: '1'
(0020, 0013) Instance Number                     IS: '1'
(0020, 0020) Patient Orientation                 CS: ''
(0028, 0002) Samples per Pixel                   US: 1
(0028, 0004) Photometric Interpretation          CS: 'MONOCHROME2'
(0028, 0010) Rows                                US: 1024
(0028, 0011) Columns                             US: 1024
(0028, 0030) Pixel Spacing                       DS: ['0.14300000000000002', '0.14300000000000002']
(0028, 0100) Bits Allocated                      US: 8
(0028, 0101) Bits Stored                         US: 8
(0028, 0102) High Bit                            US: 7
(0028, 0103) Pixel Representation                US: 0
(0028, 2110) Lossy Image Compression             CS: '01'
(0028, 2114) Lossy Image Compression Method      CS: 'ISO_10918_1'
(7fe0, 0010) Pixel Data                          OB: Array of 142006 bytes

为了脱密,大多数包含病人可识别信息的标准头信息都已匿名化(删除),因此我们只剩下相对稀疏的元数据集。我们将要访问的主要字段是下面的像素数据:

im = dcm_data.pixel_array
print(type(im))
print(im.dtype)
print(im.shape)

uint8
(1024, 1024)

参考

正如我们在这里看到的,像素数组数据存储为Numpy数组,这是一个强大的数字Python库,用于处理和操作矩阵数据(以及其他东西)。此外,显而易见的是,我们已预先处理了最初的x光片,详情如下:

  • 相对较高的动态范围,高位深度的原始图像被重新调整为8位编码(256个灰度)。对于放射学家来说,这意味着图像已经被打开并被夷为平地。在临床实践中,通常由放射科医生手工操作图像位深度,以突出某些疾病过程。为了直观地评估自动位深缩小的质量,并考虑潜在地改善这一基线,考虑咨询放射科医生。(没有医生可以咨询。。)

  • 相对大型的原始图像矩阵(通常在>2000 x 2000中获得)被调整为数据科学友好的形状1024 x 1024。就这一挑战的目的而言,大多数肺炎病例的诊断通常可以通过该决议作出。为了直观地评估这种分辨率下诊断的可行性,并确定肺炎检测的最佳分辨率(通常可以在小于1024 x 1024的分辨率下进行),考虑咨询放射学家医生。

可视化一个例子

 

pylab.imshow(im, cmap=pylab.cm.gist_gray)
pylab.axis('off')

 

(-0.5, 1023.5, 1023.5, -0.5)

探索数据和标签:

正如上面提到的,如果有几个不同的可疑的肺炎区域,任何病人都可能有矩形区域。要将当前CSV文件dataframe折叠到具有唯一条目的字典中,请考虑以下方法:

def parse_data(df):
    """
    Method to read a CSV file (Pandas dataframe) and parse the 
    data into the following nested dictionary:

      parsed = {
        
        'patientId-00': {
            'dicom': path/to/dicom/file,
            'label': either 0 or 1 for normal or pnuemonia, 
            'boxes': list of box(es)
        },
        'patientId-01': {
            'dicom': path/to/dicom/file,
            'label': either 0 or 1 for normal or pnuemonia, 
            'boxes': list of box(es)
        }, ...

      }

    """
    # --- Define lambda to extract coords in list [y, x, height, width]
    extract_box = lambda row: [row['y'], row['x'], row['height'], row['width']]

    parsed = {}
    for n, row in df.iterrows():
        # --- Initialize patient entry into parsed 
        pid = row['patientId']
        if pid not in parsed:
            parsed[pid] = {
                'dicom': '../input/stage_1_train_images/%s.dcm' % pid,
                'label': row['Target'],
                'boxes': []}

        # --- Add box if opacity is present
        if parsed[pid]['label'] == 1:
            parsed[pid]['boxes'].append(extract_box(row))

    return parsed

parsed = parse_data(df)

 

正如我们所看,病人 00436515-870c-4b36-a041-de91049b9ab4 患有肺炎,所以看一下新的解析字典,看看他的相应矩阵。

print(parsed['00436515-870c-4b36-a041-de91049b9ab4'])
{'dicom': '../input/stage_1_train_images/00436515-870c-4b36-a041-de91049b9ab4.dcm', 'label': 1, 'boxes': [[152.0, 264.0, 379.0, 213.0], [152.0, 562.0, 453.0, 256.0]]}

可视化矩阵区域

为了在原始的灰度DICOM文件上覆盖彩色矩阵,请考虑使用以下方法(主函数draw()依赖于overlay_box()):

def draw(data):
    """
    Method to draw single patient with bounding box(es) if present 

    """
    # --- Open DICOM file
    d = pydicom.read_file(data['dicom'])
    im = d.pixel_array

    # --- Convert from single-channel grayscale to 3-channel RGB
    im = np.stack([im] * 3, axis=2)

    # --- Add boxes with random color if present
    for box in data['boxes']:
        rgb = np.floor(np.random.rand(3) * 256).astype('int')
        im = overlay_box(im=im, box=box, rgb=rgb, stroke=6)

    pylab.imshow(im, cmap=pylab.cm.gist_gray)
    pylab.axis('off')

def overlay_box(im, box, rgb, stroke=1):
    """
    Method to overlay single box on image

    """
    # --- Convert coordinates to integers
    box = [int(b) for b in box]
    
    # --- Extract coordinates
    y1, x1, height, width = box
    y2 = y1 + height
    x2 = x1 + width

    im[y1:y1 + stroke, x1:x2] = rgb
    im[y2:y2 + stroke, x1:x2] = rgb
    im[y1:y2, x1:x1 + stroke] = rgb
    im[y1:y2, x2:x2 + stroke] = rgb

    return im

正如我们上面看到的,患者00436515-870c-4b36-a041-de91049b9ab4有肺炎,所以让我们看看覆盖的边框:

draw(parsed['00436515-870c-4b36-a041-de91049b9ab4'])

探索细节标签

在这个挑战中,主要的目的将是检测由二元分类组成的边界框---例如。肺炎的存在或不存在。然而,除了二元分类外,每一个没有肺炎的边界框都被进一步分类为正常或没有肺不透明/不正常。这额外的第三类表明,虽然肺炎被确定不存在,但在图像上仍然有某种类型的异常——通常这个发现可能模仿真实肺炎的外观。记住,这个额外的类是作为补充信息提供的,以帮助提高算法的准确性;生成这个单独的类将不会用来评估在这次比赛中的表现。

如上所述,我们看到CSV文件中的第一个病人没有肺炎。让我们看看这个病人的详细标签信息:

df_detailed = pd.read_csv('../input/stage_1_detailed_class_info.csv')
print(df_detailed.iloc[0])
patientId    0004cfab-14fd-4e49-80ba-63a80b6bddd6
class                No Lung Opacity / Not Normal
Name: 0, dtype: object

正如我们在这里看到的,病人没有肺炎,但有另一个影像学异常存在。让我们仔细看看:

patientId = df_detailed['patientId'][0]
draw(parsed[patientId])

虽然在笔记本内显示的图像很小,但作为一名放射科医生,很明显病人的左肺(图像右侧)有几个边界清楚的结节密度。另外,在右肺(图片左侧)有一个大的胸腔管,放置在右肺基底部,用来排走积液(如胸腔积液),也显示出重叠的斑片状密度(如肺不张或部分肺塌陷)。

正如你所看到的,在图像上有很多异常,而这些发现与肺炎无关的决定在某种程度上是主观的,甚至在专家医师中也是如此。因此,正如医学影像数据集中几乎所有的情况一样,所提供的标准标签远不是100%客观的。在开发算法时要记住这一点,并考虑咨询放射科医生,以帮助确定减轻这些离散事件的最佳策略。

标签总结

最后,让我们仔细看看数据集中标签的分布情况。为此,我们将首先解析详细的标签信息:

summary = {}
for n, row in df_detailed.iterrows():
    if row['class'] not in summary:
        summary[row['class']] = 0
    summary[row['class']] += 1
    
print(summary)
{'No Lung Opacity / Not Normal': 11500, 'Normal': 8525, 'Lung Opacity': 8964}

如我们所见,这三种类型之间有一个相对均匀的分裂,有近2/3的数据是由没有肺炎(完全正常或没有肺不透明/不正常)组成的。与大多数疾病发病率相当低的医学影像数据集相比,这个数据集在病理学上得到了显著的丰富。

下一步

现在,您已经了解了数据结构、图像文件格式和标签类型,现在是时候创建一个算法了!请记住,主要端点是检测边界框,因此您可能会考虑各种对象定位算法。另一种策略是考虑相关的分割算法家族,并承认边界框将只是一个粗略的近似真实的逐像素图像分割掩码。

最后,正如在这本笔记本中多次提到的,放射科医师可能经常提供有用的辅助信息,算法开发策略和/或额外的标签协调。除了你可以在当地接触到的医生,RSNA还将通过Kaggle在线论坛与放射科医生进行远程接触。作为一名医学专业人士,我知道我的很多同事都对开始工作很感兴趣。

你可能感兴趣的:(机器学习,Kaggle,入门翻译系列)