医学图像分割利器:U-Net网络详解及实战

news2024/11/17 16:39:10

1 U-Net网络介绍

1.1 U-Net由来

2015年U-Net的出现使得原先需要数千个带注释的数据才能进行训练的深度学习神经网络大大减少了训练所需要的数据量,并且其针对神经网络在图像分割上的应用开创了先河。当时神经网络在图像分类任务上已经有了较好的成果,但在很多视觉的任务中由于输出需要进行定位,也就是每个像素需要分配一个类标签,这导致成千上万的训练图像在生物医学任务中通常难以获得,从而急需要一个神经网络,它不需要那么多的数据来进行训练却依旧有较好的效果,这就导致了U-Net的诞生。

U-Net几乎是当前segmentation项目中应用最广的模型。Unet能从更少的训练图像中进行学习,当它在少于40张图的生物医学数据集上训练时,IOU值仍能达到92%。

论文地址:https://arxiv.org/pdf/1505.04597.pdf

代码地址:https://github.com/milesial/Pytorch-UNet

1.2 U-Net网络

典型的encoder-decoder结构

  • 左边是encoder,也就是提取特征和下采样的部分;右边decoder解码是一系列上采样,得到最终的一个分割图

  • 图中每个长条的矩形对应的都是一个特征层,箭头都是一种操作

  • 从输入开始看,输入是572x572的单通道的图片为例,首先进行一个卷积操作(步距为1,no padding)通过卷积层之后,高和宽都会减少,当时15年还没有BN。经过两个卷积层后进行下采样(max poooling),2x2,高和宽就会减半,channel不会变化,还是64。接着再进行两个3x3卷积层,(每次下采样后都会将channel进行翻倍,64-128)。右半部分:绿色的是上采样(其实是转置卷积,可回看视频,经过之后会将特征层的高和宽放大两倍,channel减半),1024-512,对应蓝色部分;灰色箭头是copy和crop,左边的高和宽是64x64,右边得到的是56x56大小的,无法进行直接拼接,所以对左边的特征层进行中心裁剪,和右边的蓝色部分进行拼接,拼接之后channel为1024。后面依次进行上采样。直到最后得到宽和高388x388,最后经过一个灰色的箭头卷积层,这个卷积核的个数和我们分类的类别个数是一致的,论文中只分割前景和背景。输出的结构就是388x388x2。(注意最后1x1的卷积的没有激活函数)

  • 注意得到的分割图和原图并不一样,网络并不是完全对称,主要是因为用了 valid 卷积。除了收缩路径和扩张路径,中间还有一条拼接路径(skip connection),将左边的特征图(需要裁剪crop)拼接到右边。

  • 反思:现在主流的实验方式并非严格按照论文中的方式去实现,而是在卷积层加上一个padding,即每次经过3x3卷积层,不会改变特征层的高和宽,并且会在卷积和ReLu之间加上一个BN(效果:在拼接的时候不要中心裁剪了,最后得到的高和宽和我们输入的高和宽保持一致)。

  • 针对特别大(高分辨率的)图片,一般每次只分割一个patch,相邻两个预测区域之间一般会有一个重叠的部分,称为overlap,能够更好的分割边界区域。

  • pixel-wise loss的方案:细胞与细胞之间的背景取,赋予大的权重,对于大片的背景区域,施加小的权重

1.3 U-Net网络为何在医学图像分割表现好

UNet最早发表在2015的MICCAI上,短短3年,引用量目前已经达到了4070,足以见得其影响力。而后成为大多做医疗影像语义分割任务的baseline,也启发了大量研究者去思考U型语义分割网络。而如今在自然影像理解方面,也有越来越多的语义分割和目标检测SOTA模型开始关注和使用U型结构,比如语义分割Discriminative Feature Network(DFN)(CVPR2018),目标检测Feature Pyramid Networks for Object Detection(FPN)(CVPR 2017)等。

