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在线论坛与放射科医生进行远程接触。作为一名医学专业人士,我知道我的很多同事都对开始工作很感兴趣。