医学图像处理
- opencv批量分片高像素图像
- 病理图像数据增强
- 提升样本多样性
- 基于 imgaug、skimage 实现色彩增强
- 降低样本多样性
- 基于 DCGAN、TransposeConv 完成染色标准化
- 精细化提取特征
- 自动生成数据标注
- 分类场景下的医学图像分析
- 分割场景下的医学图像分析
- 检测场景下的医学图像分析
opencv批量分片高像素图像
医学图像通常是大像素(1920x1080)、超大像素(4096x2160)。
深度学习输入数据尺寸通常是 640x640、32x32。
所以我们会切分医学图像, 变成小像素片, 并对每一个方片识别或预测。
因为没有病理图,只能用相似的星辰图代替:
- 星辰和病灶细胞一样,可能分布在图像各个位置,也可以集中在图像上的某个区域
- 而且都非常小,可能不到图的1%
方片尺寸最小是1x1
, 一般我们用50x50
。
怎么实现这种分割呢?
- 选定截取区域
- 截取保存
# 截取图像[高的起点:高的终点,宽的起点:宽的终点],并保存
cv2.imwrite(os.path.join(path, "1.jpg"), imgcopy[0:1200,0:1200]
每个方片尺寸为 50*50,左上角第一个被切分的方片索引为 imgcopy[:50, :50],紧接着左数第二个方片的索引为 imgcopy[:50,50:100],第三个方片索引为 imgcopy[:50,100:150],第一行所有方片被表示为 ingcopy[:50, x:x+50]。
只要在宽度上循环,每次让宽的起点增加50,宽的终点增加50,就可以做第一行的截取。
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy # 每次分割前,获取完整的原始图像
imgheight = imgcopy.shape[0] # 获取高度
imgwidth = imgcopy.shape[1] # 获取宽度
patch_height = 50 # 方片尺寸50*50
patch_weight = 50
for y in range(0, imgheight, patch_height): # y 是高的起点
for x in range(0, imgwidth, patch_weight): # x 是宽的起点
if patch_height > imgheight or patch_weight > imgwidth: # 边界判断,如果图片小于截取尺寸取消
break
y_ = y + patch_height
x_ = x + patch_weight
if imgheight >= y_ and imgwidth >= x_: # 如果图片已经被截取到连50都到不得了,这部分就舍去,不影响
patch = imgcopy[y:y_, x:x_]
cv2.imwrite(os.path.join(path, "x"+str(x)+"_"+str(x_)+"y"+str(y)+"_"+str(y_)+".jpg"), patch)
# 保存截取图像
cv2.rectangle(imgcopy, (x,y), (x_,y_),(255,255,255),2) # 把刚刚截取区域在原图上用白色矩形圈出来
分割后。读取大批量文件:
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("分割文件夹路径")
完整代码:
import cv2
import os
import matplotlib.pyplot as plt
path = r"文件夹路径"
img = cv2.imread(os.path.join(path, "图片名字.jpg"))
if img is None:
print("opencv读取图像时,没有成功也不会报错")
else:
print("读取图像成功")
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy # 将BGR颜色空间的图像转换为RGB颜色空间,并创建一个副本以供后续使用。每次分割前,获取完整的原始图像
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_) # 绘制长方体的4角的坐标
, (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
对任意图像进行批量分割处理了。
病理图像数据增强
不同类型的图像需要使用不同的预处理和增强方式,主要是因为不同图像成像过程存在的干扰因素是大不相同的。
对高像素图像进行分片解释医疗特有的预处理方式。读这个领域数据处理和数据增强的论文,找出有用的方法。
因为恶性病灶原色往往比良性病灶越深。所以色彩差异是神经网络需要学习的一大关键点。
但是模态太多,不同实验室、不同专业人员、不同批次染色剂、不同扫描仪、不同玻片原材料、不同组织对颜色的响应程度等因素而引起的颜色差异,影响学习效果
病理图像的预处理方式、增强:排除染色操作干扰,提升模型泛化能力。
色彩的本质是明度,灰度图也有明度差异,彩色图像的丰富信息要最大程度保留。
病理数据增强:
- 提升样本多样性:训练集覆盖尽可能多的色彩,提升训练集多样性
- 降低样本多样性:把所有图像(训练集和测试集)进行色彩标准化,让所有图像尽量在色彩上一致,避免不同染色操作、不同模态的影响
- 精细化提取特征:病灶极其微小(如肺癌结点大小1mm见方),需要超精细化特征提取,而非多尺度特征,传统去噪方法很容易把关键特征去除
- 自然光成像,像素范围0-255 -> 反射成像几千,x光就是小数了
- 样本比例极度不平衡,病灶极小块才是正样本,把正样本抽出来,无限扩充
- 自动生成数据标注,用一张已标注图,生成其他所有未标注图的数据和标签
提升样本多样性
提升样本多样性:
- 随机仿射变换:对图像随机移动、随机旋转、随机拉伸、随机透视等
- 随机噪声:高斯噪声、脉冲噪声、散粒噪声等
- 随机线性变换:对像素随机亮度、随机对比度、随机色相、随机饱和度、随机色温
- 改变色彩空间:RGB-HSV、基于反卷积的RGB-H&E、RGB-HED等
- 针对单通道的随机线性变换(随机加减乘除、随机调整亮度和对比度)
- 色彩均衡、自动对比度、随机翻转、随机剪裁或填充
针对单通道的随机线性变换,是公认效果很好的手段,具体可以参考以下的论文:
- 为H&E染色的组织病理学定制自动数据增强
- HE染色增强改进了用于组织病理学有丝分裂检测的卷积网络的泛化
基于 imgaug、skimage 实现色彩增强
import imgaug # 19个增强板块:https://imgaug.readthedocs.io/en/latest/source/api.html
import skimage
import imgaug as ia
from imgaug import augmenters as iaa
from skimage import io
imgs = io.imread_collection("图片路径\\*.jpg") # 读取所有.jpg图像
aug = iaa.Affine(scale={"x":(0.6, 1.0)}, "y":(0.8, 1.2)}, # 在 x、y 轴上按比例缩小
translate_percent = {"x":(-0.05, 0.2), "y":(-0.2, 0.2)} # 在 x、y 轴上、下移动
rotate = (-25, 25) # 顺时针、逆时针旋转的角度范围
shear = -20 # 移动图像的一轴,可以将正方形变为菱形,梯形
)
imgs_aug = aug(images = imgs) # 输入图像,仿射变换
io.imshow_collection(imgs[:12]) # 显示原图效果
io.imshow_collection(imgs_aug[:12]) # 显示变化效果
aug = iaa.AdditiveGaussianNoise(loc=10, # 高斯分布的均值,均值越大噪声越多
scale = (0.1*255, 0.5*255), # 方差范围,方差越大噪声越多
per_channel = True # 为每个通道单独添加噪声,否则全部通道共享噪声 )
imgs_aug = aug(images = imgs) # 输入图像,随机抽取像素点为噪声
aug = iaa.MultiplyAndAddToBrightness(mul = (0.5, 2), add=(-100, 100)) # 随机乘、加亮度 ax+b
imgs_aug = aug(images = imgs)
aug = iaa.MultiplyHueAndSaturation(mul_hue(0.5, 2), mul_saturation=(2,3)) # 随机加倍色相(图像颜色)、饱和度(鲜艳程度)
imgs_aug = aug(images = imgs)
aug = iaa.pillike.EnhanceContrast(factor=(0, 3)) # 增强图像的对比度
imgs_aug = aug(images = imgs)
img = io.imread(train.iloc[0,1])
aug = iaa.ChangeColorTemperature(kelvin=(1000,11000)).augment_image # 随机色温,按照开尔文温度对颜色进行调整,区间在[1000, 40000]
img_aug = aug(img)
plt.figure(figsize=(1.5, 1.5))
io.imshow(img_aug)
aug = iaa.pillike.Equalize() # 色彩均衡
aug = iaa.pillike.Autocontrast(cutoff=5) # 自动对比值
aug = iaa.Fliplr(p=0.5) # 随机对 50% 的图像进行翻转
aug = iaa.CropAndPad(percent=(0.1, 0.3)) # 随机裁剪或填充
aug = iaa.ChangeColorspace(to_colorspace="HSV") # 改变色彩空间,RGB -> HSV
方案一:打包除色彩空间外的全部预处理方法:
seq1 = iaa.Sequential( [
iaa.Resize(256),
iaa.Fliplr(0.5),
iaa.Flipud(0.5),
iaa.CropAndPad(percent=(0.01, 0.02)),
iaa.MultiplyAndAddToBrightness(mul=(0.7, 1.2), add=(-10,10)),
iaa.MultiplyHueAndStauration(mul_hue=(0.8, 1.2), mul_saturation=(0.8, 1.2),
iaa.pillike.EnhanceContrast(factor=(0.75, 1.25)),
iaa.Sometimes(0.5, iaa.AdditiveGaussianNoise(loc=1, scale=(0, 0.05*255), per_channel=0.5)),
iaa.Add((-20, 5)),
iaa.Multiply((0.8, 1.2), per_channel=0.2),
iaa.Affine(scale={"x":(0.9, 1.1), "y":(0.9, 1.1)},
translate_percent={"x":(-0.05, 0.05), "y":(-0.05, 0.05)},
rotate=(-10, 10),
shear=(-3, 3))
], random_order=True)
imgs_aug = seq1(images=imgs)
数据变换的操作序列:
'''根据输入的参数生成了用于数据变换的操作序列,并以字典的形式返回相应的变换操作序列。这些操作序列用于将输入的图像数据进行增强、转换和归一化等处理,以便在训练和测试中使用'''
def alltransform( key = "train", plot = "False" )
train_sequence = [seq1.augment_image, transforms.ToPILImage()]
# 定义了在训练集上进行数据增强的操作序列。其中 seq1.augment_image 是一种数据增强操作,transforms.ToPILImage() 是将增强后的图像转换为 PIL Image 对象。
test_val_sequence = [iaa.Resize(256).augment_image, transforms.ToPILImage()]
# 定义了在验证集和测试集上进行数据增强的操作序列。iaa.Resize(256).augment_image 是将图像调整为指定大小的数据增强操作。
if plot == False:
train_sequence.extend( [transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ) ] )
# 在训练集操作序列中添加了将图像转换为张量(transforms.ToTensor())和归一化操作(transforms.Normalize())。
test_val_sequence.extend([transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ) ] )
# 在验证集和测试集操作序列中添加了将图像转换为张量和归一化的操作。
data_transforms = {'train':transforms.Compose(train_sequence), 'test_val':transforms.Compose(test_val_sequence))
# 创建了一个字典 data_transforms,其中包含了训练集和验证集/测试集对应的变换操作序列。
return data_transforms[key]
# 根据给定的 key 返回相应的数据变换操作序列。
自定义数据集:
''' 定义了一个自定义数据集类 CustomDataset,其中的 __init__ 函数用于初始化数据集,__len__ 函数用于返回数据集长度,__getitem__ 函数用于获取指定索引的样本。同时,通过 CustomDataset 类的实例化,可以加载训练数据集并应用指定的变换操作 '''
class CustomDataset( Dataset ): # 定义了一个自定义数据集类 CustomDataset,继承自 torch.utils.data.Dataset 类。
def __init__( self, df, transform=None): # 定义了类的初始化函数,接受参数 df 和 transform。df 是一个包含数据路径和标签的数据框,transform 是一个可选的数据变换操作。
super().__init__() # 调用父类的初始化函数。
self.path_label = df # 将传入的数据框赋值给类属性 path_label,用于存储数据路径和标签。
self.transform = transform # 将传入的变换操作赋值给类属性 transform,用于数据变换。
def __len__(self): # 定义了 __len__ 函数,用于返回数据集的长度。
return self.path_label.shape[0] # 返回数据集中样本的数量。
def __getitem__(self): # 定义了 __getitem__ 函数,用于获取指定索引 idx 的样本。
if torch.is_tensor( idx ) # 如果索引 idx 是一个张量,则将其转换为列表。
idx = idx.tolist() # 从数据框中获取指定索引 idx 对应的患者ID。
patient_id = self.path_label["patient_id"].values[idx] # 从数据框中获取指定索引 idx 对应的患者ID。
image_path = self.path_label["path"].values[idx] # 从数据框中获取指定索引 idx 对应的图像路径。
if self.transform: # 如果指定了变换操作,则对图像应用变换。
image = self.transform(image)
sample = {"patch":image, # 创建一个样本字典,包含图像数据、标签和患者ID。
"label":label,
"patient":patient_id}
return sample # 返回样本字典
train_ = CustomDataset( train, transform = alltransform(key="train", plot=True)) # 创建一个自定义数据集实例 train_,传入训练数据框和变换操作。
方案二:将图像转换为 HED 空间后,对单一通道完成线性变换,再转换 RGB
RGB 转 HED 步骤要多一点:
# 改变色彩空间,RGB -> HED
from skimage.color import rgb2hed
from skimage.exposure import rescale_intensity
img = io.imread(train.iloc[0,1])
img_aug = rgb2hed(img)
for channel in [0, 1, 2]:
img_aug[:, :, channel] = rescale_intensity(img_aug[:, :, channel], out_range=(0, 1))
from matplotlib.colors import LinearSegmentedColormap
# 分解为三通道图
cmap_hema = LinearSegmentedColormap.from_list('mycmap', ['mediumvioletred', 'white']) # 突出细胞核
cmap_dab = LinearSegmentedColormap.from_list('mycmap', ['white', 'brown']) # 和原图最接近
cmap_hema = LinearSegmentedColormap.from_list('mycmap', ['crimson', 'white']) # 突出形状、纹理
# 根据论文《H&E染色增强改进了用于组织病理学有丝分裂检测的卷积网络的泛化》
# 将图像转为 HED 通道后,分别在通道随机加减乘除后,再将色彩空间换回 RGB,可提高模型泛化能力
seq_all = iaa.Sequential( # 定义了一个包含多个增强操作的序列 seq_all,使用iaa.Sequential()函数创建。该序列中的增强操作按顺序应用于图像。其中包括调整大小、水平翻转、垂直翻转、亮度调整、色调和饱和度调整以及仿射变换等。最后的 .augment_image 表示将这个序列应用于图像。
[iaa.Resize(256), iaa.Fliplr(0.5), iaa.Filpud(0.5),
iaa.MultiplyAndAddToBrightness(mul=(0.8, 1.2), add=(-10, 10)),
iaa.MultiplyHueAndSaturation(mul_hue=(0.9, 1.2), mul_saturation=(0.8, 0.9)),
iaa.Affine( scale={'x':(0.9, 1.1), 'y':(0.9, 1.1)}, translate_percent={'x':(-0.05, 0.05), 'y':(-0.05, 0.05)}, rotate=(-10, 10), shear=(-3, 3) ) ] ).augment_image
def seq_channel( img, channel=0 ): # 定义了一个名为 seq_channel 的函数,用于对图像的指定通道进行增强。
seq = iaa.WithChannels([channel]), [ iaa.pillike.Enhannels([factor=(0.6, 0.8)]),
iaa.Sometimes(0.5, iaa.AdditiveGaussianNoise(loc=1, scale=(0, 0.001 * 255))),
iaa.Add(-10, 10),
iaa.Nultiply(0.5),
iaa.Sharpen(alpha=(0.1, 1.0), lightness=(0.75, 1.5))
]).augment_image
return seq(img)
image = io.imread(train.iloc[0, 1])
img = seq_all(image) # 先对所有通道进行处理
img_hed = skimage.color.rgb2hed(img)
for channel in [0, 1, 2]:
img_hed[:, :, channel] = rescale_intensity(img_hed[:, :, channel], out_range=(0, 255))
img_hed = img_hed.astype("uint8")
for channel in [0, 1, 2]: # 对三通道循环处理
img_aug = seq_channel(img_hed, channel)
img_rgb = skimage.color.hed2rgb(img_hub) # 转回rgb
for channel in [0, 1, 2]: # 归一化
img_rgb[:, :, channel] = rescale_intensity(img_rgb[:, :, channel], out_range=(0, 255))
img_rgb = img_rgb.astype("uint8")
io.imshow(img_rgb)
打包以上的过程,放入 transform 里,Pytorch 就可以调用。
class SingleChannelTrabsform(object):
def __int__(self):
seq_all = iaa.Sequential(
[iaa.Resize(256), iaa.Fliplr(0.5), iaa.Filpud(0.5),
iaa.MultiplyAndAddToBrightness(mul=(0.8, 1.2), add=(-10, 10)),
iaa.MultiplyHueAndSaturation(mul_hue=(0.9, 1.2), mul_saturation=(0.8, 0.9)),
iaa.Affine( scale={'x':(0.9, 1.1), 'y':(0.9, 1.1)}, translate_percent={'x':(-0.05, 0.05), 'y':(-0.05, 0.05)}, rotate=(-10, 10), shear=(-3, 3) ) ] ).augment_image
def seq_channel( img, channel=0 ): # 定义了一个名为 seq_channel 的函数,用于对图像的指定通道进行增强。
seq = iaa.WithChannels([channel]), [ iaa.pillike.Enhannels([factor=(0.6, 0.8)]),
iaa.Sometimes(0.5, iaa.AdditiveGaussianNoise(loc=1, scale=(0, 0.001 * 255))),
iaa.Add(-10, 10),
iaa.Nultiply(0.5),
iaa.Sharpen(alpha=(0.1, 1.0), lightness=(0.75, 1.5))
]).augment_image
return seq(img)
self.seq_channel = seq_channel
def __call__(self, sample):
image = sample
img = seq_all(image)
img_hed = skimage.color.rgb2hed(img)
for channel in [0, 1, 2]:
img_hed[:, :, channel] = rescale_intensity(img_hed[:, :, channel], out_range=(0, 255))
img_hed = img_hed.astype("uint8")
for channel in [0, 1, 2]:
img_aug = seq_channel(img_hed, channel)
img_rgb = skimage.color.hed2rgb(img.aug)
for channel in [0, 1, 2]:
img_rgb[:, :, channel] = rescale_intensity(img_rgb[:, :, channel], out_range=(0, 255))
img_rgb = img_rgb.astype("uint8")
return img_rgb
经过比对,方案一实现提升样本多样性方法效果更好。
降低样本多样性
传统方法是,先有一个标准(寻找一个标准图像作为目标图像),再将其他所有图像按照目标图像进行标准化。
- 色彩匹配:将输入图像转换为 LAB 空间(L是亮度,A是从绿色到红色的分量,B是从蓝色到黄色的分量),并以通道为单位,将所有通道的值中心化至 0 均值(在所有像素上减去一个值让均值变为0),再缩放到方差为 1(再把所有像素除以一个数让方差为 1),然后重新标准化以目标图像统计信息。
- 染色分离:使用反卷积(如 Macenko、Khan染色分离)将数据集上的染色去除,在从目标图像中提取染色特征向量对去除染色的图像进行卷积,最终生成基于目标图像重新染色的图像。
这种方法的缺陷是,必须规定一张标准图像,如果该目标图像与其他数据差异太大,就会导致被标准化的图像被破坏。
现在流行用 GANs 完成色彩标准化,以及比 GAN 更简单的预处理方法。
基于 DCGAN、TransposeConv 完成染色标准化
生成对抗网络:借助真实数据生成一组假数据训练网络,输出一组以假乱真的数据
先把彩图灰度化,把灰度图输入生成器,生成彩色图像,用生成彩色图像和原来彩色图像进行判别,在对抗的过程中,让俩者越来越接近
参考论文:
- 使用 GAN 处理组织病理学图像的神经染色式迁移学习
GAN 是处理二维数据的线性层组成,无法处理图像数据,需要使用 DCGAN 处理医疗图像,完成染色标准化。
GAN 好处:生成以假乱真的隐私数据,如医疗数据涉及隐私,数量是很少的,我们可以用真实数据生成一组假数据,用这组接近真实数据的假数据去训练。
class Discriminator(nn.Module):
def __init__(self, in_features):
super.__init__()
self.disc = nn.Sequential(nn.Linear(in_features, 128),
nn.LeakyReLU(0.1),
nn.Linear(128, 1),
nn.Sigmoid())
def forward(self, data):
return self.disc(data)
class Generator(nn.Module):
def __init__(self, in_features, out_features):
super().__int__()
self.gen = nn.Sequential(nn.Linear(in_features, 256),
nn.LeakyReLU(0.1),
nn.Linear(256, out_features),
nn.Tanh())
def forward(self, z):
gz = self.gen(z)
return gz
batch_size = 32
lr = 3e -4
num_epochs = 50
realdata_dim = 28 * 28 * 1
z_dim = 64
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(0.5), (0.5)])
dataset = dest.MNIst(root = r"文件路径", transform = transform, download = True)
dataloader = DataLoader(dataset, batch_size = batch_size, shuffle = True)
fixed_noise = torch.randn((batch_size, z_dim)).to(device)
gen = Genaretor(in_features = z_dim, out_features = realdata_dim).to(device)
disc = Discriminator(in_features = realdata_dim).to(device)
criterion = nn.BCELoss(reduction="mean")