医疗影像的特点:

  • 图像语义较为简单、结构较为固定。我们做脑的,就用脑CT和脑MRI,做胸片的只用胸片CT,做眼底的只用眼底OCT,都是一个固定的器官的成像,而不是全身的。由于器官本身结构固定和语义信息没有特别丰富,所以高级语义信息和低级特征都显得很重要(UNet的skip connection和U型结构就派上了用场)。

        脑出血. 在CT影像上,高密度的区域就大概率是一块出血,如下图红色框区域。

        眼底水肿。左图原图,右图标注(不同灰度值代表不同的水肿病变区域)。在OCT上,凸起或者凹陷的区域就大概率是一个水肿病变的区域。

  • 数据量少。医学影像的数据获取相对难一些,很多比赛只提供不到100例数据。所以我们设计的模型不宜多大,参数过多,很容易导致过拟合。原始U-Net的参数量在28M左右(上采样带转置卷积的UNet参数量在31M左右),而如果把channel数成倍缩小,模型可以更小。缩小两倍后,UNet参数量在7.75M。缩小四倍,可以把模型参数量缩小至2M以内,非常轻量。个人尝试过使用Deeplab v3+和DRN等自然图像语义分割的SOTA网络在自己的项目上,发现效果和UNet差不多,但是参数量会大很多。
  • 多模态。相比自然影像,医疗影像比较有趣和不同的一点是,医疗影像是具有多种模态的。以ISLES脑梗竞赛为例,其官方提供了CBF,MTT,CBV,TMAX,CTP等多种模态的数据。这就需要我们更好的设计网络去提取不同模态的特征feature。
  • 可解释性重要。由于医疗影像最终是辅助医生的临床诊断,所以网络告诉医生一个3D的CT有没有病是远远不够的,医生还要进一步的想知道,病灶在哪一层,在哪一层的哪个位置,分割出来了吗,能求体积嘛?同时对于网络给出的分类和分割等结果,医生还想知道为什么,所以一些神经网络可解释性的trick就有用处了,比较常用的就是画activation map。看网络的哪些区域被激活了,如下图。

2 U-Net部署及实战

2.1 conda环境准备

conda环境准备详见:annoconda

2.2 运行环境构建

git clone https://github.com/milesial/Pytorch-UNet
cd Pytorch-UNet/

conda create -n unet python=3.8
conda activate unet

pip install torchvision==0.15.1
pip install -r requirements.txt

2.3 预训练模型下载

下载地址:https://github.com/milesial/Pytorch-UNet/releases/tag/v3.0

net = torch.hub.load('milesial/Pytorch-UNet', 'unet_carvana', pretrained=True, scale=0.5)

2.4 训练数据下载

bash scripts/download_data.sh

2.5 模型训练

python train.py --amp

2.6 模型预测

python predict.py -i image.jpg -o output.jpg

3 自己动手实现U-Net网络

import torch.nn as nn
import torch


# 编码器(论文中称之为收缩路径)的基本单元
def contracting_block(in_channels, out_channels):
    block = torch.nn.Sequential(
        # 这里的卷积操作没有使用padding,所以每次卷积后图像的尺寸都会减少2个像素大小
        nn.Conv2d(kernel_size=(3, 3), in_channels=in_channels, out_channels=out_channels),
        nn.BatchNorm2d(out_channels),
        nn.ReLU(),
        nn.Conv2d(kernel_size=(3, 3), in_channels=out_channels, out_channels=out_channels),
        nn.BatchNorm2d(out_channels),
        nn.ReLU()
    )
    return block


