#!conda install pyarrow
#!pip install pydicom kornia opencv-python scikit-image nbdev
胸部X光模型
要使用 fastai.medical.imaging
,您需要
conda install pyarrow
pip install pydicom kornia opencv-python scikit-image
要在 Google Colab 上运行本教程,您需要取消注释以下两行并运行单元格
from fastai.basics import *
from fastai.callback.all import *
from fastai.vision.all import *
from fastai.medical.imaging import *
import pydicom
import pandas as pd
下载并导入 X 光 DICOM 文件
首先,我们将使用 untar_data
函数下载包含 SIIM-ACR 气胸分割 [1] 数据集子集(250 个 DICOM 文件,约 30MB)的 siim_small 文件夹。下载的 siim_small 文件夹将存储在您的 ~/.fastai/data/ 目录下。一旦下载完成,变量 pneumothorax-source
将存储 siim_small 文件夹的绝对路径。
= untar_data(URLs.SIIM_SMALL) pneumothorax_source
siim_small 文件夹具有以下目录/文件结构
什么是 DICOM?
DICOM(Digital Imaging and COmmunications in Medicine,医学数字成像和通信)是事实上的标准,它建立了规则,允许来自不同供应商的成像设备、计算机和医院之间交换医学图像(X 射线、MRI、CT)及相关信息。DICOM 格式提供了一种适当的方式,符合健康信息交换 (HIE) 标准,用于在设施之间传输健康相关数据,并符合 HL7 标准,后者是使临床应用程序能够交换数据的消息传递标准
DICOM 文件通常具有 .dcm
扩展名,并提供了一种将数据存储在单独的“标签”中的方式,例如患者信息以及图像/像素数据。一个 DICOM 文件包含一个头文件和图像数据集,打包在一个文件中。通过从这些标签中提取数据,可以访问关于患者人口统计信息、研究参数等重要信息。
16 位 DICOM 图像的值范围为 -32768
到 32768
,而 8 位灰度图像存储 0
到 255
的值。DICOM 图像中的值范围非常有用,因为它们与 Hounsfield Scale(亨氏单位)相关,亨氏单位是描述放射密度的定量标尺
绘制 DICOM 数据
为了分析我们的数据集,我们使用 get_dicom_files
函数加载 DICOM 文件的路径。调用函数时,我们将 train/ 附加到 pneumothorax_source
路径,以选择 DICOM 文件所在的文件夹。我们将每个 DICOM 文件的路径存储在 items
列表中。
= get_dicom_files(pneumothorax_source/f"train/") items
接下来,我们使用 RandomSplitter
函数将 items
列表分割为训练集 trn
和验证集 val
列表
= RandomSplitter()(items) trn,val
Pydicom 是一个用于解析 DICOM 文件的 Python 包,它使得访问 DICOM 的 header
以及将原始 pixel_data
转换为便于操作的 Python 结构变得更容易。fastai.medical.imaging
使用 pydicom.dcmread
来加载 DICOM 文件。
要绘制 X 射线图像,我们可以选择 items
列表中的一个条目,并使用 dcmread
加载 DICOM 文件。
= 7
patient = items[patient].dcmread() xray_sample
要查看 header
xray_sample
Dataset.file_meta -------------------------------
(0002, 0000) File Meta Information Group Length UL: 200
(0002, 0001) File Meta Information Version OB: b'\x00\x01'
(0002, 0002) Media Storage SOP Class UID UI: Secondary Capture Image Storage
(0002, 0003) Media Storage SOP Instance UID UI: 1.2.276.0.7230010.3.1.4.8323329.3297.1517875177.149805
(0002, 0010) Transfer Syntax UID UI: JPEG Baseline (Process 1)
(0002, 0012) Implementation Class UID UI: 1.2.276.0.7230010.3.0.3.6.0
(0002, 0013) Implementation Version Name SH: 'OFFIS_DCMTK_360'
-------------------------------------------------
(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.3297.1517875177.149805
(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: '6633c659-9249-443e-9851-b83782d1b111'
(0010, 0020) Patient ID LO: '6633c659-9249-443e-9851-b83782d1b111'
(0010, 0030) Patient's Birth Date DA: ''
(0010, 0040) Patient's Sex CS: 'M'
(0010, 1010) Patient's Age AS: '21'
(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.3297.1517875177.149804
(0020, 000e) Series Instance UID UI: 1.2.276.0.7230010.3.1.3.8323329.3297.1517875177.149803
(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 161452 elements
解释每个元素超出了本教程的范围,但 这个 网站包含关于每个条目的一些优秀信息
关于上述标签信息的一些关键点
- 像素数据 (7fe0 0010) - 这是存储原始像素数据的地方。每个图像平面的像素编码顺序是自左向右,自上向下,即左上角像素(标记为 1,1)首先编码
- 光度解释 (0028, 0004) - 也称为色彩空间。在这种情况下是
MONOCHROME2
,其中像素数据表示为单个单色图像平面,低值=暗,高值=亮。如果色彩空间是MONOCHROME
,则低值=亮,高值=暗。 - 每像素样本数 (0028, 0002) - 这应该为 1,因为此图像是单色的。如果色彩空间是 RGB,则此值将为 3
- 存储位数 (0028 0101) - 每个像素样本存储的位数。典型的 8 位图像像素范围在
0
到255
之间 - 像素表示 (0028 0103) - 可以是无符号 (0) 或有符号 (1)
- 有损图像压缩 (0028 2110) -
00
图像未经过有损压缩。01
图像经过有损压缩。 - 有损图像压缩方法 (0028 2114) - 指明使用的有损压缩类型(在此例中
ISO_10918_1
表示 JPEG 有损压缩) - 像素数据 (7fe0, 0010) - 包含 161452 个元素的数组,代表图像像素数据,pydicom 使用它将像素数据转换为图像。
PixelData
看起来像什么?
200] xray_sample.PixelData[:
b'\xfe\xff\x00\xe0\x00\x00\x00\x00\xfe\xff\x00\xe0\x9cv\x02\x00\xff\xd8\xff\xdb\x00C\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02\x02\x03\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05\x06\t\x08\n\n\t\x08\t\t\n\x0c\x0f\x0c\n\x0b\x0e\x0b\t\t\n\x11\n\x0e\x0f\x10\x10\x11\x10\n\x0c\x12\x13\x12\x10\x13\x0f\x10\x10\x10\xff\xc0\x00\x0b\x08\x04\x00\x04\x00\x01\x01\x11\x00\xff\xc4\x00\x1d\x00\x00\x02\x03\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x02\x03\x06\x00\x01\x07\x08\t\xff\xc4\x00R\x10\x00\x02\x01\x03\x03\x02\x04\x03\x06\x05\x04\x00\x04\x01\x02\x17\x01\x02\x11\x00\x03!\x04\x121\x05A\x13"Qa\x06q\x81\x142\x91\xa1\xb1\xf0#B\xc1\xd1\xe1\x07\x15R\xf1\x16$3br\x08%4C&cs\x82\x92\xa2'
由于解释 PixelData
的复杂性,pydicom 提供了一种方便的方式以便捷的形式获取它:pixel_array
,它返回一个包含像素数据的 numpy.ndarray
xray_sample.pixel_array, xray_sample.pixel_array.shape
(array([[ 0, 0, 0, ..., 13, 13, 5],
[ 0, 0, 0, ..., 13, 13, 5],
[ 0, 0, 0, ..., 13, 12, 5],
...,
[ 0, 0, 0, ..., 5, 3, 0],
[ 0, 0, 0, ..., 6, 4, 0],
[ 0, 0, 0, ..., 8, 5, 0]], dtype=uint8),
(1024, 1024))
然后可以使用 show
函数查看图像
xray_sample.show()
您还可以使用 from_dicoms
方便地为数据集中的所有图像创建一个包含所有 tag
信息作为列的数据框
= pd.DataFrame.from_dicoms(items)
dicom_dataframe 5] dicom_dataframe[:
特定字符集 | SOPClassUID | SOPInstanceUID | 研究日期 | 研究时间 | 入院号 | 模式 | 转换类型 | 转诊医师姓名 | 序列描述 | ... | 有损图像压缩 | 有损图像压缩方法 | 文件名 | 多像素间距 | 像素间距1 | 图像最小值 | 图像最大值 | 图像均值 | 图像标准差 | 图像窗口百分比 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ISO_IR 100 | 1.2.840.10008.5.1.4.1.1.7 | 1.2.276.0.7230010.3.1.4.8323329.6904.1517875201.850819 | 19010101 | 000000.00 | CR | WSD | 视图:PA | ... | 01 | ISO_10918_1 | C:\Users\tijme\.fastai\data\siim_small\train\No Pneumothorax\000000.dcm | 1 | 0.168 | 0 | 254 | 160.398039 | 53.854885 | 0.087029 | ||
1 | ISO_IR 100 | 1.2.840.10008.5.1.4.1.1.7 | 1.2.276.0.7230010.3.1.4.8323329.11028.1517875229.983789 | 19010101 | 000000.00 | CR | WSD | 视图:PA | ... | 01 | ISO_10918_1 | C:\Users\tijme\.fastai\data\siim_small\train\No Pneumothorax\000002.dcm | 1 | 0.143 | 0 | 250 | 114.524713 | 70.752315 | 0.326269 | ||
2 | ISO_IR 100 | 1.2.840.10008.5.1.4.1.1.7 | 1.2.276.0.7230010.3.1.4.8323329.11444.1517875232.977506 | 19010101 | 000000.00 | CR | WSD | 视图:PA | ... | 01 | ISO_10918_1 | C:\Users\tijme\.fastai\data\siim_small\train\No Pneumothorax\000005.dcm | 1 | 0.143 | 0 | 246 | 132.218334 | 73.023531 | 0.266901 | ||
3 | ISO_IR 100 | 1.2.840.10008.5.1.4.1.1.7 | 1.2.276.0.7230010.3.1.4.8323329.32219.1517875159.70802 | 19010101 | 000000.00 | CR | WSD | 视图:PA | ... | 01 | ISO_10918_1 | C:\Users\tijme\.fastai\data\siim_small\train\No Pneumothorax\000006.dcm | 1 | 0.171 | 0 | 255 | 153.405355 | 59.543063 | 0.144505 | ||
4 | ISO_IR 100 | 1.2.840.10008.5.1.4.1.1.7 | 1.2.276.0.7230010.3.1.4.8323329.32395.1517875160.396775 | 19010101 | 000000.00 | CR | WSD | 视图:PA | ... | 01 | ISO_10918_1 | C:\Users\tijme\.fastai\data\siim_small\train\No Pneumothorax\000007.dcm | 1 | 0.171 | 0 | 250 | 166.198407 | 50.008985 | 0.053009 |
5 行 x 42 列
接下来,我们需要加载数据集的标签。我们使用 pandas 导入 labels.csv 文件并打印前五个条目。file 列显示 .dcm 文件的相对路径,label 列指示胸部 X 射线是否患有气胸。
= pd.read_csv(pneumothorax_source/f"labels.csv")
df df.head()
文件 | 标签 | |
---|---|---|
0 | train/No Pneumothorax/000000.dcm | 无气胸 |
1 | train/Pneumothorax/000001.dcm | 气胸 |
2 | train/No Pneumothorax/000002.dcm | 无气胸 |
3 | train/Pneumothorax/000003.dcm | 气胸 |
4 | train/Pneumothorax/000004.dcm | 气胸 |
现在,我们使用 DataBlock
类准备 DICOM 数据进行训练。
由于我们处理的是 DICOM 图像,我们需要使用 PILDicom
作为 ImageBlock
类别。这样 DataBlock
就会知道如何打开 DICOM 图像。由于这是一个二分类任务,我们将使用 CategoryBlock
= DataBlock(blocks=(ImageBlock(cls=PILDicom), CategoryBlock),
pneumothorax =lambda x:pneumothorax_source/f"{x[0]}",
get_x=lambda x:x[1],
get_y=[*aug_transforms(size=224),Normalize.from_stats(*imagenet_stats)])
batch_tfms
= pneumothorax.dataloaders(df.values, num_workers=0) dls
此外,我们绘制第一个批次并应用指定的变换
= pneumothorax.dataloaders(df.values)
dls =16) dls.show_batch(max_n
Due to IPython and Windows limitation, python multiprocessing isn't available now.
So `number_workers` is changed to 0 to avoid getting stuck
训练
然后我们可以使用 vision_learner
函数并开始训练。
= vision_learner(dls, resnet34, metrics=accuracy) learn
请注意,如果您不选择损失函数或优化器函数,fastai 将尝试为任务选择最佳选项。您可以通过调用 loss_func
查看损失函数
learn.loss_func
FlattenedLoss of CrossEntropyLoss()
您也可以通过调用 opt_func
对优化器执行相同的操作
learn.opt_func
<function fastai.optimizer.Adam(params, lr, mom=0.9, sqr_mom=0.99, eps=1e-05, wd=0.01, decouple_wd=True)>
使用 lr_find
尝试找到最佳学习率
learn.lr_find()
SuggestedLRs(lr_min=0.005754399299621582, lr_steep=0.0063095735386013985)
1) learn.fit_one_cycle(
周期 | 训练损失 | 验证损失 | 准确率 | 时间 |
---|---|---|---|---|
0 | 1.191782 | 2.123666 | 0.320000 | 00:37 |
/f"train/Pneumothorax/000004.dcm") learn.predict(pneumothorax_source
当对图像进行预测时,learn.predict
返回一个元组(类别、类别张量和 [每个类别的概率])。在此数据集中,只有 无气胸
和 气胸
两个类别,因此每个概率都有两个值,第一个值是图像属于 类别 0
或 无气胸
的概率,第二个值是图像属于 类别 1
或 气胸
的概率
= learn.tta(use_max=True) tta
=16) learn.show_results(max_n
= Interpretation.from_learner(learn) interp
2) interp.plot_top_losses(
结果评估
医疗模型的影响通常很高,因此了解模型在检测特定疾病方面的性能至关重要。
该模型的准确率为 56%。准确率可以定义为正确预测的数据点数占所有数据点数的比例。然而,在此上下文中,我们可以将准确率定义为模型正确判断患者患病的概率**加上**模型正确判断患者未患病的概率
在评估医疗模型时,还需要用到其他一些关键术语
假阳性与假阴性
假阳性是一种错误,其中测试结果错误地表明存在某种疾病等疾病状况(结果为阳性),而实际上并不存在
假阴性是一种错误,其中测试结果错误地表明不存在某种疾病等疾病状况(结果为阴性),而实际上存在
敏感性与特异性
- 敏感性或真阳性率是指模型在患者确实患有疾病的情况下将患者分类为患有疾病。敏感性衡量避免假阴性的能力
示例:一项新测试对 10,000 名患者进行了测试,如果新测试的敏感性为 90%,则测试将正确检测出 9,000 名(真阳性)患者,但会遗漏 1000 名(假阴性)实际患有疾病但被测试为未患病的患者
- 特异性或真阴性率是指模型在患者确实未患有疾病的情况下将患者分类为未患有疾病。特异性衡量避免假阳性的能力
理解和使用敏感性、特异性和预测值 是一篇很棒的论文,如果您有兴趣了解更多关于理解敏感性、特异性和预测值的知识,可以阅读此文。
PPV 和 NPV
大多数医学测试通过 PPV(阳性预测值)或 NPV(阴性预测值)进行评估。
PPV - 如果模型预测患者患有某种疾病,则患者实际患有该疾病的概率是多少
NPV - 如果模型预测患者未患有某种疾病,则患者实际未患有该疾病的概率是多少
PPV 的理想值(在完美的测试中)为 1(100%),最差可能的值为零
NPV 的理想值(在完美的测试中)为 1(100%),最差可能的值为零
混淆矩阵
混淆矩阵是针对 valid
数据集绘制的
= ClassificationInterpretation.from_learner(learn)
interp = interp.top_losses()
losses,idxs len(dls.valid_ds)==len(losses)==len(idxs)
=(7,7)) interp.plot_confusion_matrix(figsize
您也可以像这样重现从 plot_confusion_matrix 中解释的结果
= interp.confusion_matrix()
upp, low = upp[0], upp[1]
tn, fp = low[0], low[1]
fn, tp print(tn, fp, fn, tp)
23 13 12 2
请注意,敏感性 = 真阳性/(真阳性 + 假阴性)
= tp/(tp + fn)
sensitivity sensitivity
0.14285714285714285
在这种情况下,模型的敏感性为 40%,因此只能正确检测出 40% 的真阳性(即患有气胸的人),但会遗漏 60% 的假阴性(实际患有气胸但被告知未患病的患者!这不是一个好的情况)。
这也称为第二类错误
特异性 = 真阴性/(假阳性 + 真阴性)
= tn/(fp + tn)
specificity specificity
0.6388888888888888
该模型的特异性为 63%,因此可以正确检测出 63% 的时间患者未患有气胸,但会错误地将 37% 的患者分类为患有气胸(假阳性),而他们实际上并未患病。
这也称为第一类错误
阳性预测值 (PPV)
= tp/(tp+fp)
ppv ppv
0.13333333333333333
在这种情况下,模型在正确预测气胸患者方面的表现较差
阴性预测值 (NPV)
= tn/(tn+fn)
npv npv
0.6571428571428571
该模型在预测无气胸患者方面表现更好
计算准确率
如前所述,此模型的准确率为 56%,但这如何计算得来?我们可以将准确率视为
准确率 = 敏感性 * 患病率 + 特异性 * (1 - 患病率)
其中患病率是一个统计概念,指特定时间点特定人群中疾病的病例数。在此情况下,患病率是指验证集中患病的患者数与总患者数之比。
要查看验证集中的文件,可以调用 dls.valid_ds.cat
= dls.valid_ds.cat
val #val[0]
验证集中有 15 张气胸图像(总共有 50 张图像,可以通过使用 len(dls.valid_ds)
查看),因此这里的患病率为 15/50 = 0.3
= 15/50
prevalence prevalence
0.3
= (sensitivity * prevalence) + (specificity * (1 - prevalence))
accuracy accuracy
0.490079365079365
引用
[1] Filice R et al. 利用 NIH 胸部 X 光数据集上的机器学习标注进行气胸标注的众包。J Digit Imaging (2019)。https://doi.org/10.1007/s10278-019-00299-9