import os
import re
import torch
import warnings
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
torch.backends.cudnn.benchmark=True
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
import cv2
import torchvision
from torch import nn
from torch import optim
from torchvision import transforms as T
from torchvision import models as M
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import matplotlib as mlp
import matplotlib.pyplot as plt
import seaborn as sns
import random
import numpy as np
import pandas as pd
import datetime
from time import time
import gc
torch.manual_seed(1412)
random.seed(1412)
np.random.seed(1412)
torch.cuda.is_available()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
【案例】Kaggle医学影像识别
1 案例背景与benchmark建立
深度学习最为关键的应用领域之一是医疗医护及卫生领域。医疗是一个与人类生存紧密相关的行业,也是一个疑难问题浩如烟海、资源稀缺、供不应求的行业。在2020年,全球大约有3.8亿人缺乏最低程度的医疗保障,禽流感、新冠等全球传染病更是时刻冲击着全球医疗系统,数以亿计的患者与医护人员渴望更多的资源、更高的效率、更低的成本,深度学习正是一项提效减负的关键技术。今天,深度学习被广泛应用于医疗行业的各个领域,例如药物研究、蛋白质合成、基因组学分析、药方审计、基础诊断、心理健康等。其中,以计算机视觉技术为代表的医疗影像识别是最为成熟、需求也最为广泛的应用。
计算机视觉可以识别核磁共振图像、CT(断层扫描)图像、超声影像、病理学图像、放射学图像等常见的医疗成像数据,这些影像往往覆盖病变组织或健康组织,被广泛应用于临床诊断和手术辅助。深度学习模型可以通过结合这些成像数据的各个方面来做出有效的诊断和解释,例如组织大小、体积、形状、颜色、质感等。现在,深度学习在糖尿病视网膜病变检测、阿尔兹海默症的早期检测、乳腺结节等超声检测方面已经有较多研究成果。在2018年,卷积神经网络在皮肤科图像中识别出黑色素瘤疾病的准确率已经比专家高出10%以上。
在本次案例课程当中,我们要完成的是病理图像识别诊断。病理检查是当代最为可靠的肿瘤性质确诊方式,其结果就是临床的最后诊断,病理图像就是病变组织在光学显微镜下所呈现的形态图像,病理学家需要观察该图像判定肿瘤的性质(良性/恶性,如果恶性具体是什么病变亚型),同时也需要判定肿瘤的侵略性分级(早期/中期/晚期)。
然而,为执行病理检查,患者需要在疑似肿瘤的组织上提取部分细胞或器官。若肿瘤为恶性,此行为相当于在癌变组织上创造一个伤口,携带癌细胞的血液会外溢并扩散至全身,极大程度地提升癌症转移可能性、降低患者生存概率。因此,能够迅速、准确地做出病理诊断,对癌症患者具有极大的意义。
在我们的案例当中,我们将使用卷积神经网络代替病理专家和医护人员,识别出病理图像中所有带恶性病灶的区域。如图所示,左侧是带有恶性病灶的原始图像,右侧则是通过深度学习完成的像素级识别。在原始图像中,病理专家人为绘制了绿色线作为标签,绿色线内部的像素则被标记为恶性病灶(标记为1),绿色线外部的像素则被标记为非恶性病灶(标记为0)。而深度学习的工作,就是利用卷积神经网络精准判断出带有恶性病灶的像素块,以判断病理图像中是否含有恶性病灶,若存在恶性病灶,也可快速精准判断出病灶大小及扩散程度,帮助患者确定肿瘤的侵略性分级。
在本次案例中,我们所使用的数据是美国国家生物技术信息中心(NCBI)所提供真实的复现浸润性导管癌(IDC)病理检查图像,由搭载标本的整体载玻片按40倍放大扫描而来,图像的像素大约在200w~400w之间(图片大小为1200*1920以上)。该数据的完整版本首次出现于哥伦比亚大学的论文《使用卷积神经网络自动检测病理图像中的浸润性导管癌》(Automatic detection of invasive ductal carcinoma in whole slide images with Convolutional Neural Networks),后来Kaggle联合美国国家医学图书馆共同整理、披露了部分患者数据,专供深度学习研究者使用。
为实现像素级识别,研究团队在论文当中将所有图像切分成了50x50的方片数据(patch)。在课程当中,我们提供了分割后的数据压缩文件:IDC_regular_ps50_idx5.zip。解压之后,即可看到以患者ID为名的文件夹:
每个文件夹内部即是标签为0和为1的文件夹,其中标签0表示非恶性病灶,标签1表示恶性病灶:
标签文件夹下是被分割后的方片数据。分割后,每张图像文件名一般呈现为10253_idx5_x1351_y1101_class0.png 的格式,其中idx5 之前的数字为患者ID,最后的class为样本的性质,其中class0代表非恶性(也称阴性),class1代表恶性(阳性)。中间的x数字_y数字 则表示该方片的左上角位于原始图像中的坐标。
在本次案例当中,我们的目标是超越论文《使用卷积神经网络自动检测病理图像中的浸润性导管癌》中呈现的结果。在论文当中,哥伦比亚大学使用了F1分数与平衡后的准确率(Balanced Accuracy,简写为BAC)。
- 将论文中提供的最好结果作为我们的benchmark
| 精确度 Pr | 召回率/敏感度 Rc/Sen | 特异度 Spc | F1分数 | BAC |
---|
ColUni | 0.6540 | 0.7960 | 0.8886 | 0.7180 | 0.8423 |
其中,两个核心评估指标的计算公式为:
F
1
=
2
?
P
r
?
R
c
P
r
+
R
c
F1 = \frac{2* Pr * Rc}{Pr + Rc}
F1=Pr+Rc2?Pr?Rc?
B
A
C
=
S
e
n
+
S
p
c
2
BAC = \frac{Sen + Spc}{2}
BAC=2Sen+Spc?
通常来说我们是向着某个损失最小化的方向训练卷积神经网络,因此PyTorch中默认提供的迭代目标基本都是损失函数。为保持与论文中评估指标一致,我们可以自定义损失及评估指标。接下来,就让我们以超越0.8423的BAC为目标开启我们的案例。
2 基于OpenCV批量分片高像素影像
为了更加精确的诊断和治疗,医疗影像往往是大像素(1920x1080)或超大像素图像(4k图像4096x2160)。这类图像的尺寸与深度学习实验数据常见尺寸(227x227,或32x32)有巨大的差别,不仅对网络的深度要求更高,所需的算力、内存、运算时间等成本也会更高。因此,在处理医疗图像或遥感图像这样的高像素图像时,往往都需要将其批量处理成小像素方片,并针对每一个方片进行识别或预测。
幸运的是,在本次案例当中所使用的IDC病理检查图像已经被哥伦比亚大学的论文团队分割成了小像素方片,因此无需我们再人工进行处理,然而掌握高像素图像的分片技巧十分必要,因此本节我们将以数张大像素的图像为例展示批量分片高像素图像的代码与相关知识。
注意,考虑到各类器官的高像素病灶影像可能引起不适,本节我们将不会使用IDC病理检查数据集的原始图像,而是使用与病理图像高度相似的星辰图来替代。星辰与病灶细胞一样,可能分布在图像的各个位置,也可能集中在图像上的某个区域,因此适合用来作为替代图。所有图像都位于文件夹Universe当中。我们先以一张图像为例进行分片:
import cv2
import os
import matplotlib.pyplot as plt
cv2.__version__
PATH = "/Users/zhucan/Desktop/桌面内容/00 深度学习二期课程课件/Lesson 18/data/Universe"
img = cv2.imread(os.path.join(PATH,"Universe01.jpg"))
if img is None:
print("failed")
else:
print("succeed")
img.shape
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy()
plt.figure(dpi=200)
plt.imshow(imgcopy)
plt.axis("off");
imgcopy.shape
imgcopy.shape[0] * imgcopy.shape[1]
我们的最终目标:将图像分割为数千个小型方片:
分割的本质就是先裁剪图像、再对裁剪后的图像进行保存。例如,假设我们现在要将以下图像按中线分割为两部分,最简单的手段就是先在原始图像上裁剪出左半边的图像,保存为一个单独的文件,再在原始图像上裁剪出右半边的图像,保存为一个单独的文件。在这个过程当中,我们没有对原始图像做任何的更改,却可以得到两张新的图像。 由于图像数据都是三维矩阵,因此只要通过索引就可以轻松将图像进行裁剪。对一张图像来说,索引的顺序是从上到下、从左到右,因此,如果要取出整个图像的左侧,则需要对图像进行如下索引:
imgcopy.shape
imgcopy[:,0:1280].shape
plt.figure(dpi=140)
plt.imshow(imgcopy[:,0:1280])
plt.axis("off");
plt.figure(dpi=140)
plt.imshow(imgcopy[:,1280:])
plt.axis("off");
不难发现,对图像进行截取时,只需要按照图像[高的起点:高的终点,宽的起点:宽的终点] 进行索引即可,只要我们知道图像当中的相对位置(四个坐标),就可以按照任意的起点和终点截取任何尺寸、任何形状的图片。接下来,当我们索引出想要的图像后,只需要将这张图像保存成新的图片文件即可。在这里我们要使用的是opencv中的经典方法imwrite:
cv2.imwrite(保存图像的目录+文件名,需要保存的图片对象)
例如,上述星辰图的左侧保存到之前设置好的PATH目录,则有:
PATH
os.path.join(PATH,"star1.jpg")
cv2.imwrite(os.path.join(PATH,"star2.jpg"),imgcopy[:,0:1280])
返回True,则说明保存成功,如果报错或返回False则保存失败。此时我们可以在PATH设定的目录下查看保存出的文件。
将图像分割为小尺寸方片的原理与将图像分割成两半的原理完全一致:首先,从图像中裁剪出我们需要的小尺寸方片,然后将该方片对象保存为一张新的图像。然而,一张大尺寸图像可以被分割为成千上万的小尺寸方片,因此我们需要将每个小方片一一索引出来,再一一保存: 我们必然不可能手动一一裁剪方片,因此我们需要一个循环来帮助我们进行分割。观察上图,每个方片的尺寸为50x50,左上角第一个被切分出的方片索引为图像[:50,:50] ,紧接着左数第二个方片的索引为图像[:50,50:100] ,第三个方片的索引为图像[:50,100:150] 以此类推,第一行的所有方片都可以被表示为图像[:50,x:x+50] 。可见,我们只需要在宽度上进行循环,每次让宽的起点增加50,宽的终点增加50,就可以实现第一行的截取和分割。同理,第一列的所有方片都可以被表示为图像[y:y+50,:50] ,所以只要在高度上进行循环,每次让高的起点增加50,高的终点增加50,就可以实现第一列的截取和分割。那只要同时在高和宽上进行循环,就可以实现对上图中所有方片的截取和分割。
以宽为例,我们可以有:
imgcopy.shape[1]
[*range(0,imgcopy.shape[1],50)]
for x in range(0,imgcopy.shape[1],50):
print("[{}:{}]".format(x,x+50))
[0:50]
imgcopy[:50,2550:2600].shape
现在,同时对宽和高执行循环:
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy()
imgheight = imgcopy.shape[0]
imgwidth = imgcopy.shape[1]
patch_height = 50
patch_weight = 50
for y in range(0, imgheight, patch_height):
for x in range(0, imgwidth, patch_weight):
if patch_height > imgheight or patch_weight > imgwidth:
print("方片尺寸过大,超过原始尺寸")
break
y_ = y + patch_height
x_ = x + patch_weight
if imgheight >= y_ and imgwidth >= x_:
patch = imgcopy[y:y_, x:x_]
cv2.imwrite(os.path.join(PATH,"patches"
,"universe"+"x"+str(x)+"_"+str(x_)+"y"+str(y)+"_"+ str(y_) +".jpg")
, patch)
cv2.rectangle(imgcopy
, (x, y), (x_, y_)
, (255, 255, 255)
, 2
)
plt.figure(dpi=300)
plt.imshow(imgcopy)
plt.axis("off");
现在可以检查目标文件夹,观察所有被分割的图像了。
当我们有大量的大型图像需要处理时,我们首先要从文件夹中将所有的图像都读入。在这里,我们需要借助os库下著名函数listdir的力量:
os.listdir(path):读取目录中所有的文件名
只要有文件名,我们就可以在cv2.imread 当中按照文件名一一读入相应的图像。同时,对于不是图片数据的文件名,cv.imread 会读取失败,从而导致读入的对象为None。因此我们也需要对读入的对象进行检查,对于任何不是None的对象(也就是对于任意成功读取的图像),我们将其保存在列表当中:
def load_images_from_folder(folder):
"""批量读取文件夹中的图片"""
images = []
for filename in os.listdir(folder):
img = cv2.imread(os.path.join(folder,filename))
if img is not None:
images.append(img)
return images
images = load_images_from_folder(PATH)
PATH
os.listdir(PATH)
len(images)
for i in images:
plt.figure(dpi=200)
plt.imshow(i)
plt.axis("off");
接下来我们就可以批量分割现有图像了。每张图像会被分割成数千个切片,我们希望将来自一张图像的切片保存在同一个文件夹当中,并且该文件夹的名称就是该图片的名称。因此我们需要提取出每张图片的文件名(去掉拓展名),并且使用os中两个用于创建目录的函数: os.mkdir(path):创建path所指定的目录。注意,只有当path所指定的目录还不存在时有效,如果该目录已经存在,则报错。
os.path.exists(path):检查path所制定的目录是否已经存在。
os.listdir(PATH)
os.listdir(PATH)[1].split(".")[0]
os.path.join(PATH,os.listdir(PATH)[1].split(".")[0])
os.path.exists(os.path.join(PATH,"Universe01"))
os.path.exists(os.path.join(PATH,"Universe01.jpg"))
def extract_images_from_folder(folder):
"""对图像进行批量分片(对一个文件夹中所有的图像进行分片)"""
for filename in os.listdir(folder):
img = cv2.imread(os.path.join(folder,filename))
if img is not None:
subfolder = os.path.join(PATH,filename.split(".")[0])
if os.path.exists(subfolder):
print("folder exists")
else:
os.mkdir(subfolder)
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy()
imgheight = imgcopy.shape[0]
imgwidth = imgcopy.shape[1]
patch_height = 50
patch_weight = 50
for y in range(0, imgheight, patch_height):
for x in range(0, imgwidth, patch_weight):
if patch_height > imgheight or patch_weight > imgwidth:
break
y_ = y + patch_height
x_ = x + patch_weight
if imgheight >= y_ and imgwidth >= x_:
patch = imgcopy[y:y_, x:x_]
cv2.imwrite(os.path.join(subfolder,str(filename.split(".")[0])+"x"+str(x)+"_"+str(x_)+"y"+str(y)+"_"+ str(y_) +".jpg")
, patch)
cv2.rectangle(imgcopy
, (x, y), (x_, y_)
, (255, 255, 255)
, 2
)
plt.figure(dpi=300)
plt.imshow(imgcopy)
plt.axis("off");
extract_images_from_folder(PATH)
好了,现在你可以使用函数load_images_from_folder 和extract_images_from_folder 对任意图像进行批量分割处理了。你还可以改进这个函数,例如将每张图像需要分割的切片尺寸大小作为函数的参数,例如将是否在分割完毕后打印分割后图像作为函数的参数(毕竟如果需要分割的图像太多的话,也无需打印每一张的分割结果)。同时,你还可以加入更多功能,例如打印每一张待分割图像的尺寸,或者对每张图像分割后的切片数进行计数等等。你可以调用一切你需要的功能来完善这两个函数。
3 数据导入与数据探索
将数据分割为小方片后,我们可以开始进行数据探索了。对于一般图像数据,我们的探索一般包括以下几个步骤:
- 探索数据量/数据结构,包括图像的尺寸、数量、通道数,以确认所需的算力、架构深度以及验证策略
- 可视化图像,确认图像中的具体内容,以确定所需要使用的预处理方法
- 标签分布探索,探索类别数量、样本均衡情况,以确定迭代用的损失函数,以及是否进行均衡处理
对于被分片的病理图像,我们可能还需要:
- 查看完整数据的尺寸,以判断切除的病灶大小,切除病灶越小,说明患者的生存率越大
- 可视化完整数据,对病理数据来说,特别的,需要确定恶性病灶的占比
幸运的是,目前我们使用的数据集为普通zip压缩文件,因此只要解压缩之后就可以直接查看每张图片的大致情况,因此我们可以或许省略可视化图像这一步骤。但对于大部分深度学习所使用的图像数据集来说,还是需要读取数据后单独对图像进行查看。
PATH = "/Users/zhucan/Desktop/桌面内容/00 深度学习二期课程课件/Lesson 18/data/IDC_regular_ps50_idx5"
patients = os.listdir(PATH)
len(patients)
patients[:10]
positive_patches = 0
negative_patches = 0
for patient_id in patients:
class0_path = os.path.join(PATH,patient_id,str(0))
class0_patches = os.listdir(class0_path)
negative_patches += len(class0_patches)
class1_path = os.path.join(PATH,patient_id,str(1))
class1_patches = os.listdir(class1_path)
positive_patches += len(class1_patches)
positive_patches
negative_patches
total_patches = positive_patches + negative_patches
total_patches
现在我们可以获得的数据集中包含279个患者的病理图像,一共27.7w张方片图像,其中19.8w数据为阴性(良性病灶,标记为0),7.8w数据呈现为阳性(恶性病灶,标记为1)。图像的数据量超过10w级别,整体数据量较大,在运行时需要注意内存管理,如果可能尽量提供GPU作为运算资源。不过,这些数据来源于仅仅279位患者,患者整体数量较小,因此可能存在少数患者的病理信息对全局影响很大的情况。由于涉及到患者隐私及医疗伦理道德问题,医疗数据在现实当中是比较难以获取且公开的,因此基于医疗数据训练神经网络时必须时刻注意过拟合情况。
依据患者数量以及较高的过拟合风险,最佳方案是使用交叉验证,然而又考虑到较大的数据总量,在训练过程中多次执行交叉验证并不现实,因此综合来看我们需要将数据分割成训练、测试和验证集。我们首先在测试数据上寻找较好的分数,然后在验证集上进行验证,进一步验证模型的泛化能力。
现在,我们将全部图像导入,进行可视化及结构查看。考虑到导入目录的复杂程度(先进入患者ID、再进入标签类别、在读取具体文件),我们最好将每张图片的具体目录和标签都统计成表,再做后续打算。先来建立DataFrame:
data = pd.DataFrame(index=np.arange(0, total_patches)
, columns=["patient_id", "path", "target"]
)
idx = 0
for patient_id in patients:
for label in [0,1]:
class_path = os.path.join(PATH,patient_id,str(label))
class_patches = os.listdir(class_path)
for patch in class_patches:
data.loc[idx,"path"] = os.path.join(class_path,patch)
data.loc[idx,"target"] = label
data.loc[idx,"patient_id"] = patient_id
idx += 1
idx
data.shape
data.head()
data.tail()
data["path"][0]
(data.isnull()).sum()
现在,我们可以随机选取图像进行可视化、并且检查图像的结构了。对于方片数据,我们可以直接实现5行10列的排列:
selection = np.random.choice(data.index.values,size=50,replace=False)
selection
for n in range(5):
for m in range(10):
print(m + 10*n)
"""
for n in range(a):
for m in range(b):
m + b*n
range(0,a*b)
"""
fig, axs = plt.subplots(5,10,figsize=(20,10))
for n in range(5):
for m in range(10):
idx = selection[m+10*n]
image = cv2.imread(data.loc[idx,"path"])
if image is not None:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
print(image.shape)
else:
print("No Image")
axs[n,m].imshow(image)
axs[n,m].grid(False)
axs[n,m].axis("off")
首先观察图像的结构,所有的图像都是50x50的3通道图像。一般切片图像的结构都较为规整,原始图像的结构会更加复杂。如果发现不同的图像呈现不同的尺寸,则需要进行规整尺寸的预处理,反之则可以选择不处理。然而,考虑到医学影像的处理难度,以及各类经典神经网络常见的图像输入尺寸,我们优先考虑将50x50的尺寸放大为227x227,并使用深度较深的网络进行训练。
从可视化的结果来看,图像颜色深浅、线条粗细、轮廓等细节是千变万化的,在不具有专业知识的情况下,肉眼难以判断出具体病灶的情况。我们可以尝试单独可视化恶性区域和良性区域,看看能否发现一些区别:
pos_selection = np.random.choice(data[data.target==1].index.values, size=50, replace=False)
neg_selection = np.random.choice(data[data.target==0].index.values, size=50, replace=False)
fig, axs = plt.subplots(5,10,figsize=(20,10))
for n in range(5):
for m in range(10):
idx = pos_selection[m + 10*n]
image = cv2.imread(data.loc[idx, "path"])
if image is not None:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
axs[n,m].imshow(image)
axs[n,m].grid(False)
axs[n,m].axis("off")
fig, axs = plt.subplots(5,10,figsize=(20,10))
for n in range(5):
for m in range(10):
idx = neg_selection[m + 10*n]
image = cv2.imread(data.loc[idx, "path"])
if image is not None:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
axs[n,m].imshow(image)
axs[n,m].grid(False)
axs[n,m].axis("off")
不难发现,良性样本整体呈现的颜色比恶性样本要浅,色彩饱和度更高、同时组织结构看起来也相对简单一些。恶性样本中会存在粗糙、密集且颜色较深的结构。当然,部分位于恶性病变边缘的方片可能混杂在两种样本当中,使得良性样本中也会出现一些深色的方片,而恶性样本中也会出现一切浅色的方片。
data.head()
data["target"].value_counts()
data["target"].value_counts()[1]/data["target"].shape[0]
在许多人看来,少数类比例较少(恶性比例较少)可能意味着患者的病情并不严重,但事实上并非如此。
首先,在肿瘤治疗过程中,如果能够只切除肿瘤、保留器官,则一定不会将器官整个切除。因此,如果整个病理图像上只有少许组织和器官(即一个患者文件夹当中的图像数量较少),那被切除的可能是肿瘤本身,恶性切片的覆盖率可能达到90%甚至100%。出现这种情况往往说明患者的病情较轻,可以通过清除肿瘤组织和肿瘤细胞来进行治疗。
相反,如果一个患者的病理图像尺寸越大(即一个患者文件夹当中的图像数量较多),则说明被切除的器官越大,通常意味着病情已经较为严重。例如,以肿瘤为核心,肿瘤细胞已经扩散到整个器官,必须切除整个器官,或者,病变的亚型恶性较高、发展较快,为降低转移风险而主动切除整个器官。这类患者恶性切片的比例可能较低,但是病变程度更加严重。因此我们的少数类比例反映出来的现实是:提供这批真实影像的患者中可能包含许多病情较为严重、需要切除整个器官的患者。
当然,在手术和病情的复杂具体情况下(如患者年龄较大、医生的流派较为保守、或患者本人要求切除整个器官、患者本人不再需要使用该器官),并不能排除有患者在病情较轻、且肿瘤较小的情况下依然切除了大部分组织甚至整个器官。同时,也不排除某些患者体型较大、身材魁梧,因此器官比一般器官更大的情况。这些患者一般属于少数人,因此我们可以不必考虑他们的情况。
data.columns
data.head()
data.groupby("patient_id")["target"].count().describe()
根据统计结果,这批患者的病理图像最小值接近0、最大值接近2400,均值为994,表现相对均匀。这说明大部分患者还是只切除了部分组织,但只切除少许肿瘤组织的患者很少。
table = data.pivot_table(index="patient_id",columns="target"
,aggfunc="count")
table.head()
table.columns = ["0","1"]
table.index = patients
table.head()
table["wholepicture"] = table["0"] + table["1"]
table["positive_ratio"] = table["1"]/table["wholepicture"]
pd.set_option('display.max_rows',None)
table.sort_values("positive_ratio",ascending=False).head(10)
不难发现,恶性切片占比较高的前10名患者中只有3人的病理图像包含超过1000个切片,说明大部分恶性切片占比较高的患者都是病症较轻、可以直接切除肿瘤组织的患者。
table.sort_values("positive_ratio",ascending=True).head(10)
相对的,恶性切片占比最低的前10名患者的病理图像尺寸都较大,只有1人的病理图像包含1000个以下切片,说明大部分恶性切片占比较低的患者反而是重症患者、需要切除整个器官。当然,如果患者的整体病理图像尺寸较大、且恶性切片占比也较大,则说明该患者的病情最为严重。
观察我们每一个切片的文件名,不难发现我们有该切片位于原始病理图像上的坐标点位置,只要我们找出每个切片所对应的坐标点位置,就可以复原完整的病理图像上、恶性组织所占的部分。
data.loc[0,"path"]
data.head()
data.loc[10,"path"]
data["x"] = data["path"].apply(lambda x: int(x.split("_")[-3][1:]))
data["y"] = data["path"].apply(lambda x: int(x.split("_")[-2][1:]))
data.head()
def visualize_single_patient(patient_id):
"""绘制单个患者的病理图像及恶性占比"""
singlepatient = data[data["patient_id"] == patient_id]
sns.set()
plt.scatter(singlepatient["x"],singlepatient["y"],c=singlepatient["target"],cmap="coolwarm",s=10);
plt.grid(linewidth=1,color="white")
visualize_single_patient("10305")
visualize_single_patient("14209")
visualize_single_patient("9077")
for n in range(5):
for m in range(3):
print(m+3*n)
np.random.shuffle(data["patient_id"].unique())
fig, axs = plt.subplots(5,3,figsize=(20, 27))
patient_ids = data["patient_id"].unique()
np.random.shuffle(patient_ids)
for n in range(5):
for m in range(3):
patient_id = patient_ids[m+3*n]
singlepatient = data[data["patient_id"] == patient_id]
axs[n,m].scatter(singlepatient["x"],singlepatient["y"],c=singlepatient["target"],cmap="coolwarm",s=20)
axs[n,m].set_title("patient " + patient_id + " " + str(round(table.loc[patient_id,"positive_ratio"],3)))
保留恶性组织为核心,非恶性组织选择性保留
到这里,我们已经理解了当前数据集的基本结构、图像形态以及相关的部分专业知识。接下来,我们就可以对数据集进行预处理和建模了。从下一节开始我们将讲解医疗图像的数据预处理。
|