# 解码器(论文中称之为扩张路径)的基本单元
class expansive_block(nn.Module):
    def __init__(self, in_channels, mid_channels, out_channels):
        super(expansive_block, self).__init__()

        # 每进行一次反卷积,通道数减半,尺寸扩大2倍
        self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=(3, 3), stride=2, padding=1,
                                     output_padding=1)
        self.block = nn.Sequential(
            # 这里的卷积操作没有使用padding,所以每次卷积后图像的尺寸都会减少2个像素大小
            nn.Conv2d(kernel_size=(3, 3), in_channels=in_channels, out_channels=mid_channels),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(),
            nn.Conv2d(kernel_size=(3, 3), in_channels=mid_channels, out_channels=out_channels),
            nn.BatchNorm2d(out_channels),
            nn.ReLU()
        )

    def forward(self, e, d):
        d = self.up(d)
        # concat
        # e是来自编码器部分的特征图,d是来自解码器部分的特征图,它们的形状都是[B,C,H,W]
        diffY = e.size()[2] - d.size()[2]
        diffX = e.size()[3] - d.size()[3]
        # 裁剪时,先计算e与d在高和宽方向的差距diffY和diffX,然后对e高方向进行裁剪,具体方法是两边分别裁剪diffY的一半,
        # 最后对e宽方向进行裁剪,具体方法是两边分别裁剪diffX的一半,
        # 具体的裁剪过程见下图一
        e = e[:, :, diffY // 2:e.size()[2] - diffY // 2, diffX // 2:e.size()[3] - diffX // 2]
        cat = torch.cat([e, d], dim=1)  # 在特征通道上进行拼接
        out = self.block(cat)
        return out


# 最后的输出卷积层
def final_block(in_channels, out_channels):
    block = nn.Conv2d(kernel_size=(1, 1), in_channels=in_channels, out_channels=out_channels)
    return block


class UNet(nn.Module):

    def __init__(self, in_channel, out_channel):
        super(UNet, self).__init__()

        # 编码器 (Encode)
        self.conv_encode1 = contracting_block(in_channels=in_channel, out_channels=64)
        self.conv_pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv_encode2 = contracting_block(in_channels=64, out_channels=128)
        self.conv_pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv_encode3 = contracting_block(in_channels=128, out_channels=256)
        self.conv_pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv_encode4 = contracting_block(in_channels=256, out_channels=512)
        self.conv_pool4 = nn.MaxPool2d(kernel_size=2, stride=2)

        # 编码器与解码器之间的过渡部分(Bottleneck)
        self.bottleneck = nn.Sequential(
            nn.Conv2d(kernel_size=(3, 3), in_channels=512, out_channels=1024),
            nn.BatchNorm2d(1024),
            nn.ReLU(),
            nn.Conv2d(kernel_size=(3, 3), in_channels=1024, out_channels=1024),
            nn.BatchNorm2d(1024),
            nn.ReLU()
        )

        # 解码器(Decode)
        self.conv_decode4 = expansive_block(1024, 512, 512)
        self.conv_decode3 = expansive_block(512, 256, 256)
        self.conv_decode2 = expansive_block(256, 128, 128)
        self.conv_decode1 = expansive_block(128, 64, 64)

        self.final_layer = final_block(64, out_channel)

    def forward(self, x):
        # Encode
        encode_block1 = self.conv_encode1(x)
        encode_pool1 = self.conv_pool1(encode_block1)
        encode_block2 = self.conv_encode2(encode_pool1)
        encode_pool2 = self.conv_pool2(encode_block2)
        encode_block3 = self.conv_encode3(encode_pool2)
        encode_pool3 = self.conv_pool3(encode_block3)
        encode_block4 = self.conv_encode4(encode_pool3)
        encode_pool4 = self.conv_pool4(encode_block4)

        # Bottleneck
        bottleneck = self.bottleneck(encode_pool4)

        # Decode
        decode_block4 = self.conv_decode4(encode_block4, bottleneck)
        decode_block3 = self.conv_decode3(encode_block3, decode_block4)
        decode_block2 = self.conv_decode2(encode_block2, decode_block3)
        decode_block1 = self.conv_decode1(encode_block1, decode_block2)

        final_layer = self.final_layer(decode_block1)
        return final_layer


if __name__ == '__main__':
    image = torch.rand((1, 3, 572, 572))
    unet = UNet(in_channel=3, out_channel=2)
    mask = unet(image)
    print(mask.shape)

    # 输出结果:
    torch.Size([1, 2, 388, 388])

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1129770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

出现了一个全新的编程语言——Mojo

最近,编程领域又一个黑马忽然冲进了开发者们的视野并正式开放下载。 Mojo 的简介 Mojo是一种新推出的编程语言,它将Python的简单性与Rust的速度和内存安全性结合在一起。 它处于开发的早期阶段,为用户提供了一个在线游乐场来探索其功能。 …

【JAVA学习笔记】 30 - Object类详解(equal,hashCode,toString)

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter08/src/com/yinhai/object_ 一、equal方法 和equal的对比 1.既可以判断基本类型,又可以判断引用类型,返回boolean值 2. 如果判断基本类型,判断的值是否相…

Vue 项目进行 SEO 优化

SSR 服务器渲染 服务端渲染, 在服务端 html 页面节点, 已经解析创建完了, 浏览器直接拿到的是解析完成的页面解构 关于服务器渲染:Vue 官网介绍 ,对 Vue 版本有要求,对服务器也有一定要求,需要支持 nodejs 环境。 优势: 更好的 …

Mysql视图特性用户管理

目录 一、视图基本使用 二、用户管理 2.1 用户 ①用户信息 ②创建用户 tips:(解决无法创建用户) ③删除用户 ④修改用户密码 2.2数据库的权限 ①给用户授权 ②回收权限 视图:视图是一种虚拟表。视图是基于一个或多个基础表中的数据所创建的一个查询结果…

ATA-8061射频功率放大器应用领域介绍

ATA-8061射频功率放大器简介 ATA-8061是一款射频功率放大器。其P1dB输出功率500W,饱和输出功率1000W。增益数控可调,一键保存设置,提供了方便简洁的操作选择,可与主流的信号发生器配套使用,实现射频信号的完美放大。宽…

[common c/c++] 使用 posix 共享内存 和 mmap 实现 inter process function call

正文: mmap 可以映射某个文件的某块内存区域,因此可以通过 mmap 和 共享内存的方式将两个不同进程内的函数的所有二进制码映射到共享内存里,以实现跨进程的函数调用。 实际上,linux 动态库的动态链接正是通过mmap 把动态库文件 …

基于ARM+FPGA+AD的多通道精密数据采集仪方案

XM 系列具备了数据采集仪应具备的“操作简单、便于携带、满足各种测量需求”等功能的产品。具有超小、超轻量的手掌大小尺寸,支持8 种测量模块,还可进行最多576 Ch的多通道测量。另外,支持省配线系统,可大幅削减配线工时。使用时不…

MySQL的基础(一)

文章目录 SQLSQL的语法特点主要包括以下几点:一、 SQL - DDL -- 数据定义语言1.1 数据库操作1.1 显示现有的数据库1.2 创建数据库1.3 删除数据库1.4 使用 1.2 数据表操作1.2.1 表查询1.2.2 表创建1.2.3 修改表 1.2.4 小结 二、SQL - DML -- 数据操作语言2.1 添加数据…

Kotlin基础——函数、变量、字符串模板、类

函数、变量、字符串模板、类 函数变量字符串模板类 函数 函数组成为 fun 函数名(参数名: 参数类型, …): 返回值{} fun max(a: Int, b: Int): Int {return if (a > b) a else b }上面称为代码块函数体,当函数体由单个表达式构成时,可简化为表达式函…

Java New对象分配内存流程

一、流程图 二、流程介绍 1、进行逃逸分析,判断是否能够分配到栈上: Y: 如果能分配到栈上,则进行分配。等方法出栈时,对象内存销毁,可有效减少GC的次数。 N:无法分配到栈上,则判断是…

阿里7年经验之谈 —— 如何实现前端项目的自动化测试?

这其实就是我们常说的“UI自动化测试”,针对这个问题,我先告知答题思路如下: 1、什么是UI自动化?有什么优势? 2、UI自动化实践中会遇到什么难题? 3、如何解决难题,将UI落实到实践中?…

Go学习第七章——数组arr,切片slice和映射map

Go数组arr,切片slice和映射map 1 数组1.1 快速入门1.2 数组的内存布局1.3 四种初始化数组的方式1.4 数组的遍历1.5 注意事项以及分析1.6 数组反转1.7 二维数组 2 切片2.1 快速入门2.2 内存解析2.3 切片的使用和遍历2.4 注意事项和细节说明2.5 string和slice关系2.6 …

基于大数据的社交平台数据爬虫舆情分析可视化系统 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果**实现功能****可视化统计****web模块界面展示**3 LDA模型 4 情感分析方法**预处理**特征提取特征选择分类器选择实验 5 部分核心代码6 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 基于大数据…

充气膜结构的内压设计应考虑哪几种状态?

内压是充气膜结构独有的设计参数,也是结构形成刚度维持稳定的核心因素。同时,作为一种长期荷载保持结构在外荷载作用下具有的合理刚度,调整结构形态,以免产生过大变形、膜材失效褶皱等状况。结构的内压随不同气候条件进行调整&…

【MySQL-->数据操作】

文章目录 前言一、insert1.单行插入2.多行插入3.插入更新/替换 二、select1.全列查询2.指定列插入3.列别名4. 表达式计算5.去重6.where条件查询7.排序8.limit分页显示 三、update四、delete五、插入查询结果六、聚合函数六、聚合分组1.格式2.where和having的区别 前言 一、inse…

文心一言 VS 讯飞星火 VS chatgpt (120)-- 算法导论10.3 5题

五、用go语言,设 L 是一个长度为 n 的双向链表,存储于长度为 m 的数组key、prev 和next 中。假设这些数组由维护双链自由表 F的两个过程 ALLOCATE-OBJECT 和 FREE-OBJECT 进行管理。又假设 m 个元素中,恰有 n 个元素在链表 L 上,m…

BUUCTF 乌镇峰会种图 1

BUUCTF:https://buuoj.cn/challenges 题目描述: 乌镇互联网大会召开了,各国巨头汇聚一堂,他们的照片里隐藏着什么信息呢?(答案格式:flag{答案},只需提交答案&#xff0…

Linux NFS的整体架构与核心代码解析

前面文章我们从应用层面对NFS进行了介绍,接下来的文章我们将进入实现层面。本文首先从整体上对Linux的NFS软件架构进行介绍,然后介绍代码与实际业务逻辑介绍一下NFS的处理流程。 NFS文件系统的架构分析 NFS分布式文件系统是一个客户端-服务端架构&#…

从力扣[203]理解递归思想

本文旨在通过使用递归方法的使用来进一步了解递归思想 class Solution {public ListNode removeElements(ListNode head, int val) {if (head null) {return head;}head.next removeElements(head.next, val);return head.val val ? head.next : head;} }既然要使用递归算法…

LVS+keepalive高可用集群

keepalive简介 keepalive为LVS应用延伸的高可用服务。lvs的调度器无法做高可用。但keepalive不是为lvs专门集群服务的,也可以为其他的的代理服务器做高可用。 keepalive在lvs的高可用集群,主调度器和备调度器(可以有多个) 一主两备或一主一备。 VRRP: k…