1. 程式人生 > >kaggle 入門系列翻譯(三) RSNA 肺炎預測

kaggle 入門系列翻譯(三) RSNA 肺炎預測

概述

比賽主要用來識別二維高解析度影象的胸片中是否存在肺炎的區域。肺炎只是導致胸片顯示出問題的一種可能,且每幅圖可能有數個肺炎區域或沒有肺炎區域。

文章由一個放射科醫師和機器學習雙重專家編寫,介紹該資料集的底層結構、成像結構和標籤型別

首先匯入依賴庫:

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)
<class 'numpy.ndarray'>
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線上論壇與放射科醫生進行遠端接觸。作為一名醫學專業人士,我知道我的很多同事都對開始工作很感興趣。