diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4350b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual envs +.venv/ +venv/ +env/ + +# Editor / OS +.DS_Store +.idea/ +.vscode/ +*.swp + +# Build / dist +build/ +dist/ +*.egg-info/ + +# CMake build dirs +cmake-build-*/ +CMakeCache.txt +CMakeFiles/ + +# Model weights & datasets (very large) +*.pth +*.pt +*.ckpt +*.bin +*.safetensors +weights/ +checkpoints/ +data/raw/ diff --git a/README.md b/README.md index 13bb047..145802d 100644 --- a/README.md +++ b/README.md @@ -1,1186 +1,95 @@ -
- - # **深度学习图像分割** - # Deep Learning Image Segmentation -
-
- -**v1.0 louwill**
-**Machine Learning Lab** - - -
-
-
-
- - -## 引言 -图像分类、目标检测和图像分割是基于深度学习的计算机视觉三大核心任务。三大任务之间明显存在着一种递进的层级关系,图像分类聚焦于整张图像,目标检测定位于图像具体区域,而图像分割则是细化到每一个像素。基于深度学习的图像分割具体包括语义分割、实例分割和全景分割。语义分割的目的是要给每个像素赋予一个语义标签。语义分割在自动驾驶、场景解析、卫星遥感图像和医学影像等领域都有着广泛的应用前景。本文作为基于PyTorch的语义分割技术手册,对语义分割的基本技术框架、主要网络模型和技术方法提供一个实战性指导和参考。 - - -## 1. 语义分割概述 -图像分割主要包括语义分割(Semantic Segmentation)和实例分割(Instance Segmentation)。那语义分割和实例分割具体都是什么含义?二者又有什么区别和联系?语义分割是对图像中的每个像素都划分出对应的类别,即实现像素级别的分类;而类的具体对象,即为实例,那么实例分割不但要进行像素级别的分类,还需在具体的类别基础上区别开不同的个体。例如,图像有多个人甲、乙、丙,那边他们的语义分割结果都是人,而实例分割结果却是不同的对象。另外,为了同时实现实例分割与不可数类别的语义分割,相关研究又提出了全景分割(Panoptic Segmentation)的概念。语义分割、实例分割和全景分割具体如图1(b)、(c)和(d)图所示。 -
- -
-
Fig1. Image Segmentation
-
-
-在开始图像分割的学习和尝试之前,我们必须明确语义分割的任务描述,即搞清楚语义分割的输入输出都是什么。输入是一张原始的RGB图像或者单通道图像,但是输出不再是简单的分类类别或者目标定位,而是带有各个像素类别标签的与输入同分辨率的分割图像。简单来说,我们的输入输出都是图像,而且是同样大小的图像。如图2所示。 -
- -
-
Fig2. Pixel Representation
-
-
-类似于处理分类标签数据,对预测分类目标采用像素上的one-hot编码,即为每个分类类别创建一个输出的通道。如图3所示。 -
- -
-
Fig3. Pixel One-hot
-
-
-图4是将分割图添加到原始图像上的叠加效果。这里需要明确一下mask的概念,在图像处理中我们将其译为掩码,如Mask R-CNN中的Mask。Mask可以理解为我们将预测结果叠加到单个通道时得到的该分类所在区域。 -
- -
-
Fig4. Pixel labeling
-
-
-所以,语义分割的任务就是输入图像经过深度学习算法处理得到带有语义标签的同样尺寸的输出图像。 - -## 2. 关键技术组件 -在语义分割发展早期,为了能够让深度学习进行像素级的分类任务,在分类任务的基础上对CNN做了一些修改,将分类网络中浓缩语义表征的全连接层去掉,提出用全卷积网络(Fully Convolutional Networks)来处理语义分割问题。然后U-Net的提出,奠定了编解码结构的U形网络深度学习语义分割中的总统山地位。这里我们对语义分割的关键技术组件进行分开描述,编码器、解码器和Skip Connection属于分割网络的核心结构组件,空洞卷积(Dilate Conv)是独立于U形结构的第二大核心设计。条件随机场(CRF)和马尔科夫随机场(MRF)则是用于优化神经网络分割后的细节处理。深监督作为一种常用的结构设计Trick,在分割网络中也有广泛应用。除此之外,则是针对于语义分割的通用技术点。 -### 2.1 编码器与分类网络 -编码器对于分割网络来说就是进行特征提取和语义信息浓缩的过程,这对熟悉各种分类网络的我们来说并不陌生。编码器通过卷积和池化的组合不断对图像进行下采样,得到的特征图空间尺寸也会越来越小,但会更加具备语义分辨性。这也是大多数分类网络的通用模式,不断卷积池化使得特征图越来越小,然后配上几层全连接网络即可进行分类判别。常用的分类网络包括AlexNet、VGG、ResNet、Inception、DenseNet和MobileNet等等。 -
- -既然之前有那么多优秀的SOTA网络用来做特征提取,所以很多时候分割网络的编码器并不需要我们write from scratch,时刻要有迁移学习的敏感度,直接用现成分类网络的卷积层部分作为编码器进行特征提取和信息浓缩,往往要比从头开始训练一个编码器要快很多。 -
- -比如我们以VGG16作为SegNet编码器的预训练模型,以PyTorch为例,来看编码器的写法。 - -```Python -from torchvision import models - -class SegNet(nn.Module): - - def __init__(self, classes): - super().__init__() - vgg16 = models.vgg16(pretrained=True) - features = vgg16.features - self.enc1 = features[0: 4] - self.enc2 = features[5: 9] - self.enc3 = features[10: 16] - self.enc4 = features[17: 23] - self.enc5 = features[24: -1] -``` -在上述代码中,可以看到我们将vgg16的31个层分作5个编码模块,每个编码模块的基本结构如下所示: -```Python -(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) -(1): ReLU(inplace=True) -(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) -(3): ReLU(inplace=True) -(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) -``` - - -### 2.2 解码器与上采样 -编码器不断将输入不断进行下采样达到信息浓缩,而解码器则负责上采样来恢复输入尺寸。解码器中除了一些卷积方法用为辅助之外,最关键的还是一些上采样方法,主要包括双线性插值、转置卷积和反池化。 -#### 双线性插值 -插值法(Interpolation)是一种经典的数值分析方法,一些经典插值大家或多或少都有听到过,比如线性插值、三次样条插值和拉格朗日插值法等。在说双线性插值前我们先来了解一下什么是线性插值(Linear interpolation)。线性插值法是指使用连接两个已知量的直线来确定在这两个已知量之间的一个未知量的值的方法。如下图所示: -
- -
-
Fig5. Linear Interpolation
-
- -
- -已知直线上两点坐标分别为$(x_1,y_1)$和$(x_2,y_2)$,现在想要通过线性插值法来得到某一点$x$在直线上的值。基本就是一个初中数学题,这里就不做过多展开,点$x$在直线上的值$y$可以表示为: - -
- -$y=\frac{x_2-x}{x_2-x_1}y_2+\frac{x-x_1}{x_2-x_1}y_1$ - -
- -再来看双线性插值。线性插值用到两个点来确定插值,双线性插值则需要四个点。在图像上采样中,双线性插值利用四个点的像素值来确定要插值的一个像素值,其本质上还是分别在$x$和$y$方向上分别进行两次线性插值。如下图所示,我们来看具体做法。 -
- -
-
Fig6. Bilinear Interpolation
-
-
- -图中$Q_{11}-Q_{22}$四个黄色的点是已知数据点,红色点$P$是待插值点。假设$Q_{11}$为$(x_1,y_1)$,$Q_{12}$为$(x_1,y_2)$,$Q_{21}$为$(x_2,y_1)$,$Q_{22}$为$(x_2,y_2)$。我们先在$x$轴方向上进行线性插值,先求得$R_1$和$R_2$的插值。根据线性插值公式,有: - -
- -$f(R_1)=\frac{x_2-x}{x_2-x_1}f(Q_{11})+\frac{x-x_1}{x_2-x_1}f(Q_{21})$ -$f(R_2)=\frac{x_2-x}{x_2-x_1}f(Q_{12})+\frac{x-x_1}{x_2-x_1}f(Q_{22})$ - -
- -得到$R_1$和$R_2$点坐标之后,便可继续在$y$轴方向进行线性插值。可得目标点$P$的插值为: -
- -$f(P)=\frac{y_2-y}{y_2-y_1}f(R_1)+\frac{y-y_1}{y_2-y_1}f(R_2)$ - -
- -双线性插值在众多经典的语义分割网络中都有用到,比如说奠定语义分割编解码框架的FCN网络。假设将$3\times6$的图像通过双线性插值变为$6\times12$的图像,如下图所示。 -
- -
-
Fig7. Bilinear Interpolation Example
-
-
- 双线性插值的优点是速度非常快,计算量小,但缺点就是效果不是特别理想。 -
- -#### 转置卷积 -转置卷积(Transposed Convolution)也叫解卷积(Deconvolution),有些人也将其称为反卷积,但这个叫法并不太准确。大家都知道,在常规卷积时,我们每次得到的卷积特征图尺寸是越来越小的。但在图像分割等领域,我们是需要逐步恢复输入时的尺寸的。如果把常规卷积时的特征图不断变小叫做下采样,那么通过转置卷积来恢复分辨率的操作可以称作上采样。 - -本质上来说,转置卷积跟常规卷积并无区别。不同之处在于先按照一定的比例进行padding来扩大输入尺寸,然后把常规卷积中的卷积核进行转置,再按常规卷积方法进行卷积就是转置卷积。假设输入图像矩阵为$X$,卷积核矩阵为$C$,常规卷积的输出为$Y$,则有: -
- -$Y=CX$ -
- -两边同时乘以卷积核的转置$C^T$,这个公式便是转置卷积的输入输出计算。 -
- -$X=C^TY$ -
- -假设输入大小为$4\times4$,滤波器大小为$3\times3$,常规卷积下输出为$2\times2$,为了演示转置卷积,我们将滤波器矩阵进行稀疏化处理为$4\times16$,将输入矩阵进行拉平为$16\times1$,相应输出结果也会拉平为$4\times1$,图示如下: -
- -
-
Fig8. Matrix of Convolution
-
- -然后按照转置卷积的做法我们把卷积核矩阵进行转置,按照$X=C^TY$进行验证: -
- -
-
Fig9. Matrix of Transpose Convolution
-
- - -#### 反池化 -反池化(Unpooling)可以理解为池化的逆操作,相较于前两种上采样方法,反池化用的并不是特别多。其简要原理如下,在池化时记录下对应kernel中的坐标,在反池化时将一个元素根据kernel进行放大,根据之前的坐标将元素填写进去,其他位置补位为0即可。 - -### 2.3 Skip Connection -跳跃连接本身是在ResNet中率先提出,用于学习一个恒等式和残差结构,后面在DenseNet、FCN和U-Net等网络中广泛使用。最典型的就是U-Net的跳跃连接,在每个编码和解码层之间各添加一个跳跃连接,每一次下采样都会有一个跳跃连接与对应的上采样进行级联,这种不同尺度的特征融合对上采样恢复像素大有帮助。 - - -### 2.4 Dilate Conv与多尺度 -空洞卷积(Dilated/Atrous Convolution)也叫扩张卷积或者膨胀卷积,字面意思上来说就是在卷积核中插入空洞,起到扩大感受野的作用。空洞卷积的直接做法是在常规卷积核中填充0,用来扩大感受野,且进行计算时,空洞卷积中实际只有非零的元素起了作用。假设以一个变量a来衡量空洞卷积的扩张系数,则加入空洞之后的实际卷积核尺寸与原始卷积核尺寸之间的关系: -
- -$K=k+(k-1)(a-1)$ -
- -其中$k$为原始卷积核大小,$a$为卷积扩张率(dilation rate),$K$为经过扩展后实际卷积核大小。除此之外,空洞卷积的卷积方式跟常规卷积一样。当$a=1$时,空洞卷积就退化为常规卷积。$a=1,2,4$时,空洞卷积示意图如下: -
- -
-
Fig10. Dialate Convolution
-
- -
- -当$a=1$,原始卷积核size为$3\times3$,就是常规卷积。$a=2$时,加入空洞之后的卷积核$size=3+(3-1)\times(2-1)=5$,对应的感受野可计算为$2^{(a+2)}-1=7$。$a=3$时,卷积核size可以变化到$3+(3-1)(4-1)=9$,感受野则增长到$2^{(a+2)}-1=15$。对比不加空洞卷积的情况,在stride为1的情况下3层3x3卷积的叠加,第三层输出特征图对应的感受野也只有$1+(3-1)\times3=7$。所以,空洞卷积的一个重要作用就是增大感受野。 - -在语义分割的发展历程中,增大感受野是一个非常重要的设计。早期FCN提出以全卷积方式来处理像素级别的分割任务时,包括后来奠定语义分割baseline地位的U-Net,网络结构中存在大量的池化层来进行下采样,大量使用池化层的结果就是损失掉了一些信息,在解码上采样重建分辨率的时候肯定会有影响。特别是对于多目标、小物体的语义分割问题,以U-Net为代表的分割模型一直存在着精度瓶颈的问题。而基于增大感受野的动机背景下就提出了以空洞卷积为重大创新的deeplab系列分割网络,我们在深度学习语义分割模型中会对deeplab进行详述,这里不做过多展开。 - - -对于语义分割而言,空洞卷积主要有三个作用: -- 第一是扩大感受野,具体前面已经说的比较多了,这里不做重复。但需要明确一点,池化也可以扩大感受野,但空间分辨率降低了,相比之下,空洞卷积可以在扩大感受野的同时不丢失分辨率,且保持像素的相对空间位置不变。简单而言就是空洞卷积可以同时控制感受野和分辨率。 - -- 第二就是获取多尺度上下文信息。当多个带有不同dilation rate的空洞卷积核叠加时,不同的感受野会带来多尺度信息,这对于分割任务是非常重要的。 - -- 第三就是可以降低计算量,不需要引入额外的参数,如上图空洞卷积示意图所示,实际卷积时只有带有红点的元素真正进行计算。 -
- -### 2.5 后处理技术 - - -
-早期语义分割模型效果较为粗糙,在没有更好的特征提取模型的情况下,研究者们便在神经网络模型的粗糙结果进行后处理(Post-Processing),主要方法就是一些常用的概率图模型,比如说条件随机场(Conditional Random Field,CRF)和马尔可夫随机场(Markov Random Field,MRF)。 - -CRF是一种经典的概率图模型,简单而言就是给定一组输入序列的条件下,求另一组输出序列的条件概率分布模型,CRF在自然语言处理领域有着广泛应用。CRF在语义分割后处理中用法的基本思路如下:对于FCN或者其他分割网络的粗粒度分割结果而言,每个像素点$i$具有对应的类别标签$x_i$和观测值$y_i$,以每个像素为节点,以像素与像素之间的关系作为边即可构建一个CRF模型。在这个CRF模型中,我们通过观测变量$y_i$来预测像素$i$对应的标签值$x_i$。 -
- -
-
Fig11. CRF
-
- -以上做法也叫DenseCRF,具体细节可参考论文: -[Efficient Inference in Fully Connected CRFs with Gaussian Edge Potentials](https://papers.nips.cc/paper/4296-efficient-inference-in-fully-connected-crfs-with-gaussian-edge-potentials.pdf),除此之外还有CRFasRNN,采用平均场近似的方式将CRF方法融入到神经网络过程中,本质上是对DenseCRF的一种优化。 - -另一种后处理概率图模型是MRF,MRF与CRF较为类似,只是对CRF的二元势函数做了调整,其优点在于可以使用平均场来构造CNN网络,并且推理过程可以一次性搞定。MRF在Deep Parsing Network(DPN)中有详细描述,相关细节可参考论文[Semantic Image Segmentation via Deep Parsing Network](https://arxiv.org/pdf/1509.02634.pdf)。 - -语义分割发展前期,在分割网络模型的结果上加上CRF和MRF等后处理技术形成了早期的语义分割技术框架: -
- -
-
Fig12. Framework of Semantic Segmentation with CRF/MRF
-
- -但从Deeplab v3开始,主流的语义分割网络就不再热衷于后处理技术了。一个典型的观点认为神经网络分割效果不好才会用后处理技术,这说明在分割网络本身上还有很大的提升空间。一是CRF本身不太容易训练,二来语义分割任务的端到端趋势。后来语义分割领域的SOTA网络也确实证明了这一点。尽管如此,CRF等后处理技术作为语义分割发展历程上的一个重要方法,我们有必要在此进行说明。从另一方面看,深度学习和概率图的结合虽然并不是那么顺利,但相信未来依旧会大有前景。 -
- -### 2.6 深监督 -所谓深监督(Deep Supervision),就是在深度神经网络的某些中间隐藏层加了一个辅助的分类器作为一种网络分支来对主干网络进行监督的技巧,用来解决深度神经网络训练梯度消失和收敛速度过慢等问题。 - -带有深监督的一个8层深度卷积网络结构如下图所示。 -
- -
-
Fig13. Deep Supervision Example
-
-
- -可以看到,图中在第四个卷积块之后添加了一个监督分类器作为分支。`Conv4`输出的特征图除了随着主网络进入`Conv5`之外,也作为输入进入了分支分类器。如图所示,该分支分类器包括一个卷积块、两个带有`Dropout`和`ReLu`的全连接块和一个纯全连接块。带有深监督的卷积模块例子如下。 - -```Python -class C1DeepSup(nn.Module): - def __init__(self, num_class=150, fc_dim=2048, use_softmax=False): - super(C1DeepSup, self).__init__() - self.use_softmax = use_softmax - self.cbr = conv3x3_bn_relu(fc_dim, fc_dim // 4, 1) - self.cbr_deepsup = conv3x3_bn_relu(fc_dim // 2, fc_dim // 4, 1) - # 最后一层卷积 - self.conv_last = nn.Conv2d(fc_dim // 4, num_class, 1, 1, 0) - self.conv_last_deepsup = nn.Conv2d(fc_dim // 4, num_class, 1, 1, 0) - - # 前向计算流程 - def forward(self, conv_out, segSize=None): - conv5 = conv_out[-1] - x = self.cbr(conv5) - x = self.conv_last(x) - # is True during inference - if self.use_softmax: - x = nn.functional.interpolate( - x, size=segSize, mode='bilinear', align_corners=False) - x = nn.functional.softmax(x, dim=1) - return x - # 深监督模块 - conv4 = conv_out[-2] - _ = self.cbr_deepsup(conv4) - _ = self.conv_last_deepsup(_) - - # 主干卷积网络softmax输出 - x = nn.functional.log_softmax(x, dim=1) - # 深监督分支网络softmax输出 - _ = nn.functional.log_softmax(_, dim=1) - return (x, _) -``` - -### 2.7 通用技术 -通用技术主要是指深度学习流程中会用到的基本模块,比如说损失函数的选取以及采用哪种精度衡量指标。其他的像优化器的选取,学习率的控制等,这里限于篇幅进行省略。 -#### 损失函数 -常用的分类损失均可用作语义分割的损失函数。最常用的就是交叉熵损失函数,如果只是前景分割,则可以使用二分类的交叉熵损失(Binary CrossEntropy Loss, BCE loss),对于目标物体较小的情况我们可以使用Dice损失,对于目标物体类别不均衡的情况可以使用加权的交叉熵损失(Weighted CrossEntropy Loss, WCE Loss),另外也可以尝试多种损失函数的组合。 -#### 精度描述 -语义分割作为经典的图像分割问题,其本质上还是一种图像像素分类。既然是分类,我们就可以使用常见的分类评价指标来评估模型好坏。语义分割常见的评价指标包括像素准确率(Pixel Accuracy)、平均像素准确率(Mean Pixel Accuracy)、平均交并比(Mean IoU)、频权交并比(FWIoU)和Dice系数(Dice Coeffcient)等。 - -**像素准确率(PA)。** 像素准确率跟分类中的准确率含义一样,即所有分类正确的像素数占全部像素的比例。PA的计算公式如下: -
- -$PA=\frac{\sum_{i=0}^{n}p_{ii}}{\sum_{i=0}^{n}\sum_{j=0}^{n}p_{ij}}$ -
- -**平均像素准确率(MPA)。** 平均像素准确率其实更应该叫平均像素精确率,是指分别计算每个类别分类正确的像素数占所有预测为该类别像素数比例的平均值。所以,从定义上看,这是精确率(Precision)的定义,MPA的计算公式如下: -
- -$MPA=\frac{1}{n+1}\sum_{i=0}^{n}\frac{p_{ii}}{\sum_{j=0}^{n}p_{ij}}$ -
- -**平均交并比(MIoU)。** 交并比(Intersection over Union)的定义很简单,将标签图像和预测图像看成是两个集合,计算两个集合的交集和并集的比值。而平均交并比则是将所有类的IoU取平均。 MIoU的计算公式如下: -
- -$MIoU=\frac{1}{n+1}\sum_{i=0}^{n}\frac{p_{ii}}{\sum_{j=0}^{n}p_{ij}+\sum_{j=0}^{n}p_{ji}-p_{ii}}$ -
- -**频权交并比(FWIoU)。** 频权交并比顾名思义,就是以每一类别的频率为权重和其IoU加权计算出来的结果。FWIoU的设计思想很明确,语义分割很多时候会面临图像中各目标类别不平衡的情况,对各类别IoU直接求平均不是很合理,所以考虑各类别的权重就非常重要了。FWIoU的计算公式如下: -
- -$FWIoU=\frac{1}{\sum_{i=0}^{n}\sum_{j=0}^{n}p_{ij}}\sum_{i=0}^{n}\frac{\sum_{j=0}^{n}p_{ij}p_{ii}}{\sum_{j=0}^{n}p_{ij}+\sum_{j=0}^{n}p_{ji}-p_{ii}}$ -
- -**Dice系数。** Dice系数是一种度量两个集合相似性的函数,是语义分割中最常用的评价指标之一。Dice系数定义为两倍的交集除以像素和,跟IoU有点类似,其计算公式如下: -
- -$dice=\frac{2|X\cap{Y}|}{|X|+|Y|}$ -
- -dice本质上跟分类指标中的F1-Score类似。作为最常用的分割指标之一,这里给出PyTorch的实现方式。 - -```Python -import torch - -def dice_coef(pred, target): - """ - Dice = (2*|X & Y|)/ (|X|+ |Y|) - = 2*sum(|A*B|)/(sum(A^2)+sum(B^2)) - """ - smooth = 1. - m1 = pred.view(-1).float() - m2 = target.view(-1).float() - intersection = (m1 * m2).sum().float() - dice = (2. * intersection + smooth) / (torch.sum(m1*m1) + torch.sum(m2*m2) + smooth) - return dice -``` - -## 3. 数据Pipeline -这里主要说一下PyTorch的自定义数据读取pipeline模板和相关trciks以及如何优化数据读取的pipeline等。我们从PyTorch的数据对象类`Dataset`开始。`Dataset`在PyTorch中的模块位于`utils.data`下。 -```Python -from torch.utils.data import Dataset -``` -### 3.1 Torch数据读取模板 -PyTorch官方为我们提供了自定义数据读取的标准化代码代码模块,作为一个读取框架,我们这里称之为原始模板。其代码结构如下: -```Python -from torch.utils.data import Dataset - -class CustomDataset(Dataset): - def __init__(self, ...): - # stuff - - def __getitem__(self, index): - # stuff - return (img, label) - - def __len__(self): - # return examples size - return count -``` - -### 3.2 transform与数据增强 -PyTorch数据增强功能可以放在`transform`模块下,添加`transform`后的数据读取结构如下所示: -```Python -from torch.utils.data import Dataset -from torchvision import transforms as T - -class CustomDataset(Dataset): - def __init__(self, ...): - # stuff - # ... - # compose the transforms methods - self.transform = T.Compose([T.CenterCrop(100), - T.RandomResizedCrop(256), - T.RandomRotation(45), - T.ToTensor()]) - - def __getitem__(self, index): - # stuff - # ... - data = # Some data read from a file or image - labe = # Some data read from a file or image - - # execute the transform - data = self.transform(data) - label = self.transform(label) - return (img, label) - - def __len__(self): - # return examples size - return count - -if __name__ == '__main__': - # Call the dataset - custom_dataset = CustomDataset(...) -``` - -需要说明的是,PyTorch `transform`模块所做的数据增强并不是我们所理解的广义上的数据增强。`transform`所做的增强,仅仅是在数据读取过程中随机地对某张图像做转化操作,实际数据量上并没有增多,可以将其视为是一种在线增强的策略。如果想要实现实际训练数据成倍数的增加,可以使用离线增强策略。 - -与图像分类仅需要对输入图像做增强不同的是,对于语义分割的数据增强而言,需要同时对输入图像和输入的mask同步进行数据增强工作。实际写代码时,要记得使用随机种子,在不失随机性的同时,保证输入图像和输出mask具备同样的转换。一个完整的语义分割在线数据增强代码实例如下: -```Python -import os -import random -import torch -from torch.utils.data import Dataset -from PIL import Image - -class SegmentationDataset(Dataset): - # read the input images - @staticmethod - def _load_input_image(path): - with open(path, 'rb') as f: - img = Image.open(f) - return img.convert('RGB') - # read the mask images - @staticmethod - def _load_target_image(path): - with open(path, 'rb') as f: - img = Image.open(f) - return img.convert('L') - - def __init__(self, input_root, target_root, transform_input=None, - transform_target=None, seed_fn=None): - self.input_root = input_root - self.target_root = target_root - self.transform_input = transform_input - self.transform_target = transform_target - self.seed_fn = seed_fn - # sort the ids - self.input_ids = sorted(img for img in os.listdir(self.input_root)) - self.target_ids = sorted(img for img in os.listdir(self.target_root)) - assert(len(self.input_ids) == len(self.target_ids)) - - # set random number seed - def _set_seed(self, seed): - random.seed(seed) - torch.manual_seed(seed) - if self.seed_fn: - self.seed_fn(seed) - - def __getitem__(self, idx): - input_img = self._load_input_image( - os.path.join(self.input_root, self.input_ids[idx])) - target_img = self._load_target_image( - os.path.join(self.target_root, self.target_ids[idx])) - - if self.transform_input: - # ensure that the input and output have the same randomness. - seed = random.randint(0, 2**32) - self._set_seed(seed) - input_img = self.transform_input(input_img) - self._set_seed(seed) - target_img = self.transform_target(target_img) - return input_img, target_img, self.input_ids[idx] - - def __len__(self): - return len(self.input_ids) -``` - -其中`transform_input`和`transform_target`均可由`transform`模块下的函数封装而成。一个皮肤病灶分割的在线数据增强实例效果如下图所示。 -
- -
-
Fig14. Example of online augmentation
-
- -## 4. 模型与算法 -早期基于深度学习的图像分割以FCN为核心,旨在重点解决如何更好从卷积下采样中恢复丢掉的信息损失。后来逐渐形成了以U-Net为核心的这样一种编解码对称的U形结构。**语义分割界迄今为止最重要的两个设计,一个是以U-Net为代表的U形结构,目前基于U-Net结构的创新就层出不穷,比如说应用于3D图像的V-Net,嵌套U-Net结构的U-Net++等。除此在外还有SegNet、RefineNet、HRNet和FastFCN。另一个则是以DeepLab系列为代表的Dilation设计,主要包括DeepLab系列和PSPNet。随着模型的Baseline效果不断提升,语义分割任务的主要矛盾也逐从downsample损失恢复像素逐渐演变为如何更有效地利用context上下文信息。** - -### 4.1 FCN -FCN(Fully Convilutional Networks)是语义分割领域的开山之作。FCN的提出是在2016年,相较于此前提出的AlexNet和VGG等卷积全连接的网络结构,FCN提出用卷积层代替全连接层来处理语义分割问题,这也是FCN的由来,即全卷积网络。 - -FCN的关键点主要有三,一是全卷积进行特征提取和下采样,二是双线性插值进行上采样,三是跳跃连接进行特征融合。 -
- -
-
Fig15. FCN
-
- -利用PyTorch实现一个FCN-8网络: -```Python -import torch -import torch.nn as nn -import torch.nn.init as init -import torch.nn.functional as F - -from torch.utils import model_zoo -from torchvision import models - -class FCN8(nn.Module): - - def __init__(self, num_classes): - super().__init__() - - feats = list(models.vgg16(pretrained=True).features.children()) - - self.feats = nn.Sequential(*feats[0:9]) - self.feat3 = nn.Sequential(*feats[10:16]) - self.feat4 = nn.Sequential(*feats[17:23]) - self.feat5 = nn.Sequential(*feats[24:30]) - - for m in self.modules(): - if isinstance(m, nn.Conv2d): - m.requires_grad = False - - self.fconn = nn.Sequential( - nn.Conv2d(512, 4096, 7), - nn.ReLU(inplace=True), - nn.Dropout(), - nn.Conv2d(4096, 4096, 1), - nn.ReLU(inplace=True), - nn.Dropout(), - ) - self.score_feat3 = nn.Conv2d(256, num_classes, 1) - self.score_feat4 = nn.Conv2d(512, num_classes, 1) - self.score_fconn = nn.Conv2d(4096, num_classes, 1) - - def forward(self, x): - feats = self.feats(x) - feat3 = self.feat3(feats) - feat4 = self.feat4(feat3) - feat5 = self.feat5(feat4) - fconn = self.fconn(feat5) - - score_feat3 = self.score_feat3(feat3) - score_feat4 = self.score_feat4(feat4) - score_fconn = self.score_fconn(fconn) - - score = F.upsample_bilinear(score_fconn, score_feat4.size()[2:]) - score += score_feat4 - score = F.upsample_bilinear(score, score_feat3.size()[2:]) - score += score_feat3 - - return F.upsample_bilinear(score, x.size()[2:]) -``` -从代码中可以看到,我们使用了vgg16作为FCN-8的编码部分,这使得FCN-8具备较强的特征提取能力。 - -### 4.2 UNet -早期基于深度学习的图像分割以FCN为核心,旨在重点解决如何更好从卷积下采样中恢复丢掉的信息损失。后来逐渐形成了以UNet为核心的这样一种编解码对称的U形结构。 - -UNet结构能够在分割界具有一统之势,最根本的还是其效果好,尤其是在医学图像领域。所以,做医学影像相关的深度学习应用时,一定都用过UNet,而且最原始的UNet一般都会有一个不错的baseline表现。2015年发表UNet的MICCAI,是目前医学图像分析领域最顶级的国际会议,UNet为什么在医学上效果这么好非常值得探讨一番。 - -U-Net结构如下图所示: -
- -
-
Fig16. UNet
-
-
-乍一看很复杂,U形结构下貌似有很多细节问题。我们来把UNet简化一下,如下图所示: -
- -
-
Fig17. U-Net简化
-
-
-从图中可以看到,简化之后的UNet的关键点只有三条线: - -- 下采样编码 -- 上采样解码 -- 跳跃连接 - -下采样进行信息浓缩和上采样进行像素恢复,这是其他分割网络都会有的部分,UNet自然也不会跳出这个框架,可以看到,UNet进行了4次的最大池化下采样,每一次采样后都使用了卷积进行信息提取得到特征图,然后再经过4次上采样恢复输入像素尺寸。但UNet最关键的、也是最特色的部分在于图中红色虚线的Skip Connection。每一次下采样都会有一个跳跃连接与对应的上采样进行级联,这种不同尺度的特征融合对上采样恢复像素大有帮助,具体来说就是高层(浅层)下采样倍数小,特征图具备更加细致的图特征,底层(深层)下采样倍数大,信息经过大量浓缩,空间损失大,但有助于目标区域(分类)判断,当high level和low level的特征进行融合时,分割效果往往会非常好。从某种程度上讲,这种跳跃连接也可以视为一种Deep Supervision。 - -U-Net的简单实现如下: -```Python -# 编码块 -class UNetEnc(nn.Module): - - def __init__(self, in_channels, out_channels, dropout=False): - super().__init__() - - layers = [ - nn.Conv2d(in_channels, out_channels, 3, dilation=2), - nn.ReLU(inplace=True), - nn.Conv2d(out_channels, out_channels, 3, dilation=2), - nn.ReLU(inplace=True), - ] - if dropout: - layers += [nn.Dropout(.5)] - layers += [nn.MaxPool2d(2, stride=2, ceil_mode=True)] - - self.down = nn.Sequential(*layers) - - def forward(self, x): - return self.down(x) - -# 解码块 -class UNetDec(nn.Module): - - def __init__(self, in_channels, features, out_channels): - super().__init__() - - self.up = nn.Sequential( - nn.Conv2d(in_channels, features, 3), - nn.ReLU(inplace=True), - nn.Conv2d(features, features, 3), - nn.ReLU(inplace=True), - nn.ConvTranspose2d(features, out_channels, 2, stride=2), - nn.ReLU(inplace=True), - ) - - def forward(self, x): - return self.up(x) -# U-Net -class UNet(nn.Module): - - def __init__(self, num_classes): - super().__init__() - - self.enc1 = UNetEnc(3, 64) - self.enc2 = UNetEnc(64, 128) - self.enc3 = UNetEnc(128, 256) - self.enc4 = UNetEnc(256, 512, dropout=True) - self.center = nn.Sequential( - nn.Conv2d(512, 1024, 3), - nn.ReLU(inplace=True), - nn.Conv2d(1024, 1024, 3), - nn.ReLU(inplace=True), - nn.Dropout(), - nn.ConvTranspose2d(1024, 512, 2, stride=2), - nn.ReLU(inplace=True), - ) - self.dec4 = UNetDec(1024, 512, 256) - self.dec3 = UNetDec(512, 256, 128) - self.dec2 = UNetDec(256, 128, 64) - self.dec1 = nn.Sequential( - nn.Conv2d(128, 64, 3), - nn.ReLU(inplace=True), - nn.Conv2d(64, 64, 3), - nn.ReLU(inplace=True), - ) - self.final = nn.Conv2d(64, num_classes, 1) - - # 前向传播过程 - def forward(self, x): - enc1 = self.enc1(x) - enc2 = self.enc2(enc1) - enc3 = self.enc3(enc2) - enc4 = self.enc4(enc3) - center = self.center(enc4) - # 包含了同层分辨率级联的解码块 - dec4 = self.dec4(torch.cat([ - center, F.upsample_bilinear(enc4, center.size()[2:])], 1)) - dec3 = self.dec3(torch.cat([ - dec4, F.upsample_bilinear(enc3, dec4.size()[2:])], 1)) - dec2 = self.dec2(torch.cat([ - dec3, F.upsample_bilinear(enc2, dec3.size()[2:])], 1)) - dec1 = self.dec1(torch.cat([ - dec2, F.upsample_bilinear(enc1, dec2.size()[2:])], 1)) - - return F.upsample_bilinear(self.final(dec1), x.size()[2:]) -``` - -### 4.3 SegNet -SegNet网络是典型的编码-解码结构。SegNet编码器网络由VGG16的前13个卷积层构成,所以通常是使用VGG16的预训练权重来进行初始化。每个编码器层都有一个对应的解码器层,因此解码器层也有13层。编码器最后的输出输入到softmax分类器中,输出每个像素的类别概率。SegNet如下图所示。 -
- -
-
Fig18. SegNet结构
-
- -SegNet的一个简易参考实现如下: -```Python -import torch -import torch.nn as nn -import torch.nn.init as init -import torch.nn.functional as F -from torchvision import models - -# define Decoder -class SegNetDec(nn.Module): - - def __init__(self, in_channels, out_channels, num_layers): - super().__init__() - layers = [ - nn.Conv2d(in_channels, in_channels // 2, 3, padding=1), - nn.BatchNorm2d(in_channels // 2), - nn.ReLU(inplace=True), - ] - layers += [ - nn.Conv2d(in_channels // 2, in_channels // 2, 3, padding=1), - nn.BatchNorm2d(in_channels // 2), - nn.ReLU(inplace=True), - ] * num_layers - layers += [ - nn.Conv2d(in_channels // 2, out_channels, 3, padding=1), - nn.BatchNorm2d(out_channels), - nn.ReLU(inplace=True), - ] - self.decode = nn.Sequential(*layers) - - def forward(self, x): - return self.decode(x) - -# SegNet -class SegNet(nn.Module): - - def __init__(self, classes): - super().__init__() - vgg16 = models.vgg16(pretrained=True) - features = vgg16.features - self.enc1 = features[0: 4] - self.enc2 = features[5: 9] - self.enc3 = features[10: 16] - self.enc4 = features[17: 23] - self.enc5 = features[24: -1] - - for m in self.modules(): - if isinstance(m, nn.Conv2d): - m.requires_grad = False - - self.dec5 = SegNetDec(512, 512, 1) - self.dec4 = SegNetDec(512, 256, 1) - self.dec3 = SegNetDec(256, 128, 1) - self.dec2 = SegNetDec(128, 64, 0) - - self.final = nn.Sequential(*[ - nn.Conv2d(64, classes, 3, padding=1), - nn.BatchNorm2d(classes), - nn.ReLU(inplace=True) - ]) - - def forward(self, x): - x1 = self.enc1(x) - e1, m1 = F.max_pool2d(x1, kernel_size=2, stride=2, return_indices=True) - x2 = self.enc2(e1) - e2, m2 = F.max_pool2d(x2, kernel_size=2, stride=2, return_indices=True) - x3 = self.enc3(e2) - e3, m3 = F.max_pool2d(x3, kernel_size=2, stride=2, return_indices=True) - x4 = self.enc4(e3) - e4, m4 = F.max_pool2d(x4, kernel_size=2, stride=2, return_indices=True) - x5 = self.enc5(e4) - e5, m5 = F.max_pool2d(x5, kernel_size=2, stride=2, return_indices=True) - - def upsample(d): - d5 = self.dec5(F.max_unpool2d(d, m5, kernel_size=2, stride=2, output_size=x5.size())) - d4 = self.dec4(F.max_unpool2d(d5, m4, kernel_size=2, stride=2, output_size=x4.size())) - d3 = self.dec3(F.max_unpool2d(d4, m3, kernel_size=2, stride=2, output_size=x3.size())) - d2 = self.dec2(F.max_unpool2d(d3, m2, kernel_size=2, stride=2, output_size=x2.size())) - d1 = F.max_unpool2d(d2, m1, kernel_size=2, stride=2, output_size=x1.size()) - return d1 - - d = upsample(e5) - return self.final(d) -``` - -### 4.4 Deeplab系列 -Deeplab系列可以算是深度学习语义分割的另一个主要架构,其代表方法就是基于Dilation的多尺度设计。Deeplab系列主要包括: -- Deeplab v1 -- Deeplab v2 -- Deeplab v3 -- Deeplab v3+ - -Deeplab v1主要是率先使用了空洞卷积,是Deeplab系列最原始的版本。Deeplab v2在Deeplab v1的基础上最大的改进在于提出了ASPP(Atrous Spatial Pyramid Pooling),即带有空洞卷积的金字塔池化,该设计的主要目的就是提取图像的多尺度特征。另外Deeplab v2也将Deeplab v1的Backone网络更换为ResNet。Deeplab v1和v2还有一个比较大的特点就是使用了CRF作为后处理技术。 - -这里重点说一下多尺度问题。多尺度问题就是当图像中的目标对象存在不同大小时,分割效果不佳的现象。比如同样的物体,在近处拍摄时物体显得大,远处拍摄时显得小。解决多尺度问题的目标就是不论目标对象是大还是小,网络都能将其分割地很好。Deeplab v2使用ASPP处理多尺度问题,ASPP设计结构如下图所示。 -
- -
-
Fig19. ASPP
-
- -从Deeplab v3开始,Deeplab系列舍弃了CRF后处理模块,提出了更加通用的、适用任何网络的分割框架,对ResNet最后的Block做了复制和级联(Cascade),对ASPP模块做了升级,在其中添加了BN层。改进后的ASPP如下图所示。 -
- -
-
Fig20. ASPP of Deeplab v3
-
- -Deeplab v3+在Deeplab v3的基础上做了扩展和改进,其主要改进就是在编解码结构上使用了ASPP。Deeplab v3+可以视作是融合了语义分割两大流派的一项工作,即编解码+ASPP结构。另外Deeplab v3+的Backbone换成了Xception,其深度可分离卷积的设计使得分割网络更加高效。Deeplab v3+结构如下图所示。 -
- -
-
Fig21. ASPP
-
- -关于Deeplab系列各个版本的技术点构成总结如下表所示。Deeplab系列算法实现可参考GitHub上各版本,这里不再一一给出。 -
- -
-
Fig22. Summary of Deeplab series.
-
- -### 4.5 PSPNet -PSPNet是针对多尺度问题提出的另一种代表性分割网络。PSPNet认为此前的分割网络没有引入足够的上下文信息及不同感受野下的全局信息而存在分割出现错误的情况,因而引入Global-Scence-Level的信息解决该问题,其Backbone网络也是ResNet。简单来说,PSPNet就是将Deeplab的ASPP模块之前的特征图Pooling了四种尺度,然后将原始特征图和四种Pooling之后的特征图进行合并到一起,再经过一系列卷积之后进行预测的过程。PSPNet结构如下图所示。 -
- -
-
Fig23. PSPNet
-
- -一个简易的带有深监督的PSPNet PPM模块写法如下: -```Python -# Pyramid Pooling Module -class PPMDeepsup(nn.Module): - def __init__(self, num_class=150, fc_dim=4096, - use_softmax=False, pool_scales=(1, 2, 3, 6)): - super(PPMDeepsup, self).__init__() - self.use_softmax = use_softmax - # PPM - self.ppm = [] - for scale in pool_scales: - self.ppm.append(nn.Sequential( - nn.AdaptiveAvgPool2d(scale), - nn.Conv2d(fc_dim, 512, kernel_size=1, bias=False), - BatchNorm2d(512), - nn.ReLU(inplace=True) - )) - self.ppm = nn.ModuleList(self.ppm) - # Deep Supervision - self.cbr_deepsup = conv3x3_bn_relu(fc_dim // 2, fc_dim // 4, 1) - - self.conv_last = nn.Sequential( - nn.Conv2d(fc_dim+len(pool_scales)*512, 512, - kernel_size=3, padding=1, bias=False), - BatchNorm2d(512), - nn.ReLU(inplace=True), - nn.Dropout2d(0.1), - nn.Conv2d(512, num_class, kernel_size=1) - ) - self.conv_last_deepsup = nn.Conv2d(fc_dim // 4, num_class, 1, 1, 0) - self.dropout_deepsup = nn.Dropout2d(0.1) -``` - -### 4.6 UNet++ -自从2015年UNet网络提出后,这么多年大家没少在这个U形结构上折腾。大部分做语义分割的朋友都没少在UNet结构上做各种魔改,如果把UNet++算作是UNet的一种魔改的话,那它一定是最成功的魔改者。 - -UNet++是一种嵌套的U-Net结构,即内置了不同深度的UNet网络,并且利用了全尺度的跳跃连接(skip connection)和深度监督(deep supervisions)。另外UNet++还设计一种剪枝方案,加快了UNet++的推理速度。UNet++的结构示意图如下所示。 -
- -
-
Fig24. UNet++
-
- -单纯从结构设计的角度来看,UNet++效果好要归功于其嵌套结构和重新设计的跳跃连接,旨在解决UNet的两个关键挑战:1)优化整体结构的未知深度和2)跳跃连接的不必要的限制性设计。UNet++的一个简单的实现代码如下所示。 -```Python -import torch -from torch import nn - -class NestedUNet(nn.Module): - def __init__(self, num_classes, input_channels=3, deep_supervision=False, **kwargs): - super().__init__() - nb_filter = [32, 64, 128, 256, 512] - self.deep_supervision = deep_supervision - - self.pool = nn.MaxPool2d(2, 2) - self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) - - self.conv0_0 = VGGBlock(input_channels, nb_filter[0], nb_filter[0]) - self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1]) - self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2]) - self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3]) - self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4]) - - self.conv0_1 = VGGBlock(nb_filter[0]+nb_filter[1], nb_filter[0], nb_filter[0]) - self.conv1_1 = VGGBlock(nb_filter[1]+nb_filter[2], nb_filter[1], nb_filter[1]) - self.conv2_1 = VGGBlock(nb_filter[2]+nb_filter[3], nb_filter[2], nb_filter[2]) - self.conv3_1 = VGGBlock(nb_filter[3]+nb_filter[4], nb_filter[3], nb_filter[3]) - - self.conv0_2 = VGGBlock(nb_filter[0]*2+nb_filter[1], nb_filter[0], nb_filter[0]) - self.conv1_2 = VGGBlock(nb_filter[1]*2+nb_filter[2], nb_filter[1], nb_filter[1]) - self.conv2_2 = VGGBlock(nb_filter[2]*2+nb_filter[3], nb_filter[2], nb_filter[2]) - - self.conv0_3 = VGGBlock(nb_filter[0]*3+nb_filter[1], nb_filter[0], nb_filter[0]) - self.conv1_3 = VGGBlock(nb_filter[1]*3+nb_filter[2], nb_filter[1], nb_filter[1]) - - self.conv0_4 = VGGBlock(nb_filter[0]*4+nb_filter[1], nb_filter[0], nb_filter[0]) - - if self.deep_supervision: - self.final1 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1) - self.final2 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1) - self.final3 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1) - self.final4 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1) - else: - self.final = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1) - - - def forward(self, input): - x0_0 = self.conv0_0(input) - x1_0 = self.conv1_0(self.pool(x0_0)) - x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1)) - - x2_0 = self.conv2_0(self.pool(x1_0)) - x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1)) - x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1)) - - x3_0 = self.conv3_0(self.pool(x2_0)) - x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1)) - x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1)) - x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1)) - - x4_0 = self.conv4_0(self.pool(x3_0)) - x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1)) - x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1)) - x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1)) - x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1)) - - if self.deep_supervision: - output1 = self.final1(x0_1) - output2 = self.final2(x0_2) - output3 = self.final3(x0_3) - output4 = self.final4(x0_4) - return [output1, output2, output3, output4] - else: - output = self.final(x0_4) - return output -``` -完整实现过程可参考[GitHub](https://github.com/4uiiurz1/pytorch-nested-unet/)开源代码。 - -以上仅对几个主要的语义分割网络模型进行介绍,从当年的FCN到如今的各种模型层出不穷,想要对所有的SOTA模型全部进行介绍已经不太可能。其他诸如ENet、DeconvNet、RefineNet、HRNet、PixelNet、BiSeNet、UpperNet等网络模型,均各有千秋。本小节旨在让大家熟悉语义分割的主要模型结构和设计。深度学习和计算机视觉发展日新月异,一个新的SOTA模型出来,肯定很快就会被更新的结构设计所代替,重点是我们要了解语义分割的发展脉络,对主流的前沿研究能够保持一定的关注。 - -## 5. 语义分割训练Tips -PyTorch是一款极为便利的深度学习框架。在日常实验过程中,我们要多积累和总结,假以时日,人人都能总结出一套自己的高效模型搭建和训练套路。这一节我们给出一些惯用的PyTorch代码搭建方式,以及语义分割训练过程中的可视化方法,方便大家在训练过程中能够直观的看到训练效果。 - -### 5.1 PyTorch代码搭建方式 -无论是分类、检测还是分割抑或是其他非视觉的深度学习任务,其代码套路相对来说较为固定,不会跳出基本的代码框架。一个深度学习的实现代码框架无非就是以下五个主要构成部分: -- 数据:Data -- 模型:Model -- 判断:Criterion -- 优化:Optimizer -- 日志:Logger - -所以一个基本的顺序实现范式如下: -```Python -# data -dataset = VOC()||COCO()||ADE20K() -data_loader = data.DataLoader(dataSet) - -# model -model = ... -model_parallel = torch.nn.DataParallel(model) - -# Criterion -loss = criterion(...) - -# Optimizer -optimer = optim.SGD(...) - -# Logger and Visulization -visdom = ... -tensorboard = ... -textlog = ... - -# Model Parameters -data_size, batch_size, epoch_size, iterations = ..., ... -``` -不论是哪种深度学习任务,一般都免不了以上五项基本模块。所以一个简单的、相对完整的PyTorch模型项目代码应该是如下结构的: -``` -|-- semantic segmentation example - |-- dataset.py - |-- models - |-- unet.py - |-- deeplabv3.py - |-- pspnet.py - |-- ... - |-- _config.yml - |-- main.py - |-- utils - | |-- visual.py - | |-- loss.py - | |-- ... - |-- README.md - ... -``` -上面的示例代码结构中,我们把训练和验证相关代码都放到`main.py`文件中,但在实际实验中,这块的灵活性极大。一般来说,模型训练策略有三种,一种是边训练边验证最后再测试、另一种则是在训练中验证,将验证过程糅合到训练过程中,还有一种最简单,就是训练完了再单独验证和测试。所以,我们这里也可以单独定义对应的函数,训练`train()`、验证`val()`以及测试`test()`除此之外,还有一些辅助功能需要设计,包括打印训练信息`print()`、绘制损失函数`plot()`、保存最优模型`save()`,调整训练参数`update()`。 - -所以训练代码控制流程可以归纳为TVT+PPSU的模式。 - -### 5.2 可视化方法 -PyTorch原生的可视化支持模块是Visdom,当然鉴于TensorFlow的应用广泛性,PyTorch同时也支持TensorBoard的可视化方法。语义分割需要能够直观的看到训练效果,所以在训练过程中辅以一定的可视化方法是十分必要的。 - -#### Visdom -visdom是一款用于创建、组织和共享实时大量训练数据可视化的灵活工具。深度学习模型训练通常放在远程的服务器上,服务器上训练的一个问题就在于不能方便地对训练进行可视化,相较于TensorFlow的可视化工具TensorBoard,visdom则是对应于PyTorch的可视化工具。直接通过`pip install visdom`即可完成安装,之后在终端输入如下命令即可启动visdom服务: -``` -python -m visdom.server -``` -启动服务后输入本地或者远程地址,端口号8097,即可打开visdom主页。具体到深度学习训练时,我们可以在torch训练代码下插入visdom的可视化模块: -```Python -if args.steps_plot > 0 and step % args.steps_plot == 0: - image = inputs[0].cpu().data - vis.image(image,f'input (epoch: {epoch}, step: {step})') - vis.image(outputs[0].cpu().max(0)[1].data, f'output (epoch: {epoch}, step: {step})') - vis.image(targets[0].cpu().data, f'target (epoch: {epoch}, step: {step})') - vis.image(loss, f'loss (epoch: {epoch}, step: {step})') -``` - -visdom效果展示如下: -
- -
-
Fig25. visdom example
-
- -#### TensorBoard -很多TensorFlow用户更习惯于使用TensorBoard来进行训练的可视化展示。为了能让PyTorch用户也能用上TensorBoard,有开发者提供了PyTorch版本的TensorBoard,也就是tensorboardX。熟悉TensorBoard的用户可以无缝对接到tensorboardX,安装方式为: -``` -pip install tensorboardX -``` -除了要安装PyTorch之外,还需要安装TensorFlow。跟TensorBoard一样,tensorboardX也支持scalar, image, figure, histogram, audio, text, graph, onnx_graph, embedding, pr_curve,video等不同类型对象的可视化展示方式。tensorboardX和TensorBoard的启动方式一样,直接在终端下运行: -``` -tensorboard --logdir runs -``` -一个完整tensorboardX使用demo如下: -```Python -import torch -import torchvision.utils as vutils -import numpy as np -import torchvision.models as models -from torchvision import datasets -from tensorboardX import SummaryWriter - -resnet18 = models.resnet18(False) -writer = SummaryWriter() -sample_rate = 44100 -freqs = [262, 294, 330, 349, 392, 440, 440, 440, 440, 440, 440] - -for n_iter in range(100): - - dummy_s1 = torch.rand(1) - dummy_s2 = torch.rand(1) - # data grouping by `slash` - writer.add_scalar('data/scalar1', dummy_s1[0], n_iter) - writer.add_scalar('data/scalar2', dummy_s2[0], n_iter) - - writer.add_scalars('data/scalar_group', {'xsinx': n_iter * np.sin(n_iter), - 'xcosx': n_iter * np.cos(n_iter), - 'arctanx': np.arctan(n_iter)}, n_iter) - - dummy_img = torch.rand(32, 3, 64, 64) # output from network - if n_iter % 10 == 0: - x = vutils.make_grid(dummy_img, normalize=True, scale_each=True) - writer.add_image('Image', x, n_iter) - - dummy_audio = torch.zeros(sample_rate * 2) - for i in range(x.size(0)): - # amplitude of sound should in [-1, 1] - dummy_audio[i] = np.cos(freqs[n_iter // 10] * np.pi * float(i) / float(sample_rate)) - writer.add_audio('myAudio', dummy_audio, n_iter, sample_rate=sample_rate) - - writer.add_text('Text', 'text logged at step:' + str(n_iter), n_iter) - - for name, param in resnet18.named_parameters(): - writer.add_histogram(name, param.clone().cpu().data.numpy(), n_iter) - - # needs tensorboard 0.4RC or later - writer.add_pr_curve('xoxo', np.random.randint(2, size=100), np.random.rand(100), n_iter) - -dataset = datasets.MNIST('mnist', train=False, download=True) -images = dataset.test_data[:100].float() -label = dataset.test_labels[:100] - -features = images.view(100, 784) -writer.add_embedding(features, metadata=label, label_img=images.unsqueeze(1)) - -# export scalar data to JSON for external processing -writer.export_scalars_to_json("./all_scalars.json") -writer.close() -``` -tensorboardX的展示界面如图所示。 -
- -
-
Fig26. tensorboardX example
-
-
- - -## 参考文献 -1. [awesome-semantic-segmentation](https://github.com/mrgloom/awesome-semantic-segmentation) -2. Long J , Shelhamer E , Darrell T . Fully Convolutional Networks for Semantic Segmentation[J]. IEEE Transactions on Pattern Analysis and Machine Intelligence, 2015, 39(4):640-651. -3. Ronneberger O , Fischer P , Brox T . U-Net: Convolutional Networks for Biomedical Image Segmentation[J]. 2015. -4. Badrinarayanan V , Kendall A , Cipolla R . SegNet: A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation[J]. 2015. -5. Zhao H , Shi J , Qi X , et al. Pyramid Scene Parsing Network[J]. 2016. -6. Huang H , Lin L , Tong R , et al. UNet 3+: A Full-Scale Connected UNet for Medical Image Segmentation[J]. arXiv, 2020. -7. UNet++: A Nested U-Net Architecture for Medical Image Segmentation -8. https://github.com/4uiiurz1/pytorch-nested-unet -9. Minaee S , Boykov Y , Porikli F , et al. Image Segmentation Using Deep Learning: A Survey[J]. 2020. -10. Guo Y , Liu Y , Georgiou T , et al. A review of semantic segmentation using deep neural networks[J]. International Journal of Multimedia Information Retrieval, 2017. -11. https://github.com/fabioperez/pytorch-examples/ - - +# 《深度学习图像分割》配套代码库 + +> 本仓库是《深度学习图像分割》(louwill 著)一书的配套代码库。 +> 全书共 15 章,从传统分割算法、深度学习基础架构,到 2024–2026 年的基础模型 (SAM/SAM 2)、 +> 半监督/弱监督/自监督分割,再到医学影像与遥感的工程实战。 + +## 仓库结构 + +``` +Deep-Learning-Image-Segmentation/ +├── README.md ← 本文件 +├── code/ ← 按章组织的配套代码(本次更新新增) +│ ├── ch01/ … ch14/ ← 每章一个目录,含 README + inventory +│ └── ch15/ ← 总结章(无代码) +├── Scripts/ ← 早期 v1.0 版本的 C++ 传统算法实现 +│ ├── canny.cpp / otsu.cpp / watershed.cpp / region_growth.cpp +│ ├── mh.cpp (Marr-Hildreth) / grabcut.cpp +│ └── 与 code/ch02/cpp/ 内容重合,保留以兼容历史链接。 +└── 第1章 预备知识.pdf +``` + +## 各章代码索引 + +| 章 | 主题 | Python | C++ | 其他 | +| -- | ---- | ------ | --- | ---- | +| 01 | 预备知识 [ch01/](code/ch01/README.md) | 2 | 1 | — | +| 02 | 传统图像分割算法 [ch02/](code/ch02/README.md) | 0 | 5 | — | +| 03 | 深度学习图像分割基础 [ch03/](code/ch03/README.md) | 4 | 0 | — | +| 04 | FCN 与 U-Net 系列 [ch04/](code/ch04/README.md) | 8 | 0 | — | +| 05 | DeepLab 与编解码结构 [ch05/](code/ch05/README.md) | 2 | 0 | — | +| 06 | 注意力机制与 Transformer 分割 [ch06/](code/ch06/README.md) | 2 | 0 | — | +| 07 | 三维图像分割 [ch07/](code/ch07/README.md) | 1 | 0 | — | +| 08 | 实例分割与全景分割 [ch08/](code/ch08/README.md) | 0 | 0 | — | +| 09 | 基础模型与开放词表分割 [ch09/](code/ch09/README.md) | 0 | 0 | — | +| 10 | 半监督、弱监督与自监督分割 [ch10/](code/ch10/README.md) | 0 | 0 | — | +| 11 | 图像分割数据集与评价指标 [ch11/](code/ch11/README.md) | 0 | 0 | shell×1 | +| 12 | 图像分割工程实战 [ch12/](code/ch12/README.md) | 23 | 0 | yaml×1 / shell×3 | +| 13 | 医学影像分割项目实战 [ch13/](code/ch13/README.md) | 15 | 2 | CMake×2 / shell×3 | +| 14 | 遥感与工业图像分割项目实战 [ch14/](code/ch14/README.md) | 11 | 0 | shell×2 | +| 15 | 全书总结与展望 [ch15/](code/ch15/README.md) | 0 | 0 | — | + +## 使用方法 + +1. 按章 `cd code/chXX/`,阅读该章 `README.md` 了解依赖与文件状态。 +2. 状态标注: + - **可独立运行**:单文件可经 `py_compile` + `pyflakes` 校验。 + - **教学片段**:依赖书稿上下文(已 import 的模块、上文已定义的类)。 + - **语法骨架**:原稿采用 Ellipsis 占位或 module-top-level `return` 的教学截断写法, + 本仓库**忠于书稿、不做修改**,仅在 README 中标注。 +3. 对应章节 `inventory.md` 中按源稿行号列出每个 fenced block,便于回查原文。 + +## 抽取与验证流水线 + +代码并非手工誊写,而是从 v3/v4 修订稿中以脚本自动抽取,保证与书稿同步: + +``` +book-review/drafts/chXX-vN.md ← 修订稿源 + │ + │ book-review/scripts/extract_code.py + ▼ +code/chXX/{*.py, cpp/*.cpp, scripts/*.sh, configs/*.yaml} + │ + │ book-review/scripts/validate_code.py + │ ├─ python -m py_compile + │ └─ python -m pyflakes + ▼ +book-review/scripts/validate_report.md +``` + +## 已知限制 + +- **C++ 部分**:本地 CI 未安装 OpenCV/LibTorch,因此 ch02/ch13 的 cpp 文件仅做了静态导出, + 未做 `g++ -fsyntax-only` 通过验证。其内容与源稿逐字一致,编译/运行验证需读者本地具备 SDK。 +- **教学片段**:书稿的代码主要服务于讲解,不少是依赖上下文的片段(snippet),并非 standalone script。每章 README 显式区分。 +- **大模型权重**:ch12–ch14 的训练 / 推理 / SAM 集成示例需读者自行下载权重, + 本仓库不包含权重文件。 + +## 配合书稿使用 + +代码块在书稿中的标号形如「代码 X-Y」。本仓库文件命名优先匹配该标号: + +- `code_X-Y.py` / `code_X-Y.cpp` — 书稿明确编号的代码示例。 +- `snippet_NN.py` 等 — 书稿正文中没有独立编号的内联示例(按章内序号排)。 + +每章 `inventory.md` 列出每个 fenced block 的原稿行号,便于精确对应。 + +## 早期版本兼容 + +`Scripts/` 目录是仓库 v1.0 时期的 C++ 实现,主要对应第 2 章(传统分割算法)。 +新版按章重组后,等价代码在 `code/ch02/cpp/` 下。保留 `Scripts/` 以兼容历史链接。 + +--- + +若发现代码与书稿正文不一致,请优先以书稿修订稿(`book-review/drafts/chXX-vN.md`)为准; +并欢迎通过 issue / PR 反馈勘误。 diff --git a/code/ch01/README.md b/code/ch01/README.md new file mode 100644 index 0000000..4541343 --- /dev/null +++ b/code/ch01/README.md @@ -0,0 +1,32 @@ +# 第 1 章 — 预备知识 · 配套代码 + +> 抽取自《深度学习图像分割》第 1 章修订稿 `book-review/drafts/ch01-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +Python/C++ 开发环境配置与 OpenCV/PyTorch 基础调用示例。 + +## 依赖 + +- python>=3.10 +- torch>=2.0 +- torchvision + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_1-1.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_02.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `cpp/snippet_01.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch01/code_1-1.py b/code/ch01/code_1-1.py new file mode 100644 index 0000000..888ba63 --- /dev/null +++ b/code/ch01/code_1-1.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 1 章 — 代码 1-1 对应的Python版本写法如代码1-2所示。 +# 原始位置:book-review/drafts/ch01-v3.md:234 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 opencv 库 +import cv2 +import sys + +# 读取图片 +img = cv2.imread("example.jpg") +if img is None: + sys.exit("Could not read the image.") + +# 显示图片 +cv2.imshow("Display window", img) + +# 保存图片为 png 格式 +cv2.imwrite("example.png", img) diff --git a/code/ch01/cpp/snippet_01.cpp b/code/ch01/cpp/snippet_01.cpp new file mode 100644 index 0000000..61c5f80 --- /dev/null +++ b/code/ch01/cpp/snippet_01.cpp @@ -0,0 +1,29 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 1 章 — (无标号片段) +// 原始位置:book-review/drafts/ch01-v3.md:203 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +// 包含 OpenCV 的头文件 +#include +#include + +int main(int argc, char** argv) +{ + // 读取图像 + cv::Mat img = cv::imread("example.jpg"); + // 检查图像是否加载成功 + if (img.empty()) + { + std::cout << "图像加载失败,请检查路径。" << std::endl; + return -1; + } + // 创建窗口 + cv::namedWindow("显示图像", cv::WINDOW_AUTOSIZE); + // 在窗口中展示图像 + cv::imshow("显示图像", img); + // 等待用户按下任意键后关闭窗口 + cv::waitKey(0); + // 另存为 PNG 格式 + cv::imwrite("example.png", img); + return 0; +} diff --git a/code/ch01/inventory.md b/code/ch01/inventory.md new file mode 100644 index 0000000..98360ef --- /dev/null +++ b/code/ch01/inventory.md @@ -0,0 +1,12 @@ +# 第 1 章代码清单 + +- 源稿:`book-review/drafts/ch01-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 203–228 | cpp | `code/ch01/cpp/snippet_01.cpp` | — | +| 2 | 234–249 | python | `code/ch01/code_1-1.py` | 代码 1-1 | +| 3 | 265–296 | python | `code/ch01/snippet_02.py` | — | diff --git a/code/ch01/snippet_02.py b/code/ch01/snippet_02.py new file mode 100644 index 0000000..75d53da --- /dev/null +++ b/code/ch01/snippet_02.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 1 章 — (无标号片段) +# 原始位置:book-review/drafts/ch01-v3.md:265 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 torch.nn 相关模块 +import torch.nn as nn +import torch.nn.functional as F + +# 搭建一个 LeNet5 +class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + # 卷积层 1 + self.conv1 = nn.Conv2d(3, 6, 5) + # 池化层 + self.pool = nn.MaxPool2d(2, 2) + # 卷积层 2 + self.conv2 = nn.Conv2d(6, 16, 5) + # 全连接层 1 + self.fc1 = nn.Linear(16 * 5 * 5, 120) + # 全连接层 2 + self.fc2 = nn.Linear(120, 84) + # 全连接层 3 + self.fc3 = nn.Linear(84, 10) + + # 定义网络的前向推理流程 + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + x = self.fc3(x) + return x diff --git a/code/ch02/README.md b/code/ch02/README.md new file mode 100644 index 0000000..55b75e3 --- /dev/null +++ b/code/ch02/README.md @@ -0,0 +1,33 @@ +# 第 2 章 — 传统图像分割算法 · 配套代码 + +> 抽取自《深度学习图像分割》第 2 章修订稿 `book-review/drafts/ch02-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +传统图像分割算法的 C++ + OpenCV 实现:Canny 边缘检测、大津阈值、区域生长、形态学分水岭、GrabCut。 + +## 依赖 + +- OpenCV 4.x (C++) +- CMake >= 3.16 + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `cpp/code_2-1.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/code_2-2.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/code_2-3.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/code_2-4.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/code_2-5.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch02/cpp/code_2-1.cpp b/code/ch02/cpp/code_2-1.cpp new file mode 100644 index 0000000..2daabac --- /dev/null +++ b/code/ch02/cpp/code_2-1.cpp @@ -0,0 +1,36 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 2 章 — 代码 2-1 基于Canny算子的边缘检测算法 +// 原始位置:book-review/drafts/ch02-v3.md:262 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +#include +#include + +int main() { + // 读取图像:将图像读取为灰度图 + Mat img = imread("./example.png", IMREAD_GRAYSCALE); + if (img.empty()) { + cout << "Failed to read image." << endl; + return -1; + } + + // 第一步: 高斯平滑 + Mat blurred; + // 高斯模糊,去除噪声 + GaussianBlur(img, blurred, Size(5, 5), 1.0); + + // 第二步: 使用Canny算子进行边缘检测 + Mat edges; + double lower_threshold = 50; // 设置Canny算子的低阈值 + double upper_threshold = 150; // 设置Canny算子的高阈值 + Canny(blurred, edges, lower_threshold, upper_threshold); + + // 第三步: 保存分割结果 + imwrite("./canny_edges.png", edges); + + // 显示结果 + namedWindow("Canny Edges", WINDOW_NORMAL); + imshow("Canny Edges", edges); + waitKey(0); + return 0; +} diff --git a/code/ch02/cpp/code_2-2.cpp b/code/ch02/cpp/code_2-2.cpp new file mode 100644 index 0000000..3d2d6d5 --- /dev/null +++ b/code/ch02/cpp/code_2-2.cpp @@ -0,0 +1,41 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 2 章 — 代码 2-2 大津法阈值分割 +// 原始位置:book-review/drafts/ch02-v3.md:379 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +#include +#include + +using namespace cv; +using namespace std; + +int main() { + // 读取图像(灰度模式) + Mat image = imread("otsu.png", cv::IMREAD_GRAYSCALE); + if (image.empty()) { + cerr << "无法打开图像文件!" << endl; + return -1; + } + + // 创建保存大津法结果的矩阵 + Mat otsu_result; + + // 使用大津法进行图像二值化,返回阈值 + double otsu_thresh_val = threshold(image, otsu_result, 0, + 255, THRESH_BINARY + THRESH_OTSU); + + // 输出自动计算的大津阈值 + cout << "Otsu's Threshold Value: " << otsu_thresh_val << endl; + + // 显示原始图像和分割结果 + imshow("Original Image", image); + imshow("Otsu Threshold Image", otsu_result); + + // 保存分割结果 + imwrite("./otsu_result.png", otsu_result); + cout << "Otsu分割结果已保存为otsu_result.png" << endl; + + // 等待用户按键关闭窗口 + waitKey(0); + return 0; +} diff --git a/code/ch02/cpp/code_2-3.cpp b/code/ch02/cpp/code_2-3.cpp new file mode 100644 index 0000000..932c193 --- /dev/null +++ b/code/ch02/cpp/code_2-3.cpp @@ -0,0 +1,60 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 2 章 — 代码 2-3 区域生长算法 +// 原始位置:book-review/drafts/ch02-v3.md:480 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +#include +#include +#include + +using namespace std; +using namespace cv; + +// 定义区域生长分割算法 +void regionGrowing(const Mat& src, Mat& dst, Point seed, int threshold) { + // 图像尺寸 + int rows = src.rows; + int cols = src.cols; + + // 初始化输出图像,所有像素初始为0(黑色) + dst = Mat::zeros(src.size(), CV_8UC1); + + // 定义8邻域 + int dx[8] = { -1, 1, 0, 0, -1, -1, 1, 1 }; + int dy[8] = { 0, 0, -1, 1, -1, 1, -1, 1 }; + + // 获取种子点的灰度值 + int seedGrayValue = src.at(seed); + + // 创建一个队列用于存储待处理的点 + queue pointQueue; + pointQueue.push(Point(seed.x, seed.y)); + + // 将种子点标记为已处理 + dst.at(seed) = 255; // 标记区域为白色 + + // 开始区域生长 + while (!pointQueue.empty()) { + Point currentPoint = pointQueue.front(); + pointQueue.pop(); + + // 遍历当前点的8邻域 + for (int i = 0; i < 8; i++) { + int newX = currentPoint.x + dx[i]; + int newY = currentPoint.y + dy[i]; + + // 确保邻域点在图像范围内 + if (newX >= 0 && newX < cols && newY >= 0 && newY < rows) { + // 如果该点还没有被标记并且与种子点的灰度差小于阈值 + int neighborGrayValue = src.at(newY, newX); + if (dst.at(newY, newX) == 0 && + abs(neighborGrayValue - seedGrayValue) <= threshold) { + // 将该点加入到队列中 + pointQueue.push(Point(newX, newY)); + // 标记该点为区域的一部分 + dst.at(newY, newX) = 255; + } + } + } + } +} diff --git a/code/ch02/cpp/code_2-4.cpp b/code/ch02/cpp/code_2-4.cpp new file mode 100644 index 0000000..06b3609 --- /dev/null +++ b/code/ch02/cpp/code_2-4.cpp @@ -0,0 +1,89 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 2 章 — 代码 2-4 形态学分水岭算法 +// 原始位置:book-review/drafts/ch02-v3.md:582 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +#include +#include + +using namespace cv; +using namespace std; + +int main(int argc, char** argv) { + // 读取输入图像 + Mat image = imread("example.png"); + if (image.empty()) { + cout << "Could not open or find the image!" << endl; + return -1; + } + + // 转换为灰度图像 + Mat gray; + cvtColor(image, gray, COLOR_BGR2GRAY); + + // 高斯模糊去噪 + Mat blurred; + GaussianBlur(gray, blurred, Size(5, 5), 0); + + // 自适应阈值获取二值图像 + Mat binary; + threshold(blurred, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU); + + // 形态学开运算去噪 + Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3)); + Mat opening; + morphologyEx(binary, opening, MORPH_OPEN, kernel, Point(-1, -1), 2); + + // 距离变换并阈值化获取前景区域 + Mat distTransform; + distanceTransform(opening, distTransform, DIST_L2, 5); + normalize(distTransform, distTransform, 0, 1.0, NORM_MINMAX); + threshold(distTransform, distTransform, 0.7, 1.0, THRESH_BINARY); + Mat sureFg; + distTransform.convertTo(sureFg, CV_8U, 255); + + // 膨胀以获取背景区域 + Mat sureBg; + dilate(opening, sureBg, kernel, Point(-1, -1), 3); + + // 计算未知区域(背景减去前景) + Mat unknown; + subtract(sureBg, sureFg, unknown); + + // 连通组件标记 + Mat markers; + connectedComponents(sureFg, markers); + + // 将所有标记加1,以确保背景标记为1 + markers = markers + 1; + + // 将未知区域标记为0 + for (int i = 0; i < unknown.rows; i++) { + for (int j = 0; j < unknown.cols; j++) { + if (unknown.at(i, j) == 255) { + markers.at(i, j) = 0; + } + } + } + + // 应用分水岭算法 + watershed(image, markers); + + // 将分割结果转换为二值图像 + Mat binaryResult = Mat::zeros(markers.size(), CV_8U); + for (int i = 0; i < markers.rows; i++) { + for (int j = 0; j < markers.cols; j++) { + if (markers.at(i, j) > 1) { + // 将非背景区域设为白色 + binaryResult.at(i, j) = 255; + } + } + } + + // 显示灰度图像和二值分割结果 + imshow("Grayscale Image", gray); + imshow("Binary Segmentation Result (Watershed)", binaryResult); + + waitKey(0); + return 0; +} diff --git a/code/ch02/cpp/code_2-5.cpp b/code/ch02/cpp/code_2-5.cpp new file mode 100644 index 0000000..5219d40 --- /dev/null +++ b/code/ch02/cpp/code_2-5.cpp @@ -0,0 +1,41 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 2 章 — 代码 2-5 GrabCut分割代码示例 +// 原始位置:book-review/drafts/ch02-v3.md:734 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +#include +#include + +using namespace cv; +using namespace std; + +int main() { + // 读取图像 + Mat image = imread("./example.png"); + if (image.empty()) { + cout << "Could not open or find the image!" << endl; + return -1; + } + + // 定义一个矩形框选区域(用于初始前景) + // 注意:这里的Rect参数需要根据实际图像内容调整 + Rect rectangle(50, 100, 350, 330); + + // 创建掩码、前景模型和背景模型 + Mat mask, bgdModel, fgdModel; + + // 使用GrabCut算法分割图像 + grabCut(image, mask, rectangle, bgdModel, fgdModel, 5, GC_INIT_WITH_RECT); + + // 提取前景区域 + compare(mask, GC_PR_FGD, mask, CMP_EQ); + Mat foreground(image.size(), CV_8UC3, Scalar(255, 255, 255)); + image.copyTo(foreground, mask); + + // 显示结果 + imshow("Original Image", image); + imshow("Binary Foreground (GrabCut Result)", foreground); + + waitKey(0); + return 0; +} diff --git a/code/ch02/inventory.md b/code/ch02/inventory.md new file mode 100644 index 0000000..1caa4cf --- /dev/null +++ b/code/ch02/inventory.md @@ -0,0 +1,37 @@ +# 第 2 章代码清单 + +- 源稿:`book-review/drafts/ch02-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 68–72 | math | `—` | — | +| 2 | 76–80 | math | `—` | — | +| 3 | 82–86 | math | `—` | — | +| 4 | 90–97 | math | `—` | — | +| 5 | 99–105 | math | `—` | — | +| 6 | 112–119 | math | `—` | — | +| 7 | 121–128 | math | `—` | — | +| 8 | 133–141 | math | `—` | — | +| 9 | 143–151 | math | `—` | — | +| 10 | 155–163 | math | `—` | — | +| 11 | 165–173 | math | `—` | — | +| 12 | 187–191 | math | `—` | — | +| 13 | 197–201 | math | `—` | — | +| 14 | 222–226 | math | `—` | — | +| 15 | 228–232 | math | `—` | — | +| 16 | 234–238 | math | `—` | — | +| 17 | 240–244 | math | `—` | — | +| 18 | 262–294 | cpp | `code/ch02/cpp/code_2-1.cpp` | 代码 2-1 | +| 19 | 333–337 | math | `—` | — | +| 20 | 339–343 | math | `—` | — | +| 21 | 347–351 | math | `—` | — | +| 22 | 355–359 | math | `—` | — | +| 23 | 363–367 | math | `—` | — | +| 24 | 379–416 | cpp | `code/ch02/cpp/code_2-2.cpp` | 代码 2-2 | +| 25 | 480–536 | cpp | `code/ch02/cpp/code_2-3.cpp` | 代码 2-3 | +| 26 | 582–667 | cpp | `code/ch02/cpp/code_2-4.cpp` | 代码 2-4 | +| 27 | 685–689 | math | `—` | — | +| 28 | 734–771 | cpp | `code/ch02/cpp/code_2-5.cpp` | 代码 2-5 | diff --git a/code/ch03/README.md b/code/ch03/README.md new file mode 100644 index 0000000..45737a8 --- /dev/null +++ b/code/ch03/README.md @@ -0,0 +1,33 @@ +# 第 3 章 — 深度学习图像分割基础 · 配套代码 + +> 抽取自《深度学习图像分割》第 3 章修订稿 `book-review/drafts/ch03-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +分割网络的基础组件示例:SegNet 编码器 (VGG-16)、ViT 构建、ResNet 残差块、深监督。 + +## 依赖 + +- torch>=2.0 +- torchvision +- einops (ch03/code_3-3) + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_3-1.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `code_3-3.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `code_3-4.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `code_3-5.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch03/code_3-1.py b/code/ch03/code_3-1.py new file mode 100644 index 0000000..84e1666 --- /dev/null +++ b/code/ch03/code_3-1.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 3 章 — 代码 3-1 SegNet VGG16 编码器 +# 原始位置:book-review/drafts/ch03-v3.md:102 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 torchvision 的预训练模型模块 +from torchvision import models +import torch.nn as nn + +# 定义 SegNet 类(仅包含编码器部分) +class SegNet(nn.Module): + def __init__(self, classes): + super().__init__() + # 获取 VGG16 训练权重模型结构 + vgg16 = models.vgg16(pretrained=True) + # 获取 VGG16 网络层特征 + features = vgg16.features + # 1-4 层作为第一个编码块 + self.enc1 = features[0:4] + # 5-8 层作为第二个编码块 + self.enc2 = features[5:9] + # 9-15 层作为第三个编码块 + self.enc3 = features[10:16] + # 16-22 层作为第四个编码块 + self.enc4 = features[17:23] + # 23 到最后一层作为第五个编码块 + self.enc5 = features[24:] # ⚠️ EDIT: 原为 features[24:-1],会跳过最后的 MaxPool2d,与 SegNet 设计不一致 diff --git a/code/ch03/code_3-3.py b/code/ch03/code_3-3.py new file mode 100644 index 0000000..70b972b --- /dev/null +++ b/code/ch03/code_3-3.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 3 章 — 代码 3-3 ViT 的构建过程 +# 原始位置:book-review/drafts/ch03-v3.md:152 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 构建一个 ViT 类 +class ViT(nn.Module): + def __init__(self, *, image_size, patch_size, num_classes, dim, depth, + heads, mlp_dim, pool='cls', channels=3, dim_head=64, + dropout=0., emb_dropout=0.): + super().__init__() + image_height, image_width = pair(image_size) + patch_height, patch_width = pair(patch_size) + # patch 数量 + num_patches = (image_height // patch_height) * (image_width // patch_width) + # patch 维度 + patch_dim = channels * patch_height * patch_width + # 定义块嵌入 + self.to_patch_embedding = nn.Sequential( + Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', + p1=patch_height, p2=patch_width), + nn.Linear(patch_dim, dim), + ) + # 定义位置编码 + self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim)) + # 定义类别向量 + self.cls_token = nn.Parameter(torch.randn(1, 1, dim)) + self.dropout = nn.Dropout(emb_dropout) + self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim, dropout) + self.pool = pool + self.to_latent = nn.Identity() + # 定义 MLP + self.mlp_head = nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, num_classes) + ) + + # ViT 前向流程 + def forward(self, img): + # 块嵌入 + x = self.to_patch_embedding(img) + b, n, _ = x.shape + # 追加类别向量 + cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b) + x = torch.cat((cls_tokens, x), dim=1) + # 追加位置编码 + x += self.pos_embedding[:, :(n + 1)] + # dropout + x = self.dropout(x) + # 输入到 transformer + x = self.transformer(x) + x = x.mean(dim=1) if self.pool == 'mean' else x[:, 0] + x = self.to_latent(x) + # MLP + return self.mlp_head(x) diff --git a/code/ch03/code_3-4.py b/code/ch03/code_3-4.py new file mode 100644 index 0000000..76ef3a4 --- /dev/null +++ b/code/ch03/code_3-4.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 3 章 — 代码 3-4 残差块 +# 原始位置:book-review/drafts/ch03-v3.md:358 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 torch 相关模块 +import torch +from torch import nn +import torch.nn.functional as F + +# 定义残差块类 +class ResidualBlock(nn.Module): + def __init__(self, in_channels, out_channels, + stride=[1, 1], downsample=None): + super(ResidualBlock, self).__init__() + self.conv1 = nn.Conv2d( + in_channels, out_channels, kernel_size=3, stride=stride[0], + padding=1, bias=False + ) + self.conv2 = nn.Conv2d( + out_channels, out_channels, kernel_size=3, + stride=stride[1], + padding=1, bias=False + ) + self.bn = nn.BatchNorm2d(out_channels) + + # 定义残差块的前向计算流程 + def forward(self, x): + residual = x + out = F.relu(self.bn(self.conv1(x))) + out = self.bn(self.conv2(out)) + # 残差连接 + out = out + residual + out = F.relu(out) + return out diff --git a/code/ch03/code_3-5.py b/code/ch03/code_3-5.py new file mode 100644 index 0000000..0569dad --- /dev/null +++ b/code/ch03/code_3-5.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 3 章 — 代码 3-5 深度监督方法示例 +# 原始位置:book-review/drafts/ch03-v3.md:483 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 定义一个深度监督类 +class DeepSup(nn.Module): + def __init__(self, num_class=100, fc_dim=2048, use_softmax=False): + super(DeepSup, self).__init__() + self.use_softmax = use_softmax + self.cbr = conv3x3_bn_relu(fc_dim, fc_dim // 4, 1) + # 定义深监督函数,由卷积、BN 和 ReLU 激活构成 + self.cbr_deepsup = conv3x3_bn_relu(fc_dim // 2, + fc_dim // 4, 1) + # 最后一层卷积 + self.conv_last = nn.Conv2d(fc_dim // 4, num_class, 1, 1, 0) + self.conv_last_deepsup = nn.Conv2d(fc_dim // 4, + num_class, 1, 1, 0) + + # 前向计算流程 + def forward(self, conv_out, segSize=None): + conv5 = conv_out[-1] + x = self.cbr(conv5) + x = self.conv_last(x) + # 深度监督模块 + conv4 = conv_out[-2] + dsv = self.cbr_deepsup(conv4) + dsv = self.conv_last_deepsup(dsv) + # 主干卷积网络 softmax 输出 + x = nn.functional.log_softmax(x, dim=1) + # 深监督分支网络 softmax 输出 + dsv = nn.functional.log_softmax(dsv, dim=1) + return (x, dsv) diff --git a/code/ch03/inventory.md b/code/ch03/inventory.md new file mode 100644 index 0000000..2676d9b --- /dev/null +++ b/code/ch03/inventory.md @@ -0,0 +1,41 @@ +# 第 3 章代码清单 + +- 源稿:`book-review/drafts/ch03-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 102–125 | python | `code/ch03/code_3-1.py` | 代码 3-1 | +| 2 | 131–138 | text | `—` | 代码 3-2 | +| 3 | 152–203 | python | `code/ch03/code_3-3.py` | 代码 3-3 | +| 4 | 221–225 | math | `—` | — | +| 5 | 239–243 | math | `—` | — | +| 6 | 245–249 | math | `—` | — | +| 7 | 253–257 | math | `—` | — | +| 8 | 271–275 | math | `—` | — | +| 9 | 279–283 | math | `—` | — | +| 10 | 344–348 | math | `—` | — | +| 11 | 358–389 | python | `code/ch03/code_3-4.py` | 代码 3-4 | +| 12 | 395–399 | math | `—` | — | +| 13 | 483–512 | python | `code/ch03/code_3-5.py` | 代码 3-5 | +| 14 | 530–534 | math | `—` | — | +| 15 | 538–542 | math | `—` | — | +| 16 | 546–550 | math | `—` | — | +| 17 | 556–563 | math | `—` | — | +| 18 | 567–574 | math | `—` | — | +| 19 | 578–585 | math | `—` | — | +| 20 | 597–601 | math | `—` | — | +| 21 | 605–610 | math | `—` | — | +| 22 | 616–620 | math | `—` | — | +| 23 | 624–628 | math | `—` | — | +| 24 | 644–648 | math | `—` | — | +| 25 | 652–656 | math | `—` | — | +| 26 | 660–664 | math | `—` | — | +| 27 | 670–674 | math | `—` | — | +| 28 | 678–682 | math | `—` | — | +| 29 | 686–690 | math | `—` | — | +| 30 | 694–698 | math | `—` | — | +| 31 | 700–704 | math | `—` | — | +| 32 | 706–710 | math | `—` | — | diff --git a/code/ch04/README.md b/code/ch04/README.md new file mode 100644 index 0000000..8076177 --- /dev/null +++ b/code/ch04/README.md @@ -0,0 +1,36 @@ +# 第 4 章 — FCN 与 U-Net 系列 · 配套代码 + +> 抽取自《深度学习图像分割》第 4 章修订稿 `book-review/drafts/ch04-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +FCN-8s 与 U-Net 等典型编解码网络的 PyTorch 简易实现。 + +## 依赖 + +- torch>=2.0 +- torchvision + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_4-2.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `code_4-3.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_01.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_04.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `snippet_05.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_06.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_07.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_08.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch04/code_4-2.py b/code/ch04/code_4-2.py new file mode 100644 index 0000000..6c9c422 --- /dev/null +++ b/code/ch04/code_4-2.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — 代码 4-2 给出 FCN-8s 的一个 PyTorch 简略实现方式,便于读者加深对 FCN 的理解。代码中对于卷积下采样使用了 VGG-16 的预训练权重,分别构建了四个特征提取模块、一个卷积块和三个独立的卷积层。在前向传播流程中,将 conv7、pool3 和 pool4 进行融合,最后再做 8 倍的双线性插值上采样。 +# 原始位置:book-review/drafts/ch04-v3.md:135 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import models + + +class FCN8(nn.Module): + def __init__(self, num_classes): + super().__init__() + # 提取 VGG-16 预训练权重作为特征 + feats = list(models.vgg16(pretrained=True).features.children()) + # 取前 9 层为第一特征模块 + self.feat1 = nn.Sequential(*feats[0:9]) + # 第二特征模块 + self.feat2 = nn.Sequential(*feats[10:16]) + # 第三特征模块 + self.feat3 = nn.Sequential(*feats[17:23]) + # 第四特征模块 + self.feat4 = nn.Sequential(*feats[24:30]) + + # 卷积层权重不参与训练更新 + for m in self.modules(): + if isinstance(m, nn.Conv2d): + m.requires_grad = False + + self.conv_blocks = nn.Sequential( + nn.Conv2d(512, 4096, 7), nn.ReLU(inplace=True), nn.Dropout(), + nn.Conv2d(4096, 4096, 1), nn.ReLU(inplace=True), nn.Dropout(), + ) + + # 把最后三层全连接改为 1×1 卷积 + self.conv1 = nn.Conv2d(256, num_classes, 1) + self.conv2 = nn.Conv2d(512, num_classes, 1) + self.conv3 = nn.Conv2d(4096, num_classes, 1) + + def forward(self, x): + feat1 = self.feat1(x) + feat2 = self.feat2(feat1) + feat3 = self.feat3(feat2) + feat4 = self.feat4(feat3) + conv_blocks = self.conv_blocks(feat4) + + conv1 = self.conv1(feat2) + conv2 = self.conv2(feat3) + conv3 = self.conv3(conv_blocks) + + outputs = F.upsample_bilinear(conv_blocks, conv2.size()[2:]) + # 第一次融合 + outputs = outputs + conv2 + outputs = F.upsample_bilinear(outputs, conv1.size()[2:]) + # 第二次融合 + outputs = outputs + conv1 + return F.upsample_bilinear(outputs, x.size()[2:]) diff --git a/code/ch04/code_4-3.py b/code/ch04/code_4-3.py new file mode 100644 index 0000000..34412b4 --- /dev/null +++ b/code/ch04/code_4-3.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — 代码 4-3 给出了 U-Net 结构的一个简易实现版本。代码中先分别搭建了包含卷积和 ReLU 的编码块和解码块,然后在编解码块的基础上搭建完整的 U-Net 结构,在前向计算流程中补充同层跳跃连接。 +# 原始位置:book-review/drafts/ch04-v3.md:231 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +# 编码块 +class UNetEnc(nn.Module): + def __init__(self, in_channels, out_channels, dropout=False): + super().__init__() + layers = [ + nn.Conv2d(in_channels, out_channels, 3, dilation=2), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, 3, dilation=2), + nn.ReLU(inplace=True), + ] + if dropout: + layers += [nn.Dropout(.5)] + layers += [nn.MaxPool2d(2, stride=2, ceil_mode=True)] + self.down = nn.Sequential(*layers) + + def forward(self, x): + return self.down(x) + + +# 解码块 +class UNetDec(nn.Module): + def __init__(self, in_channels, features, out_channels): + super().__init__() + self.up = nn.Sequential( + nn.Conv2d(in_channels, features, 3), + nn.ReLU(inplace=True), + nn.Conv2d(features, features, 3), + nn.ReLU(inplace=True), + nn.ConvTranspose2d(features, out_channels, 2, stride=2), + nn.ReLU(inplace=True), + ) + + def forward(self, x): + return self.up(x) + + +# 基于编解码的 U-Net +class UNet(nn.Module): + def __init__(self, num_classes): + super().__init__() + # 四个编码块 + self.enc1 = UNetEnc(3, 64) + self.enc2 = UNetEnc(64, 128) + self.enc3 = UNetEnc(128, 256) + self.enc4 = UNetEnc(256, 512, dropout=True) + # U 形底部 + self.center = nn.Sequential( + nn.Conv2d(512, 1024, 3), nn.ReLU(inplace=True), + nn.Conv2d(1024, 1024, 3), nn.ReLU(inplace=True), + nn.Dropout(), + nn.ConvTranspose2d(1024, 512, 2, stride=2), + nn.ReLU(inplace=True), + ) + # 四个解码块 + self.dec4 = UNetDec(1024, 512, 256) + self.dec3 = UNetDec(512, 256, 128) + self.dec2 = UNetDec(256, 128, 64) + self.dec1 = nn.Sequential( + nn.Conv2d(128, 64, 3), nn.ReLU(inplace=True), + nn.Conv2d(64, 64, 3), nn.ReLU(inplace=True), + ) + self.final = nn.Conv2d(64, num_classes, 1) + + def forward(self, x): + enc1 = self.enc1(x) + enc2 = self.enc2(enc1) + enc3 = self.enc3(enc2) + enc4 = self.enc4(enc3) + center = self.center(enc4) + # 同层分辨率级联的解码块 + dec4 = self.dec4(torch.cat([center, F.upsample_bilinear(enc4, center.size()[2:])], 1)) + dec3 = self.dec3(torch.cat([dec4, F.upsample_bilinear(enc3, dec4.size()[2:])], 1)) + dec2 = self.dec2(torch.cat([dec3, F.upsample_bilinear(enc2, dec3.size()[2:])], 1)) + dec1 = self.dec1(torch.cat([dec2, F.upsample_bilinear(enc1, dec2.size()[2:])], 1)) + return F.upsample_bilinear(self.final(dec1), x.size()[2:]) diff --git a/code/ch04/inventory.md b/code/ch04/inventory.md new file mode 100644 index 0000000..b4841e6 --- /dev/null +++ b/code/ch04/inventory.md @@ -0,0 +1,20 @@ +# 第 4 章代码清单 + +- 源稿:`book-review/drafts/ch04-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 76–80 | math | `—` | — | +| 2 | 90–113 | python | `code/ch04/snippet_01.py` | — | +| 3 | 135–189 | python | `code/ch04/code_4-2.py` | 代码 4-2 | +| 4 | 231–312 | python | `code/ch04/code_4-3.py` | 代码 4-3 | +| 5 | 334–407 | python | `code/ch04/snippet_04.py` | — | +| 6 | 435–483 | python | `code/ch04/snippet_05.py` | — | +| 7 | 509–565 | python | `code/ch04/snippet_06.py` | — | +| 8 | 589–593 | math | `—` | — | +| 9 | 595–599 | math | `—` | — | +| 10 | 613–637 | python | `code/ch04/snippet_07.py` | — | +| 11 | 671–725 | python | `code/ch04/snippet_08.py` | — | diff --git a/code/ch04/snippet_01.py b/code/ch04/snippet_01.py new file mode 100644 index 0000000..7c91270 --- /dev/null +++ b/code/ch04/snippet_01.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:90 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +from torch import nn + +# 指定输入向量 +x = torch.rand(25088,) + +# 定义全连接层 +fc = nn.Linear(25088, 4096) + +# 定义卷积层 +conv2d = nn.Conv2d(512, 4096, 7) + +# 全连接层计算 +fc_output = fc(x) +print(fc_output.size()) # torch.Size([4096]) + +# 输入向量变换为 (B, C, H, W) +x = torch.reshape(x, (1, 512, 7, 7)) + +# 卷积层计算 +conv_output = conv2d(x) +print(conv_output.size()) # torch.Size([1, 4096, 1, 1]) diff --git a/code/ch04/snippet_04.py b/code/ch04/snippet_04.py new file mode 100644 index 0000000..f59857f --- /dev/null +++ b/code/ch04/snippet_04.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:334 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import models + + +class SegNetDec(nn.Module): + def __init__(self, in_channels, out_channels, num_layers): + super().__init__() + layers = [ + nn.Conv2d(in_channels, in_channels // 2, 3, padding=1), + nn.BatchNorm2d(in_channels // 2), + nn.ReLU(inplace=True), + ] + layers += [ + nn.Conv2d(in_channels // 2, in_channels // 2, 3, padding=1), + nn.BatchNorm2d(in_channels // 2), + nn.ReLU(inplace=True), + ] * num_layers + layers += [ + nn.Conv2d(in_channels // 2, out_channels, 3, padding=1), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + ] + self.decode = nn.Sequential(*layers) + + def forward(self, x): + return self.decode(x) + + +class SegNet(nn.Module): + def __init__(self, classes): + super().__init__() + vgg16 = models.vgg16(pretrained=True) + features = vgg16.features + self.enc1 = features[0:4] + self.enc2 = features[5:9] + self.enc3 = features[10:16] + self.enc4 = features[17:23] + self.enc5 = features[24:-1] + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + m.requires_grad = False + + self.dec5 = SegNetDec(512, 512, 1) + self.dec4 = SegNetDec(512, 256, 1) + self.dec3 = SegNetDec(256, 128, 1) + self.dec2 = SegNetDec(128, 64, 0) + self.final = nn.Sequential( + nn.Conv2d(64, classes, 3, padding=1), + nn.BatchNorm2d(classes), nn.ReLU(inplace=True), + ) + + def forward(self, x): + x1 = self.enc1(x) + e1, m1 = F.max_pool2d(x1, kernel_size=2, stride=2, return_indices=True) + x2 = self.enc2(e1) + e2, m2 = F.max_pool2d(x2, kernel_size=2, stride=2, return_indices=True) + x3 = self.enc3(e2) + e3, m3 = F.max_pool2d(x3, kernel_size=2, stride=2, return_indices=True) + x4 = self.enc4(e3) + e4, m4 = F.max_pool2d(x4, kernel_size=2, stride=2, return_indices=True) + x5 = self.enc5(e4) + e5, m5 = F.max_pool2d(x5, kernel_size=2, stride=2, return_indices=True) + + d5 = self.dec5(F.max_unpool2d(e5, m5, kernel_size=2, stride=2, output_size=x5.size())) + d4 = self.dec4(F.max_unpool2d(d5, m4, kernel_size=2, stride=2, output_size=x4.size())) + d3 = self.dec3(F.max_unpool2d(d4, m3, kernel_size=2, stride=2, output_size=x3.size())) + d2 = self.dec2(F.max_unpool2d(d3, m2, kernel_size=2, stride=2, output_size=x2.size())) + d1 = F.max_unpool2d(d2, m1, kernel_size=2, stride=2, output_size=x1.size()) + return self.final(d1) diff --git a/code/ch04/snippet_05.py b/code/ch04/snippet_05.py new file mode 100644 index 0000000..29927ea --- /dev/null +++ b/code/ch04/snippet_05.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:435 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class RefineNetBlock(nn.Module): + def __init__(self, in_channels, out_channels): + super(RefineNetBlock, self).__init__() + self.rcu1 = ResidualConvUnit(in_channels) + self.rcu2 = ResidualConvUnit(out_channels) + self.mrf = MultiResolutionFusion(out_channels, in_channels, out_channels) + self.crp = ChainedResidualPooling(out_channels) + self.output_conv = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1) + + def forward(self, *inputs): + x = self.mrf(*[self.rcu1(inp) for inp in inputs]) + x = self.crp(x) + return self.output_conv(self.rcu2(x)) + + +class RefineNet(nn.Module): + def __init__(self, num_classes): + super(RefineNet, self).__init__() + resnet = resnet50(pretrained=True) + self.layer1 = nn.Sequential(*list(resnet.children())[:5]) + self.layer2 = nn.Sequential(*list(resnet.children())[5]) + self.layer3 = nn.Sequential(*list(resnet.children())[6]) + self.layer4 = nn.Sequential(*list(resnet.children())[7]) + + self.refine3 = RefineNetBlock(1024, 256) + self.refine2 = RefineNetBlock(512, 256) + self.refine1 = RefineNetBlock(256, 256) + self.refine0 = RefineNetBlock(64, 256) + self.output_conv = nn.Conv2d(256, num_classes, kernel_size=1) + + def forward(self, x): + x0 = self.layer1(x) + x1 = self.layer2(x0) + x2 = self.layer3(x1) + x3 = self.layer4(x2) + + r3 = self.refine3(x3) + r2 = self.refine2(x2, r3) + r1 = self.refine1(x1, r2) + r0 = self.refine0(x0, r1) + out = self.output_conv(r0) + return F.interpolate(out, size=x.shape[2:], mode="bilinear", align_corners=False) diff --git a/code/ch04/snippet_06.py b/code/ch04/snippet_06.py new file mode 100644 index 0000000..4a01495 --- /dev/null +++ b/code/ch04/snippet_06.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:509 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +class Res_UNet(nn.Module): + def __init__(self, in_channel, out_channel, block, num_block): + super().__init__() + self.in_channel = in_channel + self.out_channel = out_channel + + self.conv1 = nn.Sequential( + nn.Conv2d(in_channel, 64, kernel_size=(7, 7), + stride=(2, 2), padding=3, bias=False), + nn.BatchNorm2d(64), nn.ReLU(inplace=True), + ) + self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + + self.conv2_x = self._make_layer(block, 64, num_block[0], 1) + self.conv3_x = self._make_layer(block, 128, num_block[1], 2) + self.conv4_x = self._make_layer(block, 256, num_block[2], 2) + self.conv5_x = self._make_layer(block, 512, num_block[3], 2) + + self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) + + self.dconv_up3 = double_conv(256 + 512, 256) + self.dconv_up2 = double_conv(128 + 256, 128) + self.dconv_up1 = double_conv(128 + 64, 64) + self.dconv_last = nn.Sequential( + nn.Conv2d(128, 64, (3, 3), padding=1), + nn.BatchNorm2d(64), nn.ReLU(inplace=True), + nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True), + nn.Conv2d(64, out_channel, (1, 1)), + ) + + def _make_layer(self, block, out_channels, num_blocks, stride): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append(block(self.in_channels, out_channels, stride)) + self.in_channels = out_channels * block.expansion + return nn.Sequential(*layers) + + def forward(self, x): + conv1 = self.conv1(x) + temp = self.maxpool(conv1) + conv2 = self.conv2_x(temp) + conv3 = self.conv3_x(conv2) + conv4 = self.conv4_x(conv3) + bottle = self.conv5_x(conv4) + + x = self.upsample(bottle) + x = torch.cat([x, conv4], dim=1); x = self.dconv_up3(x) + x = self.upsample(x) + x = torch.cat([x, conv3], dim=1); x = self.dconv_up2(x) + x = self.upsample(x) + x = torch.cat([x, conv2], dim=1); x = self.dconv_up1(x) + x = self.upsample(x) + x = torch.cat([x, conv1], dim=1) + return self.dconv_last(x) diff --git a/code/ch04/snippet_07.py b/code/ch04/snippet_07.py new file mode 100644 index 0000000..0c3a351 --- /dev/null +++ b/code/ch04/snippet_07.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:613 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +class Attention_block(nn.Module): + def __init__(self, F_g, F_l, F_int): + super(Attention_block, self).__init__() + self.W_g = nn.Sequential( + nn.Conv2d(F_g, F_int, kernel_size=1, stride=1, padding=0, bias=True), + nn.BatchNorm2d(F_int), + ) + self.W_x = nn.Sequential( + nn.Conv2d(F_l, F_int, kernel_size=1, stride=1, padding=0, bias=True), + nn.BatchNorm2d(F_int), + ) + self.psi = nn.Sequential( + nn.Conv2d(F_int, 1, kernel_size=1, stride=1, padding=0, bias=True), + nn.BatchNorm2d(1), nn.Sigmoid(), + ) + self.relu = nn.ReLU(inplace=True) + + def forward(self, g, x): + g1 = self.W_g(g) + x1 = self.W_x(x) + psi = self.relu(g1 + x1) + psi = self.psi(psi) + return x * psi diff --git a/code/ch04/snippet_08.py b/code/ch04/snippet_08.py new file mode 100644 index 0000000..e12722d --- /dev/null +++ b/code/ch04/snippet_08.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 4 章 — (无标号片段) +# 原始位置:book-review/drafts/ch04-v3.md:671 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class UNetPlusPlus(nn.Module): + def __init__(self, in_channels=3, out_channels=1): + super().__init__() # ✅ 修正 super 类名(原稿误写为 UNetPlusPlus) + # 编码器 + self.conv0_0 = ConvBlock(in_channels, 64) + self.conv1_0 = ConvBlock(64, 128) + self.conv2_0 = ConvBlock(128, 256) + self.conv3_0 = ConvBlock(256, 512) + self.conv4_0 = ConvBlock(512, 1024) + self.pool = nn.MaxPool2d(kernel_size=2, stride=2) + + # 嵌套卷积块 + self.conv0_1 = ConvBlock(64 + 128, 64) + self.conv1_1 = ConvBlock(128 + 256, 128) + self.conv2_1 = ConvBlock(256 + 512, 256) + self.conv3_1 = ConvBlock(512 + 1024, 512) + self.conv0_2 = ConvBlock(64 * 2 + 128, 64) + self.conv1_2 = ConvBlock(128 * 2 + 256, 128) + self.conv2_2 = ConvBlock(256 * 2 + 512, 256) + self.conv0_3 = ConvBlock(64 * 3 + 128, 64) + self.conv1_3 = ConvBlock(128 * 3 + 256, 128) + self.conv0_4 = ConvBlock(64 * 4 + 128, 64) + + self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) + self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1) + + def forward(self, x): + x0_0 = self.conv0_0(x) + x1_0 = self.conv1_0(self.pool(x0_0)) + x2_0 = self.conv2_0(self.pool(x1_0)) + x3_0 = self.conv3_0(self.pool(x2_0)) + x4_0 = self.conv4_0(self.pool(x3_0)) + + x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1)) + x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1)) + x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1)) + x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1)) + + x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1)) + x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1)) + x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1)) + + x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1)) + x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1)) + + x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1)) + + return self.final_conv(x0_4) diff --git a/code/ch05/README.md b/code/ch05/README.md new file mode 100644 index 0000000..5d018a0 --- /dev/null +++ b/code/ch05/README.md @@ -0,0 +1,29 @@ +# 第 5 章 — DeepLab 与编解码结构 · 配套代码 + +> 抽取自《深度学习图像分割》第 5 章修订稿 `book-review/drafts/ch05-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +PSPNet 中 PPM (Pyramid Pooling Module) 的实现示例。 + +## 依赖 + +- torch>=2.0 + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_5-1.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_02.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch05/code_5-1.py b/code/ch05/code_5-1.py new file mode 100644 index 0000000..2ec6d90 --- /dev/null +++ b/code/ch05/code_5-1.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 5 章 — 代码 5-1 给出了 PSPNet 网络结构中 PPM 的实现过程,PSPNet 完整实现可参考本书配套代码库。 +# 原始位置:book-review/drafts/ch05-v3.md:94 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class PyramidPoolingModule(nn.Module): + """ + 金字塔池化模块(PPM) + in_channels: 输入特征通道数 + pool_sizes: 金字塔池化窗口大小列表,例如 [1, 2, 3, 6] + out_channels: 每个池化分支的输出通道数 + """ + def __init__(self, in_channels, pool_sizes, out_channels): + super(PyramidPoolingModule, self).__init__() + self.stages = nn.ModuleList([ + nn.Sequential( + nn.AdaptiveAvgPool2d(pool_size), + nn.Conv2d(in_channels, out_channels, + kernel_size=1, bias=False), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + ) for pool_size in pool_sizes + ]) + + def forward(self, x): + input_size = x.size()[2:] + ppm_outs = [x] + for stage in self.stages: + pooled = stage(x) + upsampled = F.interpolate(pooled, size=input_size, + mode='bilinear', align_corners=False) + ppm_outs.append(upsampled) + return torch.cat(ppm_outs, dim=1) diff --git a/code/ch05/inventory.md b/code/ch05/inventory.md new file mode 100644 index 0000000..a68862c --- /dev/null +++ b/code/ch05/inventory.md @@ -0,0 +1,14 @@ +# 第 5 章代码清单 + +- 源稿:`book-review/drafts/ch05-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 94–128 | python | `code/ch05/code_5-1.py` | 代码 5-1 | +| 2 | 144–148 | math | `—` | — | +| 3 | 152–156 | math | `—` | — | +| 4 | 192–226 | python | `code/ch05/snippet_02.py` | — | +| 5 | 310–314 | math | `—` | — | diff --git a/code/ch05/snippet_02.py b/code/ch05/snippet_02.py new file mode 100644 index 0000000..8d2d9ee --- /dev/null +++ b/code/ch05/snippet_02.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 5 章 — (无标号片段) +# 原始位置:book-review/drafts/ch05-v3.md:192 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class ASPP(nn.Module): + def __init__(self, in_channels, out_channels): + super(ASPP, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + ) + self.conv2 = ASPPConv(in_channels, out_channels, dilation=6) + self.conv3 = ASPPConv(in_channels, out_channels, dilation=12) + self.conv4 = ASPPConv(in_channels, out_channels, dilation=18) + self.pool = ASPPPooling(in_channels, out_channels) + self.project = nn.Sequential( + nn.Conv2d(out_channels * 5, out_channels, kernel_size=1, bias=False), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + nn.Dropout(0.5), + ) + + def forward(self, x): + x1 = self.conv1(x) + x2 = self.conv2(x) + x3 = self.conv3(x) + x4 = self.conv4(x) + x5 = self.pool(x) + x = torch.cat([x1, x2, x3, x4, x5], dim=1) + x = self.project(x) + return x diff --git a/code/ch06/README.md b/code/ch06/README.md new file mode 100644 index 0000000..6ea0873 --- /dev/null +++ b/code/ch06/README.md @@ -0,0 +1,30 @@ +# 第 6 章 — 注意力机制与 Transformer 分割 · 配套代码 + +> 抽取自《深度学习图像分割》第 6 章修订稿 `book-review/drafts/ch06-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +注意力模块与 Transformer-based 分割的关键组件示例。 + +## 依赖 + +- torch>=2.0 +- einops + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `snippet_01.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_02.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch06/inventory.md b/code/ch06/inventory.md new file mode 100644 index 0000000..5bc0f72 --- /dev/null +++ b/code/ch06/inventory.md @@ -0,0 +1,28 @@ +# 第 6 章代码清单 + +- 源稿:`book-review/drafts/ch06-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 61–65 | math | `—` | — | +| 2 | 67–71 | math | `—` | — | +| 3 | 73–77 | math | `—` | — | +| 4 | 79–83 | math | `—` | — | +| 5 | 95–99 | math | `—` | — | +| 6 | 105–109 | math | `—` | — | +| 7 | 151–155 | math | `—` | — | +| 8 | 163–167 | math | `—` | — | +| 9 | 169–173 | math | `—` | — | +| 10 | 175–179 | math | `—` | — | +| 11 | 181–185 | math | `—` | — | +| 12 | 193–245 | python | `code/ch06/snippet_01.py` | — | +| 13 | 308–332 | python | `code/ch06/snippet_02.py` | — | +| 14 | 368–372 | math | `—` | — | +| 15 | 374–378 | math | `—` | — | +| 16 | 390–394 | math | `—` | — | +| 17 | 396–400 | math | `—` | — | +| 18 | 402–406 | math | `—` | — | +| 19 | 408–412 | math | `—` | — | diff --git a/code/ch06/snippet_01.py b/code/ch06/snippet_01.py new file mode 100644 index 0000000..7806f1d --- /dev/null +++ b/code/ch06/snippet_01.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 6 章 — (无标号片段) +# 原始位置:book-review/drafts/ch06-v3.md:193 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 完整实现可参考 lucidrains/vit-pytorch(https://github.com/lucidrains/vit-pytorch) +import torch +from torch import nn +from einops import repeat +from einops.layers.torch import Rearrange + + +def pair(x): + return (x, x) if isinstance(x, int) else x + + +class ViT(nn.Module): + def __init__(self, *, image_size, patch_size, num_classes, dim, depth, + heads, mlp_dim, pool='cls', channels=3, dim_head=64, + dropout=0., emb_dropout=0.): + super().__init__() + image_height, image_width = pair(image_size) + patch_height, patch_width = pair(patch_size) + + num_patches = (image_height // patch_height) * (image_width // patch_width) + patch_dim = channels * patch_height * patch_width + + self.to_patch_embedding = nn.Sequential( + Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', + p1=patch_height, p2=patch_width), + nn.Linear(patch_dim, dim), + ) + self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim)) + self.cls_token = nn.Parameter(torch.randn(1, 1, dim)) + self.dropout = nn.Dropout(emb_dropout) + + self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim, dropout) + self.pool = pool + self.to_latent = nn.Identity() + + self.mlp_head = nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, num_classes), + ) + + def forward(self, img): + x = self.to_patch_embedding(img) + b, n, _ = x.shape + cls_tokens = repeat(self.cls_token, '1 n d -> b n d', b=b) + x = torch.cat((cls_tokens, x), dim=1) + x += self.pos_embedding[:, :(n + 1)] + x = self.dropout(x) + x = self.transformer(x) + x = x.mean(dim=1) if self.pool == 'mean' else x[:, 0] + x = self.to_latent(x) + return self.mlp_head(x) diff --git a/code/ch06/snippet_02.py b/code/ch06/snippet_02.py new file mode 100644 index 0000000..41b8f14 --- /dev/null +++ b/code/ch06/snippet_02.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 6 章 — (无标号片段) +# 原始位置:book-review/drafts/ch06-v3.md:308 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +class TransUNet(nn.Module): + def __init__(self, config, img_size=224, num_classes=21843, + zero_head=False, vis=False): + super(TransUNet, self).__init__() + self.num_classes = num_classes + self.zero_head = zero_head + self.classifier = config.classifier + self.transformer = Transformer(config, img_size, vis) + self.decoder = DecoderCup(config) + self.segmentation_head = SegmentationHead( + in_channels=config['decoder_channels'][-1], + out_channels=config['n_classes'], + kernel_size=3, + ) + self.config = config + + def forward(self, x): + if x.size()[1] == 1: + x = x.repeat(1, 3, 1, 1) + x, attn_weights, features = self.transformer(x) + x = self.decoder(x, features) + logits = self.segmentation_head(x) + return logits diff --git a/code/ch07/README.md b/code/ch07/README.md new file mode 100644 index 0000000..7d6833a --- /dev/null +++ b/code/ch07/README.md @@ -0,0 +1,29 @@ +# 第 7 章 — 三维图像分割 · 配套代码 + +> 抽取自《深度学习图像分割》第 7 章修订稿 `book-review/drafts/ch07-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +3D 卷积最小示例(医学影像 3D 分割入门)。 + +## 依赖 + +- torch>=2.0 +- (3D 卷积示例) + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_7-1.py` | 可独立运行 | py_compile + pyflakes 通过 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch07/code_7-1.py b/code/ch07/code_7-1.py new file mode 100644 index 0000000..4fe0c6d --- /dev/null +++ b/code/ch07/code_7-1.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 7 章 — 代码 7-1 3D 卷积最简使用示例 +# 原始位置:book-review/drafts/ch07-v3.md:120 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn + +# 输入张量:batch=1, channels=4, D×H×W=64×64×64 +inputs = torch.randn(1, 4, 64, 64, 64) + +# 创建 3D 卷积层:输入通道 4,输出通道 12,卷积核 3×3×3 +conv3d = nn.Conv3d(in_channels=4, out_channels=12, kernel_size=3) + +# 对输入做卷积 +output = conv3d(inputs) +print(output.shape) +# 输出:torch.Size([1, 12, 62, 62, 62]) diff --git a/code/ch07/inventory.md b/code/ch07/inventory.md new file mode 100644 index 0000000..c531326 --- /dev/null +++ b/code/ch07/inventory.md @@ -0,0 +1,14 @@ +# 第 7 章代码清单 + +- 源稿:`book-review/drafts/ch07-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 120–134 | python | `code/ch07/code_7-1.py` | 代码 7-1 | +| 2 | 168–172 | math | `—` | — | +| 3 | 178–182 | math | `—` | — | +| 4 | 320–324 | math | `—` | — | +| 5 | 348–352 | math | `—` | — | diff --git a/code/ch08/README.md b/code/ch08/README.md new file mode 100644 index 0000000..2eb1855 --- /dev/null +++ b/code/ch08/README.md @@ -0,0 +1,22 @@ +# 第 8 章 — 实例分割与全景分割 · 配套代码 + +> 抽取自《深度学习图像分割》第 8 章修订稿 `book-review/drafts/ch08-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +本章为概念性章节(实例分割、全景分割综述),无独立代码示例;参考资料指向 Mask R-CNN、Mask2Former 等开源实现。 + +## 文件清单 + +(本章为概念性 / 综述章节,正文无独立可运行代码。可参考 `inventory.md` 中对源稿 fenced block 的盘点。) + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch08/inventory.md b/code/ch08/inventory.md new file mode 100644 index 0000000..b3e7292 --- /dev/null +++ b/code/ch08/inventory.md @@ -0,0 +1,12 @@ +# 第 8 章代码清单 + +- 源稿:`book-review/drafts/ch08-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 102–106 | math | `—` | — | +| 2 | 110–114 | math | `—` | — | +| 3 | 116–120 | math | `—` | — | diff --git a/code/ch09/README.md b/code/ch09/README.md new file mode 100644 index 0000000..0e6a626 --- /dev/null +++ b/code/ch09/README.md @@ -0,0 +1,22 @@ +# 第 9 章 — 基础模型与开放词表分割 · 配套代码 + +> 抽取自《深度学习图像分割》第 9 章修订稿 `book-review/drafts/ch09-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +本章为基础模型综述(SAM/SAM 2/SegGPT 等),无独立代码示例;参考各官方仓库。 + +## 文件清单 + +(本章为概念性 / 综述章节,正文无独立可运行代码。可参考 `inventory.md` 中对源稿 fenced block 的盘点。) + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch09/inventory.md b/code/ch09/inventory.md new file mode 100644 index 0000000..7da34f1 --- /dev/null +++ b/code/ch09/inventory.md @@ -0,0 +1,21 @@ +# 第 9 章代码清单 + +- 源稿:`book-review/drafts/ch09-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 279–283 | math | `—` | — | +| 2 | 285–289 | math | `—` | — | +| 3 | 316–320 | math | `—` | — | +| 4 | 322–326 | math | `—` | — | +| 5 | 328–332 | math | `—` | — | +| 6 | 388–392 | math | `—` | — | +| 7 | 394–398 | math | `—` | — | +| 8 | 400–404 | math | `—` | — | +| 9 | 412–416 | math | `—` | — | +| 10 | 422–426 | math | `—` | — | +| 11 | 428–432 | math | `—` | — | +| 12 | 436–440 | math | `—` | — | diff --git a/code/ch10/README.md b/code/ch10/README.md new file mode 100644 index 0000000..70c3451 --- /dev/null +++ b/code/ch10/README.md @@ -0,0 +1,22 @@ +# 第 10 章 — 半监督、弱监督与自监督分割 · 配套代码 + +> 抽取自《深度学习图像分割》第 10 章修订稿 `book-review/drafts/ch10-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +本章为半监督/弱监督/自监督方法论综述(UniMatch、CLIP-ES、DINOv2 等),无独立代码示例。 + +## 文件清单 + +(本章为概念性 / 综述章节,正文无独立可运行代码。可参考 `inventory.md` 中对源稿 fenced block 的盘点。) + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch10/inventory.md b/code/ch10/inventory.md new file mode 100644 index 0000000..5666c11 --- /dev/null +++ b/code/ch10/inventory.md @@ -0,0 +1,18 @@ +# 第 10 章代码清单 + +- 源稿:`book-review/drafts/ch10-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 104–108 | math | `—` | — | +| 2 | 121–125 | math | `—` | — | +| 3 | 137–141 | math | `—` | — | +| 4 | 143–147 | math | `—` | — | +| 5 | 149–153 | math | `—` | — | +| 6 | 203–207 | math | `—` | — | +| 7 | 209–213 | math | `—` | — | +| 8 | 235–239 | math | `—` | — | +| 9 | 390–394 | math | `—` | — | diff --git a/code/ch11/README.md b/code/ch11/README.md new file mode 100644 index 0000000..cfc1490 --- /dev/null +++ b/code/ch11/README.md @@ -0,0 +1,28 @@ +# 第 11 章 — 图像分割数据集与评价指标 · 配套代码 + +> 抽取自《深度学习图像分割》第 11 章修订稿 `book-review/drafts/ch11-v3.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +数据集下载脚本与数据清单(PASCAL VOC / Cityscapes / ADE20K / 医学/遥感数据)。 + +## 依赖 + +- wget / curl(数据集下载) + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `scripts/snippet_01.sh` | 脚本 | 下载 / 安装命令 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch11/inventory.md b/code/ch11/inventory.md new file mode 100644 index 0000000..732c75e --- /dev/null +++ b/code/ch11/inventory.md @@ -0,0 +1,12 @@ +# 第 11 章代码清单 + +- 源稿:`book-review/drafts/ch11-v3.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 325–327 | bash | `code/ch11/scripts/snippet_01.sh` | — | +| 2 | 331–337 | text | `—` | — | +| 3 | 361–365 | plain | `—` | — | diff --git a/code/ch11/scripts/snippet_01.sh b/code/ch11/scripts/snippet_01.sh new file mode 100644 index 0000000..9bfebcd --- /dev/null +++ b/code/ch11/scripts/snippet_01.sh @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 11 章 — (无标号片段) +# 原始位置:book-review/drafts/ch11-v3.md:325 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +for f in *.json; do labelme_json_to_dataset "$f"; done diff --git a/code/ch12/README.md b/code/ch12/README.md new file mode 100644 index 0000000..76b440b --- /dev/null +++ b/code/ch12/README.md @@ -0,0 +1,60 @@ +# 第 12 章 — 图像分割工程实战 · 配套代码 + +> 抽取自《深度学习图像分割》第 12 章修订稿 `book-review/drafts/ch12-v4.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +工程实战章——预处理、数据增强、Dataset 定义、训练循环、推理 API、Web 部署。书中 23 段 Python + 配套 yaml/shell。 + +## 依赖 + +- torch>=2.0 +- torchvision +- Pillow +- numpy +- visdom(可选) +- transformers(部分 SAM 集成示例) +- flask(API 部分) + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_12-6.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_01.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_02.py` | 语法骨架 | module-top-level 教学截断/占位符;不可独立运行 | +| `snippet_03.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_04.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_05.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_06.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_07.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_08.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_09.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_10.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `snippet_11.py` | 语法骨架 | module-top-level 教学截断/占位符;不可独立运行 | +| `snippet_12.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_13.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_14.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_15.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_16.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_17.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_18.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_19.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_20.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_21.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_23.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `configs/snippet_01.yaml` | 配置 | 训练 / 推理 yaml | +| `scripts/snippet_01.sh` | 脚本 | 下载 / 安装命令 | +| `scripts/snippet_02.sh` | 脚本 | 下载 / 安装命令 | +| `scripts/snippet_03.sh` | 脚本 | 下载 / 安装命令 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch12/code_12-6.py b/code/ch12/code_12-6.py new file mode 100644 index 0000000..c413a91 --- /dev/null +++ b/code/ch12/code_12-6.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — 代码 12-6 使用的 visdom 在 2024+ 已较为冷门,主流的训练可视化方案是: +# 原始位置:book-review/drafts/ch12-v4.md:984 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# TensorBoard 集成 +from torch.utils.tensorboard import SummaryWriter +writer = SummaryWriter(log_dir='runs/exp1') +writer.add_scalar('train/loss', np_loss, cur_itrs) +writer.add_scalar('val/mIoU', val_score['Mean IoU'], cur_itrs) +writer.add_image('val/sample', concat_img, cur_itrs) diff --git a/code/ch12/configs/snippet_01.yaml b/code/ch12/configs/snippet_01.yaml new file mode 100644 index 0000000..93c6ea1 --- /dev/null +++ b/code/ch12/configs/snippet_01.yaml @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:949 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# configs/config.yaml +defaults: + - dataset: voc + - model: deeplabv3plus + - optimizer: sgd +data_root: ./dataset +total_itrs: 30000 diff --git a/code/ch12/inventory.md b/code/ch12/inventory.md new file mode 100644 index 0000000..e844e7d --- /dev/null +++ b/code/ch12/inventory.md @@ -0,0 +1,36 @@ +# 第 12 章代码清单 + +- 源稿:`book-review/drafts/ch12-v4.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 73–91 | python | `code/ch12/snippet_01.py` | — | +| 2 | 99–117 | python | `code/ch12/snippet_02.py` | — | +| 3 | 123–180 | python | `code/ch12/snippet_03.py` | — | +| 4 | 188–191 | python | `code/ch12/snippet_04.py` | — | +| 5 | 195–198 | python | `code/ch12/snippet_05.py` | — | +| 6 | 212–232 | python | `code/ch12/snippet_06.py` | — | +| 7 | 242–301 | python | `code/ch12/snippet_07.py` | — | +| 8 | 311–345 | python | `code/ch12/snippet_08.py` | — | +| 9 | 353–368 | python | `code/ch12/snippet_09.py` | — | +| 10 | 376–412 | python | `code/ch12/snippet_10.py` | — | +| 11 | 422–513 | python | `code/ch12/snippet_11.py` | — | +| 12 | 548–572 | python | `code/ch12/snippet_12.py` | — | +| 13 | 582–612 | python | `code/ch12/snippet_13.py` | — | +| 14 | 616–618 | bash | `code/ch12/scripts/snippet_01.sh` | — | +| 15 | 638–647 | python | `code/ch12/snippet_14.py` | — | +| 16 | 657–724 | python | `code/ch12/snippet_15.py` | — | +| 17 | 728–730 | bash | `code/ch12/scripts/snippet_02.sh` | — | +| 18 | 734–740 | python | `code/ch12/snippet_16.py` | — | +| 19 | 754–833 | python | `code/ch12/snippet_17.py` | — | +| 20 | 878–896 | python | `code/ch12/snippet_18.py` | — | +| 21 | 904–922 | python | `code/ch12/snippet_19.py` | — | +| 22 | 932–941 | python | `code/ch12/snippet_20.py` | — | +| 23 | 949–957 | yaml | `code/ch12/configs/snippet_01.yaml` | — | +| 24 | 959–968 | python | `code/ch12/snippet_21.py` | — | +| 25 | 970–975 | bash | `code/ch12/scripts/snippet_03.sh` | — | +| 26 | 984–991 | python | `code/ch12/code_12-6.py` | 代码 12-6 | +| 27 | 1004–1022 | python | `code/ch12/snippet_23.py` | — | diff --git a/code/ch12/scripts/snippet_01.sh b/code/ch12/scripts/snippet_01.sh new file mode 100644 index 0000000..14d0c6d --- /dev/null +++ b/code/ch12/scripts/snippet_01.sh @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:616 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +python inference.py --data_root 2007_000676.jpg --model deeplabv3plus_resnet101 diff --git a/code/ch12/scripts/snippet_02.sh b/code/ch12/scripts/snippet_02.sh new file mode 100644 index 0000000..deb374e --- /dev/null +++ b/code/ch12/scripts/snippet_02.sh @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:728 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +python api.py diff --git a/code/ch12/scripts/snippet_03.sh b/code/ch12/scripts/snippet_03.sh new file mode 100644 index 0000000..64d1bc9 --- /dev/null +++ b/code/ch12/scripts/snippet_03.sh @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:970 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 命令行任意覆盖 +python main.py optimizer.lr=0.01 model.encoder=resnet50 +# 多任务扫描 +python main.py -m optimizer.lr=0.01,0.005,0.001 model.encoder=resnet50,resnet101 diff --git a/code/ch12/snippet_01.py b/code/ch12/snippet_01.py new file mode 100644 index 0000000..8e82b80 --- /dev/null +++ b/code/ch12/snippet_01.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:73 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 计算训练集 mean / std,结果用于 12.3 节的 transforms.Normalize +import numpy as np +from PIL import Image +from pathlib import Path + +mean = np.zeros(3) +std = np.zeros(3) +n = 0 +for p in Path('VOCdevkit/VOC2012/JPEGImages').glob('*.jpg'): + img = np.asarray(Image.open(p).convert('RGB'), dtype=np.float32) / 255.0 + mean += img.reshape(-1, 3).mean(axis=0) + std += img.reshape(-1, 3).std(axis=0) + n += 1 +mean /= n +std /= n +print('mean =', mean.tolist()) +print('std =', std.tolist()) diff --git a/code/ch12/snippet_02.py b/code/ch12/snippet_02.py new file mode 100644 index 0000000..0ce7d9c --- /dev/null +++ b/code/ch12/snippet_02.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:99 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入数据对象类 Dataset +from torch.utils.data import Dataset + +# 基于 Dataset 的数据定义和读取框架 +class CustomDataset(Dataset): + def __init__(self, ...): + # 初始化:定义数据路径、文件列表、变换方法等 + ... + + def __getitem__(self, index): + # 单样本读取:按索引读取一对 (img, label),并应用变换 + ... + return (img, label) + + def __len__(self): + # 返回数据集样本数量 + return len(self.images) diff --git a/code/ch12/snippet_03.py b/code/ch12/snippet_03.py new file mode 100644 index 0000000..5d98189 --- /dev/null +++ b/code/ch12/snippet_03.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:123 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入相关模块 +import numpy as np +import torch +from PIL import Image +from pathlib import Path +from torch.utils.data import Dataset + +# 定义 VOC 数据集类 +class VOCSegmentation(Dataset): + """A PyTorch Dataset class for the VOC Segmentation dataset.""" + + def __init__(self, root, image_set='train', transform=None): + """ + Args: + root (str): 数据集根目录 + image_set (str): 'train' 或 'val' + transform (callable, optional): 同时作用于图像与掩码的变换 + """ + self.root = Path(root) + self.transform = transform + self.image_set = image_set + + base_dir = 'VOCdevkit/VOC2012' + voc_root = self.root / base_dir + if not voc_root.is_dir(): + raise RuntimeError(f'Dataset not found at {voc_root}.') + + image_dir = voc_root / 'JPEGImages' + mask_dir = voc_root / 'SegmentationClass' + splits_dir = voc_root / 'ImageSets/Segmentation' + split_f = splits_dir / f"{image_set}.txt" + + with open(split_f, "r") as f: + file_names = [x.strip() for x in f.readlines()] + + self.images = [image_dir / f"{x}.jpg" for x in file_names] + self.masks = [mask_dir / f"{x}.png" for x in file_names] + assert len(self.images) == len(self.masks) + + def __getitem__(self, index): + img = Image.open(self.images[index]).convert('RGB') + # PASCAL VOC 分割掩码是 'P' 模式的调色板索引图,类别为 0–20, + # 边界与无关像素值为 255(即 ignore_index)。 + target = Image.open(self.masks[index]) + if self.transform is not None: + img, target = self.transform(img, target) + else: + # 当未传入 transform 时,显式把掩码转成 LongTensor, + # 保留 0–20 类别索引与 255 ignore——避免 transforms.ToTensor() + # 把 mask 缩放到 [0, 1] float32 而类别全乱。 + # + target = torch.as_tensor(np.array(target), dtype=torch.long) + return img, target + + def __len__(self): + return len(self.images) diff --git a/code/ch12/snippet_04.py b/code/ch12/snippet_04.py new file mode 100644 index 0000000..51fb16c --- /dev/null +++ b/code/ch12/snippet_04.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:188 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch.nn as nn +criterion = nn.CrossEntropyLoss(ignore_index=255) diff --git a/code/ch12/snippet_05.py b/code/ch12/snippet_05.py new file mode 100644 index 0000000..3c1647b --- /dev/null +++ b/code/ch12/snippet_05.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:195 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +train_dst = VOCSegmentation(root, image_set='train', transform=train_transform) +val_dst = VOCSegmentation(root, image_set='val', transform=val_transform) diff --git a/code/ch12/snippet_06.py b/code/ch12/snippet_06.py new file mode 100644 index 0000000..3d63491 --- /dev/null +++ b/code/ch12/snippet_06.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:212 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +from torch.utils.data import DataLoader + +train_loader = DataLoader( + train_dst, + batch_size=opts.batch_size, # VOC 上 DeepLab v3+ 常用 8 / 16 + shuffle=True, + num_workers=opts.num_workers, # 一般 4–8,按 CPU 核数调整 + pin_memory=True, # 配合 .to(non_blocking=True) 加速 H2D 拷贝 + persistent_workers=True, # PyTorch 1.7+,避免每 epoch 重启 worker + prefetch_factor=2, + drop_last=True, # 避免最后不满 batch 的 BN 抖动 + worker_init_fn=seed_worker, # 复现性:见 12.6 节 set_seed 配套实现 + generator=g, # 同上 +) +val_loader = DataLoader( + val_dst, batch_size=1, # 验证时建议 batch=1,避免 padding 影响指标 + shuffle=False, num_workers=opts.num_workers, + pin_memory=True, persistent_workers=True, +) diff --git a/code/ch12/snippet_07.py b/code/ch12/snippet_07.py new file mode 100644 index 0000000..83d5d56 --- /dev/null +++ b/code/ch12/snippet_07.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:242 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class DeepLabHeadV3Plus(nn.Module): + # + # DeepLab v3+ 原论文 §4 表 5:OS=16 用 (6,12,18);OS=8 用 (12,24,36)。 + def __init__(self, in_channels, low_level_channels, num_classes, + output_stride=16, aspp_dilate=None): + super(DeepLabHeadV3Plus, self).__init__() + if aspp_dilate is None: + if output_stride == 16: + aspp_dilate = (6, 12, 18) + elif output_stride == 8: + aspp_dilate = (12, 24, 36) + else: + raise ValueError(f'Unsupported output_stride={output_stride}') + + self.project = nn.Sequential( + nn.Conv2d(low_level_channels, 48, 1, bias=False), + nn.BatchNorm2d(48), + nn.ReLU(inplace=True), + ) + + # ASPP 空洞空间金字塔池化 + self.aspp = ASPP(in_channels, aspp_dilate) + + # 分类头 + self.classifier = nn.Sequential( + nn.Conv2d(304, 256, 3, padding=1, bias=False), + nn.BatchNorm2d(256), + nn.ReLU(inplace=True), + nn.Conv2d(256, num_classes, 1) + ) + + self._init_weight() + + def forward(self, feature): + low_level_feature = self.project(feature['low_level']) + output_feature = self.aspp(feature['out']) + output_feature = F.interpolate( + output_feature, + size=low_level_feature.shape[2:], + mode='bilinear', + align_corners=False, + ) + return self.classifier( + torch.cat([low_level_feature, output_feature], dim=1) + ) + + def _init_weight(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight) + elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) diff --git a/code/ch12/snippet_08.py b/code/ch12/snippet_08.py new file mode 100644 index 0000000..c772c74 --- /dev/null +++ b/code/ch12/snippet_08.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:311 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class FocalLoss(nn.Module): + """Focal Loss for dense prediction. Lin et al., ICCV 2017. + + 多类语义分割版本:alpha 既可为标量(教学示例的简化形式), + 也可为 shape=[C] 的 tensor 表达类别权重;ignore_index 用于跳过 + VOC 中值为 255 的边界 / 无关像素。 + """ + + # + def __init__(self, alpha=1.0, gamma=2.0, ignore_index=255): + super(FocalLoss, self).__init__() + self.alpha = alpha + self.gamma = gamma + self.ignore_index = ignore_index + + def forward(self, inputs, targets): + # 标准交叉熵——显式传 ignore_index,把 255 像素的 ce_loss 置 0 + ce_loss = F.cross_entropy( + inputs, targets, + reduction='none', + ignore_index=self.ignore_index, + ) + # Focal 调制项 + pt = torch.exp(-ce_loss) + focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss + # 仅按"有效像素"求平均,避免 .mean() 把 ignore 位置当成分母 + valid = (targets != self.ignore_index) + return focal_loss.sum() / valid.sum().clamp(min=1) diff --git a/code/ch12/snippet_09.py b/code/ch12/snippet_09.py new file mode 100644 index 0000000..5b0335d --- /dev/null +++ b/code/ch12/snippet_09.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:353 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import argparse + +parser = argparse.ArgumentParser() + +# 数据相关参数 +parser.add_argument("--data_root", type=str, default='./dataset', + help="path to Dataset") +parser.add_argument("--save_root", type=str, default='./', + help="path to save result") +parser.add_argument("--dataset", type=str, default='voc', + choices=['voc', 'cityscapes', 'ade'], + help='Name of dataset') +parser.add_argument("--num_classes", type=int, default=None, + help="num classes (default: None)") diff --git a/code/ch12/snippet_10.py b/code/ch12/snippet_10.py new file mode 100644 index 0000000..27c5d4e --- /dev/null +++ b/code/ch12/snippet_10.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:376 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 配套 utils/seed.py 或直接放在 main.py 顶部 +import os, random +import numpy as np +import torch + + +def set_seed(seed: int = 42, deterministic: bool = False): + """复现性最小骨架——分割训练前调用一次。 + + deterministic=True 时启用 PyTorch 的确定性算法(部分算子会牺牲性能); + 通常仅在最终发版前的对比实验中开启,平时训练优先使用 cudnn.benchmark + 以让 cuDNN 自动搜索最优卷积算法。 + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + if deterministic: + torch.use_deterministic_algorithms(True, warn_only=True) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + else: + torch.backends.cudnn.benchmark = True + + +def seed_worker(worker_id): + """配合 DataLoader(worker_init_fn=seed_worker, generator=g) 使用。""" + worker_seed = torch.initial_seed() % 2**32 + np.random.seed(worker_seed) + random.seed(worker_seed) + + +# DataLoader 端的 generator +g = torch.Generator() +g.manual_seed(42) diff --git a/code/ch12/snippet_11.py b/code/ch12/snippet_11.py new file mode 100644 index 0000000..a8a2eb2 --- /dev/null +++ b/code/ch12/snippet_11.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:422 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# + +# 设备自适应——避免在代码块内出现裸用 'cuda' 导致的 NameError / 无 GPU 拷贝失败 +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +model = model.to(device) +model.train() + +# 初始化区间损失 +interval_loss = 0 + +while True: + # ===== 训练阶段 ===== + cur_epochs += 1 + for (images, labels) in train_loader: + cur_itrs += 1 + # ImageNet 预训练权重已是 float32,无需手动强转 dtype; + # AMP(见 12.10.1 节)会自动管理。non_blocking 仅与 pin_memory 配合时生效。 + images = images.to(device, non_blocking=True) + labels = labels.to(device, dtype=torch.long, non_blocking=True) + + optimizer.zero_grad(set_to_none=True) # PyTorch 1.7+ 推荐用法 + outputs = model(images) + loss = criterion(outputs, labels) # CrossEntropyLoss(ignore_index=255) + loss.backward() + optimizer.step() + # PolyLR 配 total_itrs,每 iter 调一次(DeepLab 系列); + # 若改用 CosineAnnealingLR / StepLR 等 epoch 维度调度器,则把 step 移到 epoch for 外。 + scheduler.step() + + np_loss = loss.detach().cpu().numpy() + interval_loss += np_loss + if vis is not None: + vis.vis_scalar('Loss', cur_itrs, np_loss) + + # 打印训练信息(按 print_interval 区间平均) + if cur_itrs % opts.print_interval == 0: + interval_loss /= opts.print_interval + print(f"Epoch {cur_epochs}, Itrs {cur_itrs}/{opts.total_itrs}, " + f"Loss={interval_loss:.4f}") + interval_loss = 0 + + # ===== 周期性保存与验证 ===== + if cur_itrs % opts.val_interval == 0: + # 保存最新检查点(含 model + optimizer + scheduler + epoch + best_score) + save_ckpt(model, optimizer, scheduler, + cur_itrs, best_score, + os.path.join(save_path_checkpoints, 'latest.pth')) + logger.info("Save the latest model to %s" % save_path_checkpoints) + + print("validation...") + model.eval() + val_score, ret_samples = validate( + opts=opts, model=model, loader=val_loader, + device=device, metrics=metrics, + ret_samples_ids=vis_sample_id, + ) + logger.info("Validation performance: %s", val_score) + + # 保存最优模型(以 mIoU 为最优指标,符合 PASCAL VOC 评测规范) + if val_score['Mean IoU'] > best_score: + best_score = val_score['Mean IoU'] + save_ckpt(model, optimizer, scheduler, cur_itrs, best_score, + os.path.join( + save_path_checkpoints, + f'best_{opts.model}_{opts.dataset}_os{opts.output_stride}.pth', + )) + logger.info("Save best-performance model so far to %s" + % save_path_checkpoints) + + # 训练过程可视化 + if vis is not None: + vis.vis_scalar("[Val] Overall Acc", cur_itrs, val_score['Overall Acc']) + vis.vis_scalar("[Val] Mean IoU", cur_itrs, val_score['Mean IoU']) + vis.vis_table("[Val] Class IoU", val_score['Class IoU']) + for k, (img, target, lbl) in enumerate(ret_samples): + img = (denorm(img) * 255).astype(np.uint8) + target = train_dst.decode_target(target).transpose(2, 0, 1).astype(np.uint8) + lbl = train_dst.decode_target(lbl).transpose(2, 0, 1).astype(np.uint8) + concat_img = np.concatenate((img, target, lbl), axis=2) + vis.vis_image('Sample %d' % k, concat_img) + + # 验证完毕显式切回训练态——否则 BN 用 running mean/var、Dropout 失效, + # 下一个 batch 的训练会显著退化(CODE-C2)。 + model.train() + + # 显式退出条件——达到目标迭代数即结束训练 + if cur_itrs >= opts.total_itrs: + return diff --git a/code/ch12/snippet_12.py b/code/ch12/snippet_12.py new file mode 100644 index 0000000..c747ceb --- /dev/null +++ b/code/ch12/snippet_12.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:548 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# AMP 完整训练循环示例(教学层面,与代码 12-6 的最小循环互补) +from torch.amp import autocast, GradScaler + +scaler = GradScaler('cuda') # BF16 时可不使用 scaler,但启用 BF16 仍兼容 +accum_steps = 1 # 梯度累积步数;显存紧张时可调到 2 或 4 + +model.train() +for cur_itrs, (images, labels) in enumerate(train_loader, 1): + images = images.to(device, non_blocking=True) + labels = labels.to(device, dtype=torch.long, non_blocking=True) + + # BF16 在 Ampere(A100)及更新的 NVIDIA GPU 上数值范围更宽、不易 NaN; + # FP16 仅在显存极度紧张且硬件不支持 BF16(V100 / T4)时使用。 + with autocast('cuda', dtype=torch.bfloat16): + outputs = model(images) + loss = criterion(outputs, labels) / accum_steps + + scaler.scale(loss).backward() + if cur_itrs % accum_steps == 0: + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad(set_to_none=True) + scheduler.step() # PolyLR 每 iter 调度 diff --git a/code/ch12/snippet_13.py b/code/ch12/snippet_13.py new file mode 100644 index 0000000..1ecc97a --- /dev/null +++ b/code/ch12/snippet_13.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:582 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# +import os +import numpy as np +import torch +from PIL import Image + + +def inference(model, test_img, transform, device='cuda'): + """单张图像推理,输出 RGB 三通道伪彩色掩码。 + + Args: + model: 已加载权重并切换到 eval 模式的分割网络。 + test_img: 待推理图像路径。 + transform: 与验证一致的 tensor 化 + 归一化变换。 + device: 'cuda' / 'cpu' / 'mps'。 + """ + img = Image.open(test_img).convert('RGB') + input_tensor = transform(img).unsqueeze(0).to(device) + # PyTorch 1.9+ 推荐 inference_mode() 替代 no_grad()—— + # 后者会进一步禁止 view 跟踪,对推理吞吐有约 5%–10% 的提升。 + with torch.inference_mode(): + outputs = model(input_tensor) + + preds = outputs.argmax(dim=1).cpu().numpy() + pred_rgb = VOCSegmentation.decode_target(preds[0]).astype(np.uint8) + save_path = f"{test_img.rsplit('.', 1)[0]}_pred.png" + Image.fromarray(pred_rgb).save(save_path) + return save_path diff --git a/code/ch12/snippet_14.py b/code/ch12/snippet_14.py new file mode 100644 index 0000000..346035f --- /dev/null +++ b/code/ch12/snippet_14.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:638 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +from flask import Flask, jsonify + +app = Flask(__name__) + + +@app.route('/predict', methods=['POST']) +def predict(): + return jsonify({'class_id': 'IMAGE_NET_XXX', 'class_name': 'Cat'}) diff --git a/code/ch12/snippet_15.py b/code/ch12/snippet_15.py new file mode 100644 index 0000000..61e6747 --- /dev/null +++ b/code/ch12/snippet_15.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:657 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# +import io +import torch +import numpy as np +from PIL import Image +from torchvision import transforms +from flask import Flask, request, jsonify + +from datasets import VOCSegmentation +import models + +app = Flask(__name__) + +# 设备自适应——避免在无 GPU 环境下硬编码 'cuda' 直接挂 +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + +# 模型字典 +model_map = { + 'deeplabv3plus_resnet50': models.deeplabv3plus_resnet50, + 'deeplabv3plus_resnet101': models.deeplabv3plus_resnet101, +} + +# 加载模型——完整 checkpoint 含 optimizer/scheduler 等非 state_dict 张量, +# 须显式 weights_only=False;如仅加载权重子项则保持默认 True 更安全(PyTorch 2.4+)。 +model = model_map['deeplabv3plus_resnet101'](num_classes=21, output_stride=16) +ckpt = torch.load('../checkpoints/deeplabv3plus_resnet101_voc.pth', + map_location=device, weights_only=False) +model.load_state_dict(ckpt['model_state']) +model.to(device) +model.eval() +print('model loaded.') + +# 数据变换(与训练 / 验证一致——mean/std 抽出为公共常量,避免三处复制粘贴漂移) +IMAGENET_MEAN = (0.485, 0.456, 0.406) +IMAGENET_STD = (0.229, 0.224, 0.225) +transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), +]) + + +@app.route('/predict', methods=['POST']) +def predict(): + # 请求体异常处理——避免无 'image' 字段或非图像内容直接 500(CODE-M7) + if 'image' not in request.files: + return jsonify({'error': 'no image field in form-data'}), 400 + try: + buf = request.files['image'].read() + image = Image.open(io.BytesIO(buf)).convert('RGB') + except Exception as e: + return jsonify({'error': f'cannot decode image: {e}'}), 400 + + input_tensor = transform(image).unsqueeze(0).to(device) + with torch.inference_mode(): + output = model(input_tensor) + preds = output.argmax(dim=1).cpu().numpy() + preds = VOCSegmentation.decode_target(preds[0]).astype(np.uint8) + return jsonify(preds.tolist()) + + +if __name__ == '__main__': + # 仅供本地学习;生产请用 gunicorn -w 4 api:app 或 FastAPI + uvicorn 替代 dev server。 + # debug=True 会启用 Werkzeug 调试器,存在远程任意代码执行风险,禁用之。 + app.run(host='127.0.0.1', port=5000, debug=False) diff --git a/code/ch12/snippet_16.py b/code/ch12/snippet_16.py new file mode 100644 index 0000000..ff38a6f --- /dev/null +++ b/code/ch12/snippet_16.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:734 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import requests +resp = requests.post( + "http://localhost:5000/predict", + files={"image": open('./deployment/2007_000676.jpg', 'rb')}, +) diff --git a/code/ch12/snippet_17.py b/code/ch12/snippet_17.py new file mode 100644 index 0000000..86aabd9 --- /dev/null +++ b/code/ch12/snippet_17.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:754 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# +import os +import torch +import numpy as np +from PIL import Image +from torchvision import transforms +from flask import Flask, request, render_template +from werkzeug.utils import secure_filename + +from datasets import VOCSegmentation +import models + +app = Flask(__name__) + +# ===== 配置与模型加载(与 api.py 共享同一份 transform / 设备 / 权重)===== +app.config['UPLOAD_FOLDER'] = 'uploads/' +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB 上限,避免大文件 OOM +ALLOWED_EXT = {'.jpg', '.jpeg', '.png'} +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +model = models.deeplabv3plus_resnet101(num_classes=21, output_stride=16) +ckpt = torch.load('../checkpoints/deeplabv3plus_resnet101_voc.pth', + map_location=device, weights_only=False) +model.load_state_dict(ckpt['model_state']) +model.to(device).eval() + +IMAGENET_MEAN = (0.485, 0.456, 0.406) +IMAGENET_STD = (0.229, 0.224, 0.225) +transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), +]) + + +@app.route('/', methods=['GET', 'POST']) +def upload_predict(): + if request.method == 'POST': + image_file = request.files.get('image') + if not image_file: + return render_template('index.html', error='no image') + + # secure_filename 防路径穿越(如 ../../../etc/...) + fname = secure_filename(image_file.filename) + ext = os.path.splitext(fname)[1].lower() + if ext not in ALLOWED_EXT: + return render_template('index.html', error='unsupported format') + + image_location = os.path.join(app.config['UPLOAD_FOLDER'], fname) + image_file.save(image_location) + + image = Image.open(image_location).convert('RGB') + input_tensor = transform(image).unsqueeze(0).to(device) + + with torch.inference_mode(): + output = model(input_tensor) + preds = output.argmax(dim=1).cpu().numpy() + + preds = VOCSegmentation.decode_target(preds[0]).astype(np.uint8) + segmented_image = Image.fromarray(preds) + # 不再假设输入一定是 .jpg——用 rsplit 去掉真实扩展名再统一存为 .png + segmented_image_path = image_location.rsplit('.', 1)[0] + '_segmented.png' + segmented_image.save(segmented_image_path) + + return render_template( + 'index.html', + input_image='/' + image_location, + segmented_image='/' + segmented_image_path, + ) + return render_template('index.html') + + +if __name__ == '__main__': + # 同代码 12-9:debug=False;生产建议用 gunicorn / uvicorn 替代 Werkzeug dev server。 + app.run(host='127.0.0.1', port=5000, debug=False) diff --git a/code/ch12/snippet_18.py b/code/ch12/snippet_18.py new file mode 100644 index 0000000..31c13d8 --- /dev/null +++ b/code/ch12/snippet_18.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:878 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# PyTorch 2.x 训练加速:一行启用 +model = model.to(device) +model = torch.compile(model, mode='reduce-overhead') # 或 'max-autotune' + +# AMP 混合精度(与 torch.compile 兼容) +from torch.amp import autocast, GradScaler +scaler = GradScaler('cuda') + +for images, labels in train_loader: + images = images.to(device); labels = labels.to(device) + optimizer.zero_grad() + with autocast('cuda', dtype=torch.float16): + outputs = model(images) + loss = criterion(outputs, labels) + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() diff --git a/code/ch12/snippet_19.py b/code/ch12/snippet_19.py new file mode 100644 index 0000000..e47af30 --- /dev/null +++ b/code/ch12/snippet_19.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:904 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# torchvision.transforms.v2 联合变换示例(替代代码 12-2 中自定义的 et.Compose) +from torchvision.transforms import v2 as T +from torchvision import tv_tensors + +train_transform = T.Compose([ + T.ToImage(), # PIL / ndarray → Image + T.RandomResizedCrop(513, antialias=True), + T.RandomHorizontalFlip(p=0.5), + T.ToDtype(torch.float32, scale=True), + T.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]), +]) + +# 使用方式(mask 用 tv_tensors.Mask 包裹,v2 自动联合变换) +img = Image.open(img_path).convert('RGB') +mask = tv_tensors.Mask(np.array(Image.open(mask_path))) +img, mask = train_transform(img, mask) diff --git a/code/ch12/snippet_20.py b/code/ch12/snippet_20.py new file mode 100644 index 0000000..bc07ca1 --- /dev/null +++ b/code/ch12/snippet_20.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:932 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import segmentation_models_pytorch as smp + +model = smp.DeepLabV3Plus( + encoder_name='resnet101', + encoder_weights='imagenet', + in_channels=3, + classes=21, # PASCAL VOC 类别数 +) diff --git a/code/ch12/snippet_21.py b/code/ch12/snippet_21.py new file mode 100644 index 0000000..29c2a91 --- /dev/null +++ b/code/ch12/snippet_21.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:959 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# main.py +import hydra +from omegaconf import DictConfig + +@hydra.main(version_base=None, config_path="configs", config_name="config") +def main(cfg: DictConfig): + # cfg.dataset.name, cfg.model.encoder, cfg.optimizer.lr ... + train(cfg) diff --git a/code/ch12/snippet_23.py b/code/ch12/snippet_23.py new file mode 100644 index 0000000..f3860fa --- /dev/null +++ b/code/ch12/snippet_23.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 12 章 — (无标号片段) +# 原始位置:book-review/drafts/ch12-v4.md:1004 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import io, numpy as np, onnxruntime as ort +from fastapi import FastAPI, UploadFile +from PIL import Image + +app = FastAPI() +session = ort.InferenceSession( + 'deeplabv3plus_resnet101.onnx', + providers=['CUDAExecutionProvider', 'CPUExecutionProvider'], +) + +@app.post('/predict') +async def predict(image: UploadFile): + img = Image.open(io.BytesIO(await image.read())).convert('RGB') + x = preprocess(img) # 与训练一致的归一化 + out = session.run(None, {'input': x[None]})[0] # numpy 推理 + pred = out.argmax(axis=1)[0].astype(np.uint8) + return {'mask': pred.tolist()} diff --git a/code/ch13/README.md b/code/ch13/README.md new file mode 100644 index 0000000..6cbb1e2 --- /dev/null +++ b/code/ch13/README.md @@ -0,0 +1,53 @@ +# 第 13 章 — 医学影像分割项目实战 · 配套代码 + +> 抽取自《深度学习图像分割》第 13 章修订稿 `book-review/drafts/ch13-v4.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +医学影像分割实战(ICH 出血分割),含 PyTorch 训练与 LibTorch C++ 部署。 + +## 依赖 + +- torch>=2.0 +- monai +- nibabel(DICOM/NIfTI 读取) +- OpenCV 4.x(C++ 推理示例) +- LibTorch C++(CMake 构建) + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_13-1.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_01.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `snippet_02.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_04.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_05.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_06.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_07.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_08.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_09.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_10.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `snippet_11.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_12.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_13.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_14.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_15.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `cpp/snippet_01.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/snippet_02.cpp` | 需 OpenCV 4.x | 需 OpenCV / LibTorch 头文件方可编译 | +| `cpp/CMakeLists_code_13-14.txt` | 构建脚本 | CMake 配置 | +| `cpp/CMakeLists_snippet_01.txt` | 构建脚本 | CMake 配置 | +| `scripts/snippet_01.sh` | 脚本 | 下载 / 安装命令 | +| `scripts/snippet_02.sh` | 脚本 | 下载 / 安装命令 | +| `scripts/snippet_03.sh` | 脚本 | 下载 / 安装命令 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch13/code_13-1.py b/code/ch13/code_13-1.py new file mode 100644 index 0000000..86584b0 --- /dev/null +++ b/code/ch13/code_13-1.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — 代码 13-1 采用的是**离线增强**:在训练前一次性把每张原图扩充成多张并写盘,训练时直接读盘即可。这种做法在小数据集(如原始 ICH 仅 271 张)上能快速建立训练流程,但有两个缺点:① 磁盘空间膨胀 5 倍;② 每个 epoch 看到的"增强样本"是固定的有限张,**等价于扩充 5 倍数据集而非真随机增强**,可能对这固定数量的"伪样本"过拟合。 +# 原始位置:book-review/drafts/ch13-v4.md:207 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import albumentations as A +from albumentations.pytorch import ToTensorV2 + +train_transform = A.Compose([ + A.RandomScale(scale_limit=0.2, p=0.5), + A.PadIfNeeded(min_height=512, min_width=512, p=1.0), + A.RandomCrop(height=512, width=512, p=1.0), + A.HorizontalFlip(p=0.5), + A.RandomRotate90(p=0.5), + A.ElasticTransform(alpha=120, sigma=120 * 0.05, p=0.3), + A.RandomBrightnessContrast(p=0.3), + A.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]), + ToTensorV2(), +], additional_targets={'mask': 'mask'}) diff --git a/code/ch13/cpp/CMakeLists_code_13-14.txt b/code/ch13/cpp/CMakeLists_code_13-14.txt new file mode 100644 index 0000000..87cb63b --- /dev/null +++ b/code/ch13/cpp/CMakeLists_code_13-14.txt @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — 代码 13-14 相对于此前版本有 7 项关键修订:① BGR → RGB 通道顺序对齐 Python 训练端 PIL 的 `convert('RGB')`;② 先 resize 后 convertTo(cubic 插值在 uint8 上更稳,避免 float32 上的过冲);③/④/⑤ 三步对齐 Python 端 `Normalize(mean=ImageNet_mean, std=ImageNet_std)` 归一化——这一步是与训练流水线对齐**最容易被忽略**的部分;⑥ `from_blob` 显式指定 `dtype=kFloat32` 并 `.clone()` 脱离 `cv::Mat` 生命周期;⑦ 后处理用 `sigmoid + (>0.5)` 显式阈值化,避免 `cv::normalize 0–255` 在健康样本(全负 logit)上拉伸产生伪病灶。 +# 原始位置:book-review/drafts/ch13-v4.md:1219 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-15 2D 医学影像部署的 CMakeLists 文件 +cmake_minimum_required(VERSION 3.18 FATAL_ERROR) +project(deployment) + +find_package(OpenCV REQUIRED) + +# 指向 libtorch 安装目录 +list(APPEND CMAKE_PREFIX_PATH "../libtorch") +find_package(Torch REQUIRED) + +add_executable(deployment deployment.cpp) +target_link_libraries(deployment "${TORCH_LIBRARIES}" ${OpenCV_LIBS}) +set_property(TARGET deployment PROPERTY CXX_STANDARD 17) diff --git a/code/ch13/cpp/CMakeLists_snippet_01.txt b/code/ch13/cpp/CMakeLists_snippet_01.txt new file mode 100644 index 0000000..61269e7 --- /dev/null +++ b/code/ch13/cpp/CMakeLists_snippet_01.txt @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1065 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-10 编写 CMakeLists 文件 +cmake_minimum_required(VERSION 3.18 FATAL_ERROR) +project(custom_ops) + +find_package(Torch REQUIRED) + +add_executable(example-app example-app.cpp) +target_link_libraries(example-app "${TORCH_LIBRARIES}") +set_property(TARGET example-app PROPERTY CXX_STANDARD 17) diff --git a/code/ch13/cpp/snippet_01.cpp b/code/ch13/cpp/snippet_01.cpp new file mode 100644 index 0000000..c3a2164 --- /dev/null +++ b/code/ch13/cpp/snippet_01.cpp @@ -0,0 +1,33 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 13 章 — (无标号片段) +// 原始位置:book-review/drafts/ch13-v4.md:1030 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +// 代码 13-9 cpp 推理脚本(ResNet18 分类示例) +#include +#include +#include + +int main(int argc, const char* argv[]) { + if (argc != 2) { + std::cerr << "usage: example-app \n"; + return -1; + } + + torch::jit::script::Module module; + try { + module = torch::jit::load(argv[1]); + } + catch (const c10::Error& e) { + std::cerr << "error loading the model\n"; + return -1; + } + + std::vector inputs; + inputs.push_back(torch::ones({1, 3, 224, 224})); + + at::Tensor output = module.forward(inputs).toTensor(); + std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n'; + std::cout << "ok\n"; + return 0; +} diff --git a/code/ch13/cpp/snippet_02.cpp b/code/ch13/cpp/snippet_02.cpp new file mode 100644 index 0000000..7c4dfc0 --- /dev/null +++ b/code/ch13/cpp/snippet_02.cpp @@ -0,0 +1,82 @@ +// -*- coding: utf-8 -*- +// 来源:《深度学习图像分割》第 13 章 — (无标号片段) +// 原始位置:book-review/drafts/ch13-v4.md:1133 +// 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +// 修改请回写源稿后重新生成,避免代码与书稿失同步。 +// 代码 13-14 deployment.cpp 部署脚本 +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) { + if (argc != 3) { + std::cerr << "usage: deployment \n"; + return -1; + } + + // 1. 导入模型文件 + torch::jit::script::Module module; + try { + module = torch::jit::load(argv[1]); + } catch (const c10::Error& e) { + std::cerr << "Failed to load the model: " << e.what() << "\n"; + return -1; + } + module.eval(); + + // 2. 基于 OpenCV 读取影像(默认 BGR 通道) + cv::Mat img = cv::imread(argv[2], cv::IMREAD_COLOR); + if (img.empty()) { + std::cerr << "Failed to load image: " << argv[2] << "\n"; + return -1; + } + + // 3. 与训练端预处理严格对齐:BGR→RGB → resize → /255 → ImageNet mean/std + cv::cvtColor(img, img, cv::COLOR_BGR2RGB); // ① BGR→RGB + cv::resize(img, img, cv::Size(512, 512), cv::INTER_LINEAR); // ② resize(uint8 上做) + img.convertTo(img, CV_32FC3, 1.0f / 255.0f); // ③ 0–1 + cv::Scalar mean(0.485, 0.456, 0.406), stdv(0.229, 0.224, 0.225); + img -= mean; // ④ 减均值 + cv::divide(img, stdv, img); // ⑤ 除方差 + + // 4. 转换为 torch tensor(显式 dtype + clone 脱离 cv::Mat 生命周期) + auto img_tensor = torch::from_blob( + img.data, + {1, img.rows, img.cols, 3}, + torch::TensorOptions().dtype(torch::kFloat32) + ).clone(); + img_tensor = img_tensor.permute({0, 3, 1, 2}).contiguous(); // NHWC → NCHW + + std::vector inputs; + inputs.push_back(img_tensor); + + // 5. 执行预测(推荐用 try/catch 包裹,避免临床部署时崩溃上层 GUI) + torch::Tensor logits; + try { + logits = module.forward(inputs).toTensor(); + } catch (const c10::Error& e) { + std::cerr << "Model forward failed: " << e.what() << "\n"; + return -1; + } + + // 6. 后处理:sigmoid + 阈值化为 0/255 二值掩码 + // (切勿用 cv::normalize 0–255 线性拉伸——会在健康样本上产生伪病灶) + torch::Tensor probs = torch::sigmoid(logits); + torch::Tensor mask = (probs > 0.5).to(torch::kU8) * 255; + mask = mask.squeeze().detach().cpu().contiguous(); + + cv::Mat out_img( + mask.size(0), mask.size(1), CV_8U, + mask.data_ptr() + ); + out_img = out_img.clone(); // 显式拷贝,脱离 mask tensor 生命周期 + + // 7. 保存分割图像 + cv::imwrite("./output_image.jpg", out_img); + std::cout << "ok\n"; + return 0; +} diff --git a/code/ch13/inventory.md b/code/ch13/inventory.md new file mode 100644 index 0000000..6042f1e --- /dev/null +++ b/code/ch13/inventory.md @@ -0,0 +1,36 @@ +# 第 13 章代码清单 + +- 源稿:`book-review/drafts/ch13-v4.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 118–179 | python | `code/ch13/snippet_01.py` | — | +| 2 | 184–189 | python | `code/ch13/snippet_02.py` | — | +| 3 | 207–223 | python | `code/ch13/code_13-1.py` | 代码 13-1 | +| 4 | 249–306 | python | `code/ch13/snippet_04.py` | — | +| 5 | 326–362 | python | `code/ch13/snippet_05.py` | — | +| 6 | 442–446 | math | `—` | — | +| 7 | 448–452 | math | `—` | — | +| 8 | 456–460 | math | `—` | — | +| 9 | 468–538 | python | `code/ch13/snippet_06.py` | — | +| 10 | 550–554 | math | `—` | — | +| 11 | 560–564 | math | `—` | — | +| 12 | 570–593 | python | `code/ch13/snippet_07.py` | — | +| 13 | 613–633 | python | `code/ch13/snippet_08.py` | — | +| 14 | 713–747 | python | `code/ch13/snippet_09.py` | — | +| 15 | 803–892 | python | `code/ch13/snippet_10.py` | — | +| 16 | 938–958 | python | `code/ch13/snippet_11.py` | — | +| 17 | 966–972 | python | `code/ch13/snippet_12.py` | — | +| 18 | 1001–1024 | python | `code/ch13/snippet_13.py` | — | +| 19 | 1030–1059 | cpp | `code/ch13/cpp/snippet_01.cpp` | — | +| 20 | 1065–1075 | cmake | `code/ch13/cpp/CMakeLists_snippet_01.txt` | — | +| 21 | 1079–1085 | bash | `code/ch13/scripts/snippet_01.sh` | — | +| 22 | 1089–1092 | bash | `code/ch13/scripts/snippet_02.sh` | — | +| 23 | 1104–1127 | python | `code/ch13/snippet_14.py` | — | +| 24 | 1133–1211 | cpp | `code/ch13/cpp/snippet_02.cpp` | — | +| 25 | 1219–1233 | cmake | `code/ch13/cpp/CMakeLists_code_13-14.txt` | 代码 13-14 | +| 26 | 1237–1244 | bash | `code/ch13/scripts/snippet_03.sh` | — | +| 27 | 1279–1309 | python | `code/ch13/snippet_15.py` | — | diff --git a/code/ch13/scripts/snippet_01.sh b/code/ch13/scripts/snippet_01.sh new file mode 100644 index 0000000..6422a98 --- /dev/null +++ b/code/ch13/scripts/snippet_01.sh @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1079 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-11 编译部署项目 +mkdir example_test +cd example_test +cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch .. +cmake --build . --config Release diff --git a/code/ch13/scripts/snippet_02.sh b/code/ch13/scripts/snippet_02.sh new file mode 100644 index 0000000..c06b140 --- /dev/null +++ b/code/ch13/scripts/snippet_02.sh @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1089 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-12 查看部署效果 +./example-app traced_resnet_model.pt diff --git a/code/ch13/scripts/snippet_03.sh b/code/ch13/scripts/snippet_03.sh new file mode 100644 index 0000000..ea12d58 --- /dev/null +++ b/code/ch13/scripts/snippet_03.sh @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1237 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-16 执行编译 +cmake -DCMAKE_PREFIX_PATH=../libtorch .. +cmake --build . --config Release + +# 执行编译后的文件即可得到输出 +./deployment ../res_unet34_scripted.pt ../ich_test.jpg diff --git a/code/ch13/snippet_01.py b/code/ch13/snippet_01.py new file mode 100644 index 0000000..9cca7e6 --- /dev/null +++ b/code/ch13/snippet_01.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:118 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-1 ICH 的 Albumentations 增强 +import os +import cv2 +from tqdm import tqdm +from glob import glob +from albumentations import ( + CenterCrop, RandomRotate90, GridDistortion, + HorizontalFlip, VerticalFlip, +) + + +def augment_data(images, masks, save_path, augment=True): + H = 512 + W = 512 + for x_path, y_path in tqdm(zip(images, masks), total=len(images)): + x_path = x_path.replace("\\", '/') + image_name, image_extn = os.path.splitext( + os.path.basename(x_path)) # CODE-L2 修正:os.path.splitext + + y_path = y_path.replace("\\", '/') + mask_name, mask_extn = os.path.splitext( + os.path.basename(y_path)) + + # 读取图像(彩色 3 通道)和 mask(严格单通道,CODE-C2) + x = cv2.imread(x_path, cv2.IMREAD_COLOR) + y = cv2.imread(y_path, cv2.IMREAD_GRAYSCALE) + + if augment: + # 中心裁剪:image 与 mask 必须在同一次 aug 调用中完成(CODE-C1) + aug = CenterCrop(H, W, p=1.0)(image=x, mask=y) + x1, y1 = aug["image"], aug["mask"] + # 随机旋转 90 度 + aug = RandomRotate90(p=1.0)(image=x, mask=y) + x2, y2 = aug["image"], aug["mask"] + # 网格失真 + aug = GridDistortion(p=1.0)(image=x, mask=y) + x3, y3 = aug["image"], aug["mask"] + # 水平翻转 + aug = HorizontalFlip(p=1.0)(image=x, mask=y) + x4, y4 = aug["image"], aug["mask"] + # 垂直翻转 + aug = VerticalFlip(p=1.0)(image=x, mask=y) + x5, y5 = aug["image"], aug["mask"] + + save_images = [x, x1, x2, x3, x4, x5] + save_masks = [y, y1, y2, y3, y4, y5] + else: + save_images = [x] + save_masks = [y] + + # 保存增强后的图像与掩码(mask 用最近邻 resize,避免插值产生中间灰度,CODE-M1) + idx = 0 + for i, m in zip(save_images, save_masks): + i = cv2.resize(i, (W, H), interpolation=cv2.INTER_LINEAR) + m = cv2.resize(m, (W, H), interpolation=cv2.INTER_NEAREST) + tmp_img_name = f"{image_name}_{idx}{image_extn}" + tmp_mask_name = f"{mask_name}_{idx}{mask_extn}" + cv2.imwrite(os.path.join(save_path, "images", tmp_img_name), i) + cv2.imwrite(os.path.join(save_path, "masks", tmp_mask_name), m) + idx += 1 diff --git a/code/ch13/snippet_02.py b/code/ch13/snippet_02.py new file mode 100644 index 0000000..ea0a461 --- /dev/null +++ b/code/ch13/snippet_02.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:184 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 错误写法:image 与 mask 解耦——两次调用产生独立随机参数, +# 图像和标注会错位,等价于训练数据被破坏 +x_aug = RandomRotate90(p=1.0)(image=x, mask=y)["image"] +y_aug = RandomRotate90(p=1.0)(image=x, mask=y)["mask"] diff --git a/code/ch13/snippet_04.py b/code/ch13/snippet_04.py new file mode 100644 index 0000000..e383e4f --- /dev/null +++ b/code/ch13/snippet_04.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:249 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-2 ICH 数据集定义与导入 +class ICHSegmentation(data.Dataset): + cmap = color_map() + + def __init__(self, root, image_set='train', transform=None): + self.root = root + self.image_set = image_set + self.transform = transform + + if image_set == 'train': + image_dir = os.path.join(root, 'train_imgs') + mask_dir = os.path.join(root, 'train_masks') + else: + image_dir = os.path.join(root, 'valid_imgs') + mask_dir = os.path.join(root, 'valid_masks') + + self.images = sorted([os.path.join(image_dir, x) + for x in os.listdir(image_dir)]) + self.masks = sorted([os.path.join(mask_dir, x) + for x in os.listdir(mask_dir)]) + # 不仅长度相等,文件名也必须一一对应(CODE-M9) + assert len(self.images) == len(self.masks) + for img_path, mask_path in zip(self.images, self.masks): + img_stem = os.path.splitext(os.path.basename(img_path))[0] + mask_stem = os.path.splitext(os.path.basename(mask_path))[0] + assert img_stem == mask_stem, \ + f"Image/mask filenames mismatch: {img_path} vs {mask_path}" + + def __getitem__(self, index): + # CODE-C3:image 与 mask 走不同的 preprocess + img = Image.open(self.images[index]).convert('RGB') + # 用 'L' 模式(8-bit 灰度)+ 阈值化,避免 '1' 模式的 dither 噪点 + target = Image.open(self.masks[index]).convert('L') + target = np.array(target, dtype=np.uint8) + target = (target > 127).astype(np.float32) # 严格二值 + + if self.transform is not None: + img, target = self.transform(img, target) + + # image 与 mask 走不同的预处理:image 含均值方差归一化;mask 仅 reshape + img = self.image_preprocess(img) # 含 ImageNet 归一化 + img = torch.from_numpy(img).float() # (3, H, W) + target = torch.from_numpy(target).float().unsqueeze(0) # (1, H, W) + return img, target + + def __len__(self): + return len(self.images) + + @staticmethod + def image_preprocess(img): + """RGB image → float32 (3, H, W),做 ImageNet mean/std 归一化。""" + arr = np.array(img, dtype=np.float32) / 255.0 # H, W, 3 + mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) + std = np.array([0.229, 0.224, 0.225], dtype=np.float32) + arr = (arr - mean) / std + return arr.transpose(2, 0, 1) # 3, H, W diff --git a/code/ch13/snippet_05.py b/code/ch13/snippet_05.py new file mode 100644 index 0000000..eef07e6 --- /dev/null +++ b/code/ch13/snippet_05.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:326 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import re, json, random, os + +def patient_level_split(image_dir, val_ratio=0.2, seed=42): + """按 patient_id 划分 train/val,返回两个 patient_id 集合。""" + pattern = re.compile(r'(\d+)-slice') + patient_ids = sorted({ + pattern.match(f).group(1) + for f in os.listdir(image_dir) + if pattern.match(f) is not None + }) + rng = random.Random(seed) + rng.shuffle(patient_ids) + split_idx = int(len(patient_ids) * (1 - val_ratio)) + train_ids = set(patient_ids[:split_idx]) + val_ids = set(patient_ids[split_idx:]) + return train_ids, val_ids + + +def filter_by_patient(image_dir, patient_id_set): + """根据 patient_id 集合过滤切片文件名。""" + pattern = re.compile(r'(\d+)-slice') + return sorted([ + f for f in os.listdir(image_dir) + if pattern.match(f) and pattern.match(f).group(1) in patient_id_set + ]) + + +# 用法 +train_ids, val_ids = patient_level_split('data/ich/all_imgs', seed=42) +# 持久化到 splits/ich_split.json,确保多次实验可复现 +with open('splits/ich_split.json', 'w') as f: + json.dump( + {'train': sorted(train_ids), 'val': sorted(val_ids)}, + f, indent=2, + ) diff --git a/code/ch13/snippet_06.py b/code/ch13/snippet_06.py new file mode 100644 index 0000000..1b22d5c --- /dev/null +++ b/code/ch13/snippet_06.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:468 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-3 模型评估函数编写 +import numpy as np +from scipy.spatial.distance import directed_hausdorff + + +class StreamSegMetrics(_StreamMetrics): + """模型评估计算类,包括 Dice、IoU 和 HD 三个指标的计算。""" + + def __init__(self): + self.dice = [] + self.iou = [] + self.hd = [] + + def update(self, label_trues, label_preds): + for lt, lp in zip(label_trues, label_preds): + self.dice.append(self.dice_coefficient(lt, lp)) + self.iou.append(self.iou_score(lt, lp)) + self.hd.append(self.hausdorff_distance(lt, lp)) + + def dice_coefficient(self, y_true, y_pred, smooth=1e-5): + """二值 Dice,smooth 取 1e-5 避免抬高小目标分数(CODE-C5)。""" + y_true_f = y_true.flatten().astype(np.float32) + y_pred_f = y_pred.flatten().astype(np.float32) + # 双空:健康样本 + 模型未误检 → 完美预测 + if y_true_f.sum() == 0 and y_pred_f.sum() == 0: + return 1.0 + # 单空:漏检或误检 → 0 + if y_true_f.sum() == 0 or y_pred_f.sum() == 0: + return 0.0 + intersection = (y_true_f * y_pred_f).sum() + return (2.0 * intersection + smooth) / ( + y_true_f.sum() + y_pred_f.sum() + smooth + ) + + def iou_score(self, y_true, y_pred, smooth=1e-5): + y_true_f = y_true.flatten().astype(np.float32) + y_pred_f = y_pred.flatten().astype(np.float32) + if y_true_f.sum() == 0 and y_pred_f.sum() == 0: + return 1.0 + if y_true_f.sum() == 0 or y_pred_f.sum() == 0: + return 0.0 + intersection = (y_true_f * y_pred_f).sum() + union = y_true_f.sum() + y_pred_f.sum() - intersection + return (intersection + smooth) / (union + smooth) + + def hausdorff_distance(self, y_true, y_pred): + """从二值 mask 抽取前景点集后计算对称 HD(CODE-C4)。""" + # squeeze 掉可能的 channel 维,确保是 (H, W) + y_true = np.squeeze(y_true) + y_pred = np.squeeze(y_pred) + true_pts = np.argwhere(y_true > 0.5) # (N, 2) 像素坐标 + pred_pts = np.argwhere(y_pred > 0.5) + # 双空:健康样本预测正确,HD 定义上为 0 + if len(true_pts) == 0 and len(pred_pts) == 0: + return 0.0 + # 单空:HD 在数学上无定义,返回 NaN 由 nanmean 跳过 + if len(true_pts) == 0 or len(pred_pts) == 0: + return np.nan + d_ab = directed_hausdorff(true_pts, pred_pts)[0] + d_ba = directed_hausdorff(pred_pts, true_pts)[0] + return max(d_ab, d_ba) + + def get_results(self): + # 用 nanmean 跳过 HD 在单空 mask 上的 NaN(CODE-M2) + return { + "mean Dice": float(np.nanmean(self.dice)), + "mIoU": float(np.nanmean(self.iou)), + "mean HD": float(np.nanmean(self.hd)), + } diff --git a/code/ch13/snippet_07.py b/code/ch13/snippet_07.py new file mode 100644 index 0000000..55d88e4 --- /dev/null +++ b/code/ch13/snippet_07.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:570 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +from monai.metrics import ( + HausdorffDistanceMetric, SurfaceDistanceMetric, +) + +# HD95 +hd95_metric = HausdorffDistanceMetric( + include_background=False, + distance_metric="euclidean", + percentile=95, # ← 关键参数 + reduction="mean", +) + +# ASD(symmetric=True 即对称版本) +asd_metric = SurfaceDistanceMetric( + include_background=False, + symmetric=True, + reduction="mean", +) + +# y_pred / y 形状: (B, C, H, W), 二值或 one-hot +hd95_value = hd95_metric(y_pred=preds, y=labels) +asd_value = asd_metric(y_pred=preds, y=labels) diff --git a/code/ch13/snippet_08.py b/code/ch13/snippet_08.py new file mode 100644 index 0000000..59a7d18 --- /dev/null +++ b/code/ch13/snippet_08.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:613 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +import re +from collections import defaultdict + +def aggregate_dice_per_patient(images, preds, labels): + """images: 切片文件名列表;preds/labels: 与之等长的二值 mask 列表。""" + pattern = re.compile(r'(\d+)-slice') + inter_per_patient = defaultdict(float) + union_per_patient = defaultdict(float) + for img_name, p, l in zip(images, preds, labels): + pid = pattern.match(img_name).group(1) + inter_per_patient[pid] += float((p * l).sum()) + union_per_patient[pid] += float(p.sum() + l.sum()) + # 每个患者一个 Dice + dices = [ + (2 * inter_per_patient[pid] + 1e-5) / + (union_per_patient[pid] + 1e-5) + for pid in inter_per_patient + ] + return float(np.mean(dices)) diff --git a/code/ch13/snippet_09.py b/code/ch13/snippet_09.py new file mode 100644 index 0000000..f7b1b8d --- /dev/null +++ b/code/ch13/snippet_09.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:713 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-4 主函数中模型选项和损失函数选项 +# 模型选项(统一使用关键字传参 + opts.num_classes,CODE-M4) +model_map = { + 'unet': models.UNet(num_classes=opts.num_classes), + 'res_unet': models.res_unet34( + in_channel=3, + out_channel=opts.num_classes, + pretrain=True, + ), + 'nested_unet': models.NestedUNet( + num_classes=opts.num_classes, + input_channels=3, + deep_supervision=False, + ), + 'unext': models.UNext(num_classes=opts.num_classes), + 'cenet': models.CENet(num_classes=opts.num_classes), +} + +# 损失函数选项 +if opts.loss_type == 'cross_entropy': + # CODE-H1:pos_weight 不再硬编码 10,从训练集统计 + pos_weight_value = compute_pos_weight( + sorted(glob(os.path.join(opts.data_root, 'train_masks', '*.png'))) + ) + criterion = nn.BCEWithLogitsLoss( + pos_weight=torch.tensor([pos_weight_value]).to(device) + ) +elif opts.loss_type == 'dice_loss': + criterion = DiceLoss(smooth=1e-5) +elif opts.loss_type == 'focal_dice': + criterion = Focal_Dice_Loss() +elif opts.loss_type == 'bce_dice': + criterion = BCE_Dice_Loss() diff --git a/code/ch13/snippet_10.py b/code/ch13/snippet_10.py new file mode 100644 index 0000000..8d8cfaf --- /dev/null +++ b/code/ch13/snippet_10.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:803 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-5 2D 医学影像的推理 +import sys +sys.path.append("..") + +import os +import argparse +import numpy as np +import cv2 +import torch +from PIL import Image + +import models +from datasets import ( + ICHSegmentation, ISICSegmentation, BUSISegmentation, +) + + +def get_argparser(): + parser = argparse.ArgumentParser() + # 数据选项 + parser.add_argument( + "--data_root", type=str, default='', help="path to dataset", + ) + parser.add_argument( + "--dataset", type=str, default='ich', + choices=['ich', 'busi', 'isic'], + help='dataset name (controls preprocess pipeline)', + ) + # 模型选项 + parser.add_argument( + "--model", type=str, default='unet', + choices=['unet', 'res_unet', 'nested_unet', 'unext', 'cenet'], + help='model name', + ) + parser.add_argument( + "--model_root", type=str, help='path to model file', + ) + return parser + + +# CODE-H2:preprocess 按数据集分发,避免所有数据集复用 ICH 静态方法 +PREPROCESS_MAP = { + 'ich': ICHSegmentation.image_preprocess, + 'busi': BUSISegmentation.image_preprocess, + 'isic': ISICSegmentation.image_preprocess, +} + + +def inference(model, test_img, dataset_name): + img = Image.open(test_img).convert('RGB') + img = PREPROCESS_MAP[dataset_name](img) # 按数据集选 preprocess + img = torch.from_numpy(img).type(torch.float32) + img = img.unsqueeze(0).to('cuda') + + with torch.no_grad(): + outputs = model(img) + + # CODE-M5:显式 sigmoid + 阈值化,比 np.rint 隐式语义清晰 + probs = torch.sigmoid(outputs).cpu().numpy() + preds = (probs > 0.5).astype(np.uint8) * 255 + preds = np.squeeze(preds) + + # CODE-M6:用 splitext 而非 split('.')[0],对路径含点号鲁棒 + base = os.path.splitext(test_img)[0] + Image.fromarray(preds).save(f'{base}_pred.png') + + +if __name__ == "__main__": + opts = get_argparser().parse_args() + model_map = { + 'unet': models.UNet(num_classes=1), + 'res_unet': models.res_unet34( + in_channel=3, out_channel=1, pretrain=True, + ), + 'nested_unet': models.NestedUNet( + num_classes=1, input_channels=3, deep_supervision=False, + ), + 'unext': models.UNext(num_classes=1), + 'cenet': models.CENet(num_classes=1), + } + model = model_map[opts.model] + model.load_state_dict( + torch.load(opts.model_root)['model_state'] + ) + model.to('cuda') + model.eval() + print('model loaded.') + inference(model, opts.data_root, opts.dataset) diff --git a/code/ch13/snippet_11.py b/code/ch13/snippet_11.py new file mode 100644 index 0000000..2028115 --- /dev/null +++ b/code/ch13/snippet_11.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:938 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-6 基于 Tracing 的 PyTorch 模型表示 +import torch + + +class MyModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(4, 4) + + def forward(self, x, h): + new_h = torch.tanh(self.linear(x) + h) + return new_h, new_h + + +my_model = MyModel() +my_model.eval() +x, h = torch.rand(3, 4), torch.rand(3, 4) +traced_model = torch.jit.trace(my_model, (x, h)) +traced_model.save('model.pt') diff --git a/code/ch13/snippet_12.py b/code/ch13/snippet_12.py new file mode 100644 index 0000000..3256952 --- /dev/null +++ b/code/ch13/snippet_12.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:966 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-7 基于 Script 的 PyTorch 模型表示 +my_model = MyModel() # 实例化 +my_model.eval() # 切到 eval 模式(关闭 BN/Dropout 训练行为) +scripted_model = torch.jit.script(my_model) # ← 传入实例,不是类 +scripted_model.save('model.pt') diff --git a/code/ch13/snippet_13.py b/code/ch13/snippet_13.py new file mode 100644 index 0000000..4b761d4 --- /dev/null +++ b/code/ch13/snippet_13.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1001 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-8 PyTorch 模型的 TorchScript 转换 +import torch +from torchvision.models import resnet18 +from torchvision import transforms as T +from PIL import Image + +model = resnet18(weights="DEFAULT") # 真实预训练权重 +model.eval() + +# Dummy input 应尽量贴近真实分布——用真实预处理后的张量, +# 而非 torch.rand([0,1] 均匀分布与训练分布失配) +preprocess = T.Compose([ + T.Resize((224, 224)), + T.ToTensor(), + T.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]), +]) +example = preprocess(Image.open('sample.jpg').convert('RGB')).unsqueeze(0) + +with torch.no_grad(): + traced_script_module = torch.jit.trace(model, example) +traced_script_module.save("traced_resnet_model.pt") diff --git a/code/ch13/snippet_14.py b/code/ch13/snippet_14.py new file mode 100644 index 0000000..c7f3bf0 --- /dev/null +++ b/code/ch13/snippet_14.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1104 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 代码 13-13 Res-UNet 模型的 Script 转换 +import torch +from models.res_unet import res_unet34 + +model = res_unet34(in_channel=3, out_channel=1, pretrain=True) # 关键字传参 +model.load_state_dict( + torch.load('best_res_unet_ich.pt')['model_state'] +) +model.eval() # ① 切 eval 模式 + +with torch.no_grad(): + scripted_model = torch.jit.script(model) +scripted_model.save("./res_unet34_scripted.pt") + +# ② 数值一致性校验:导出前用同一输入比对 Python 与 Script 输出 +example = torch.randn(1, 3, 512, 512) +with torch.no_grad(): + py_out = model(example) + script_out = scripted_model(example) +assert torch.allclose(py_out, script_out, atol=1e-5), \ + "TorchScript 输出与 Python 输出数值不一致,部署前需排查!" +print("Python vs Script 一致性 OK.") diff --git a/code/ch13/snippet_15.py b/code/ch13/snippet_15.py new file mode 100644 index 0000000..0c41d1e --- /dev/null +++ b/code/ch13/snippet_15.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 13 章 — (无标号片段) +# 原始位置:book-review/drafts/ch13-v4.md:1279 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 13.11.5 MedSAM finetune 起手式 +import torch +import torch.nn as nn +from segment_anything import sam_model_registry + +# 加载 MedSAM ViT-B 权重(HuggingFace 上有镜像) +sam = sam_model_registry["vit_b"](checkpoint="medsam_vit_b.pth") + +# 冻结 image_encoder 与 prompt_encoder +for param in sam.image_encoder.parameters(): + param.requires_grad = False +for param in sam.prompt_encoder.parameters(): + param.requires_grad = False + +# 仅微调 mask_decoder(参数量约 4M,单卡 24G 可轻松训练) +optimizer = torch.optim.AdamW( + sam.mask_decoder.parameters(), + lr=1e-4, weight_decay=1e-2, +) + +# 损失:DiceLoss + BCEWithLogitsLoss(无显式权重) +dice_loss_fn = DiceLoss(smooth=1e-5) +bce_loss_fn = nn.BCEWithLogitsLoss() + +def loss_fn(logits, target): + return dice_loss_fn(logits, target) + bce_loss_fn(logits, target) + +# 输入分辨率 1024×1024(MedSAM 训练分辨率) +# Prompt 用 ground-truth mask 的外接 box(训练时由 GT 自动生成) diff --git a/code/ch14/README.md b/code/ch14/README.md new file mode 100644 index 0000000..ac891ad --- /dev/null +++ b/code/ch14/README.md @@ -0,0 +1,42 @@ +# 第 14 章 — 遥感与工业图像分割项目实战 · 配套代码 + +> 抽取自《深度学习图像分割》第 14 章修订稿 `book-review/drafts/ch14-v4.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +遥感影像与工业表面缺陷分割实战。 + +## 依赖 + +- torch>=2.0 +- rasterio / GDAL(遥感) +- albumentations + +## 文件清单 + +| 文件 | 状态 | 备注 | +| --- | --- | --- | +| `code_14-1.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_02.py` | 可独立运行 | py_compile 通过;未使用的 import (源稿教学保留) | +| `snippet_03.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_04.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_05.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_06.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_07.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_08.py` | 可独立运行 | py_compile + pyflakes 通过 | +| `snippet_09.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_10.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `snippet_11.py` | 教学片段 | 依赖书稿上下文的 import / 上层定义 | +| `scripts/snippet_01.sh` | 脚本 | 下载 / 安装命令 | +| `scripts/snippet_02.sh` | 脚本 | 下载 / 安装命令 | + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch14/code_14-1.py b/code/ch14/code_14-1.py new file mode 100644 index 0000000..a5b8822 --- /dev/null +++ b/code/ch14/code_14-1.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — 代码 14-1 是基于MONAI核心库的一个简单范例,旨在让读者对MONAI常用API有一个初步的了解。 +# 原始位置:book-review/drafts/ch14-v4.md:96 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 注:以下代码片段改编自 MONAI 官方教程,仅用于演示常用 API 调用形态; +# 其中 ExampleDataset 是占位类(读者实际使用时应替换为自定义 Dataset +# 或 monai.data.CacheDataset / DecathlonDataset 等具体实现, +# 完整的 Dataset 用法参见 14.3.2 节)。 + +# 导入相关 API +import numpy as np +import torch +from monai.data import DataLoader +from monai.metrics import ROCAUCMetric +from monai.networks.nets import DenseNet121 +from monai.transforms import ( + Compose, + EnsureChannelFirst, + LoadImage, + RandRotate, + ScaleIntensity, +) + +# 定义训练集增强方法 +train_transforms = Compose( + [ + LoadImage(image_only=True), + EnsureChannelFirst(), + ScaleIntensity(), + RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True), + # …(更多 transforms 略) + ] +) + +# 定义和导入数据集(ExampleDataset 为占位类) +train_ds = ExampleDataset(train_x, train_y, train_transforms) +train_loader = DataLoader(train_ds, batch_size=300, shuffle=True) + +# 设置相关训练组件和指定模型 +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +model = DenseNet121(spatial_dims=2, in_channels=1, + out_channels=num_class).to(device) +loss_function = torch.nn.CrossEntropyLoss() +optimizer = torch.optim.Adam(model.parameters(), 1e-5) +max_epochs = 4 +val_interval = 1 +auc_metric = ROCAUCMetric() + +# 执行训练 +for epoch in range(max_epochs): + model.train() + epoch_loss = 0 + step = 0 + for batch_data in train_loader: + step += 1 + inputs, labels = batch_data[0].to(device), batch_data[1].to(device) + optimizer.zero_grad() + outputs = model(inputs) + loss = loss_function(outputs, labels) + loss.backward() + optimizer.step() + epoch_loss += loss.item() + # … diff --git a/code/ch14/inventory.md b/code/ch14/inventory.md new file mode 100644 index 0000000..6290d6a --- /dev/null +++ b/code/ch14/inventory.md @@ -0,0 +1,27 @@ +# 第 14 章代码清单 + +- 源稿:`book-review/drafts/ch14-v4.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- | +| 1 | 74–77 | bash | `code/ch14/scripts/snippet_01.sh` | — | +| 2 | 96–156 | python | `code/ch14/code_14-1.py` | 代码 14-1 | +| 3 | 231–380 | python | `code/ch14/snippet_02.py` | — | +| 4 | 397–453 | python | `code/ch14/snippet_03.py` | — | +| 5 | 484–557 | python | `code/ch14/snippet_04.py` | — | +| 6 | 639–693 | python | `code/ch14/snippet_05.py` | — | +| 7 | 709–857 | python | `code/ch14/snippet_06.py` | — | +| 8 | 883–965 | python | `code/ch14/snippet_07.py` | — | +| 9 | 1033–1062 | python | `code/ch14/snippet_08.py` | — | +| 10 | 1072–1113 | python | `code/ch14/snippet_09.py` | — | +| 11 | 1121–1174 | python | `code/ch14/snippet_10.py` | — | +| 12 | 1178–1180 | bash | `code/ch14/scripts/snippet_02.sh` | — | +| 13 | 1248–1252 | math | `—` | — | +| 14 | 1256–1260 | math | `—` | — | +| 15 | 1268–1275 | math | `—` | — | +| 16 | 1283–1290 | math | `—` | — | +| 17 | 1298–1302 | math | `—` | — | +| 18 | 1337–1352 | python | `code/ch14/snippet_11.py` | — | diff --git a/code/ch14/scripts/snippet_01.sh b/code/ch14/scripts/snippet_01.sh new file mode 100644 index 0000000..b4460b2 --- /dev/null +++ b/code/ch14/scripts/snippet_01.sh @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:74 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +pip install monai +pip install monai-deploy-app-sdk diff --git a/code/ch14/scripts/snippet_02.sh b/code/ch14/scripts/snippet_02.sh new file mode 100644 index 0000000..992bea7 --- /dev/null +++ b/code/ch14/scripts/snippet_02.sh @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:1178 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +python app.py -i dcms -m models/best_unetr_btcv.ts diff --git a/code/ch14/snippet_02.py b/code/ch14/snippet_02.py new file mode 100644 index 0000000..69c1394 --- /dev/null +++ b/code/ch14/snippet_02.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:231 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入相关 API +from monai.transforms import ( + Activations, + AsDiscrete, + Compose, + ConvertToMultiChannelBasedOnBratsClassesd, + CropForegroundd, + EnsureChannelFirstd, + EnsureTyped, + LoadImaged, + NormalizeIntensityd, + Orientationd, + RandCropByPosNegLabeld, + RandFlipd, + RandRotate90d, + RandScaleIntensityd, + RandShiftIntensityd, + RandSpatialCropd, + ScaleIntensityRanged, + Spacingd, +) + +# 定义数据转换函数 +def get_transform(dataset): + # ============== BraTS(多模态 MR,3 类多标签:TC/WT/ET)============== + if dataset == 'brats': + # 训练集 transforms + train_transforms = Compose( + [ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys="image"), + EnsureTyped(keys=["image", "label"]), + # CODE-9:BraTS 4 类标签 → TC/WT/ET 3 个二值通道(与 sigmoid + to_onehot_y=False 损失配合) + ConvertToMultiChannelBasedOnBratsClassesd(keys="label"), + # 体素间距统一(image 双线性、label 最近邻;CODE-10) + Spacingd( + keys=["image", "label"], + pixdim=(1.0, 1.0, 1.0), + mode=("bilinear", "nearest"), + ), + Orientationd(keys=["image", "label"], axcodes="RAS"), + # CODE-11:BraTS 多模态 MR 强度归一化的标准做法 + # nonzero=True:背景大量 0 体素,仅在非零区域统计 mean/std + # channel_wise=True:FLAIR/T1/T1c/T2 4 个模态各自独立 z-score + NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True), + # 随机空间裁剪(BraTS 教程惯用 128³,比 BTCV 96³ 更大) + RandSpatialCropd( + keys=["image", "label"], + roi_size=(128, 128, 128), + random_size=False, + ), + RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=0), + RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=1), + RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=2), + # 随机强度扰动(BraTS 教程标配) + RandScaleIntensityd(keys="image", factors=0.1, prob=1.0), + RandShiftIntensityd(keys="image", offsets=0.1, prob=1.0), + ] + ) + # 验证集 transforms(无随机增广) + val_transforms = Compose( + [ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys="image"), + EnsureTyped(keys=["image", "label"]), + ConvertToMultiChannelBasedOnBratsClassesd(keys="label"), + Spacingd( + keys=["image", "label"], + pixdim=(1.0, 1.0, 1.0), + mode=("bilinear", "nearest"), + ), + Orientationd(keys=["image", "label"], axcodes="RAS"), + NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True), + ] + ) + + # ============== BTCV(单模态 CT,14 类多分类:13 器官 + 背景)============== + if dataset == 'btcv': + # 训练集 transforms + train_transforms = Compose( + [ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys=["image", "label"]), + Orientationd(keys=["image", "label"], axcodes="RAS"), + # CODE-10:image 双线性、label 最近邻(必须用元组而非单字符串 + # 否则 label 被插值成非整数 ID,DiceLoss 直接错乱) + Spacingd( + keys=["image", "label"], + pixdim=(1.5, 1.5, 2.0), + mode=("bilinear", "nearest"), + ), + # CT 有 HU 物理单位,可固定窗位窗宽(腹部软组织窗) + ScaleIntensityRanged( + keys="image", + a_min=-175, a_max=250, + b_min=0.0, b_max=1.0, + clip=True, + ), + # 沿前景裁剪降低背景体素占比;MONAI 1.3+ 默认 allow_smaller=False + # 必须显式传 True 以兼容小体积样本 + CropForegroundd( + keys=["image", "label"], + source_key="image", + allow_smaller=True, + ), + # 关键:基于阳/阴样本均衡的随机裁剪,是 BTCV 训练能收敛的核心 transform + RandCropByPosNegLabeld( + keys=["image", "label"], + label_key="label", + spatial_size=(96, 96, 96), + pos=1, neg=1, + num_samples=4, + image_key="image", + image_threshold=0, + ), + RandFlipd(keys=["image", "label"], prob=0.10, spatial_axis=0), + RandFlipd(keys=["image", "label"], prob=0.10, spatial_axis=1), + RandFlipd(keys=["image", "label"], prob=0.10, spatial_axis=2), + RandRotate90d(keys=["image", "label"], prob=0.10, max_k=3), + RandShiftIntensityd(keys="image", offsets=0.10, prob=0.50), + ] + ) + # 验证集 transforms(无随机增广,但保留前景裁剪与归一化) + val_transforms = Compose( + [ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys=["image", "label"]), + Orientationd(keys=["image", "label"], axcodes="RAS"), + Spacingd( + keys=["image", "label"], + pixdim=(1.5, 1.5, 2.0), + mode=("bilinear", "nearest"), + ), + ScaleIntensityRanged( + keys="image", + a_min=-175, a_max=250, + b_min=0.0, b_max=1.0, + clip=True, + ), + CropForegroundd( + keys=["image", "label"], + source_key="image", + allow_smaller=True, + ), + ] + ) + + return train_transforms, val_transforms diff --git a/code/ch14/snippet_03.py b/code/ch14/snippet_03.py new file mode 100644 index 0000000..1e1bbd4 --- /dev/null +++ b/code/ch14/snippet_03.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:397 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入数据集相关 API +from pathlib import Path +from monai.apps import DecathlonDataset +from monai.data import CacheDataset, SmartCacheDataset, load_decathlon_datalist + +# === BraTS === +train_transform, val_transform = get_transform("brats") + +# 定义 BraTS 训练集(DecathlonDataset 内部会自动管理 datalist) +train_dst = DecathlonDataset( + root_dir=opts.data_root, + task="Task01_BrainTumour", + transform=train_transform, + section="training", + download=False, + cache_rate=0.0, # 388 例多模态 4 通道全量缓存约 12 GB,按显存预算调整 + num_workers=4, +) + +# 定义 BraTS 验证集 +val_dst = DecathlonDataset( + root_dir=opts.data_root, + task="Task01_BrainTumour", + transform=val_transform, + section="validation", + download=False, + cache_rate=0.0, + num_workers=2, +) + +# === BTCV === +train_transforms, val_transforms = get_transform("btcv") + +# CODE-12:路径拼接用 pathlib,避免 "./datasets/data" + "btcv.json" 缺斜杠 +datasets_json = str(Path(opts.data_root) / "btcv.json") + +# 导入数据集 +datalist = load_decathlon_datalist(datasets_json, True, "training") +val_files = load_decathlon_datalist(datasets_json, True, "validation") + +# 定义 BTCV 训练集(30 例小数据集,全量缓存最高效) +train_dst = CacheDataset( + data=datalist, + transform=train_transforms, + cache_rate=1.0, # 全量缓存;cache_num 不再显式指定(避免与 cache_rate 双参冲突) + num_workers=8, +) + +# 定义 BTCV 验证集 +val_dst = CacheDataset( + data=val_files, + transform=val_transforms, + cache_rate=1.0, + num_workers=4, +) diff --git a/code/ch14/snippet_04.py b/code/ch14/snippet_04.py new file mode 100644 index 0000000..7ff43aa --- /dev/null +++ b/code/ch14/snippet_04.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:484 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 monai 相关分割模型 +from monai.networks.nets import SegResNet, UNETR, VNet, SwinUNETR + +# 定义模型获取函数 +def get_models(model_name, + in_channels, + out_channels, + img_size=None, + feature_size=None): + # SegResNet(CNN 编解码) + if model_name == 'segresnet': + model = SegResNet( + blocks_down=[1, 2, 2, 4], + blocks_up=[1, 1, 1], + init_filters=16, # Myronenko 2018 论文取 16;MONAI 默认 8 + in_channels=in_channels, + out_channels=out_channels, + dropout_prob=0.2, + # upsample_mode 默认 NONTRAINABLE(trilinear),论文用 pixel-shuffle + # 如需复刻论文可显式传 upsample_mode="pixelshuffle" + ) + + # VNet(CNN 编解码,3D U-shape) + if model_name == 'vnet': + # ⚠️ MONAI 1.3+ 已移除 dropout_prob,改用 dropout_prob_down / dropout_prob_up + # 1.4+ 下的等价写法见 14.11 节表 14-2;以下为 1.2 版本写法 + model = VNet( + spatial_dims=3, + in_channels=in_channels, + out_channels=out_channels, + dropout_prob=0.2, # 仅 1.2 兼容;1.3+ 报错 + dropout_dim=3, + # act 默认在 1.3+ 改为 ELU;论文(Milletari 2016)用 PReLU + # 如需复刻论文可显式传 act=("PRELU", {"num_parameters": 1}) + ) + + # UNETR(纯 ViT 编码 + CNN 解码) + if model_name == 'unetr': + model = UNETR( + in_channels=in_channels, + out_channels=out_channels, + img_size=(img_size, img_size, img_size), + feature_size=feature_size, + hidden_size=768, + mlp_dim=3072, + num_heads=12, + # CODE-2:1.4+ 已移除 pos_embed,改用 proj_type + # 注意 proj_type="conv" 是当前 MONAI 默认(patch 卷积) + # proj_type="perceptron" 才对应 UNETR 论文写法(一个全连接层),二者结构不等价 + proj_type="perceptron", + norm_name="instance", + res_block=True, + dropout_rate=0.0, + # qkv_bias 默认 False;UNETR 论文图示带 bias,如需复刻可显式 qkv_bias=True + ) + + # SwinUNETR(Swin Transformer 编码 + CNN 解码) + if model_name == 'swin_unetr': + # CODE-1:MONAI 1.3.1+ 已移除 img_size 参数(推理时由输入张量动态推断) + # 1.3+ 还引入 use_v2=True 切换到 Swin V2(双路 + 残差),建议默认开启以提升精度 + model = SwinUNETR( + in_channels=in_channels, + out_channels=out_channels, + feature_size=48, # 论文值;MONAI 默认 24 + drop_rate=0.0, + attn_drop_rate=0.0, + dropout_path_rate=0.0, + use_checkpoint=True, + use_v2=True, # 启用 SwinUNETR-v2(MONAI 1.3+) + ) + + return model diff --git a/code/ch14/snippet_05.py b/code/ch14/snippet_05.py new file mode 100644 index 0000000..c1dd548 --- /dev/null +++ b/code/ch14/snippet_05.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:639 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入 argparse 库 +import argparse + +# 定义配置管理函数,获取相关配置 +def get_argparser(): + parser = argparse.ArgumentParser() + + # 数据选项配置 + parser.add_argument("--data_root", type=str, + default="./datasets/data", + help="path to Dataset") + parser.add_argument("--dataset", type=str, default='brats', + choices=['brats', 'btcv']) + + # 模型选项配置 + parser.add_argument("--model", type=str, default='vnet', + choices=['vnet', 'segresnet', 'unetr', 'swin_unetr'], + help='model name') + parser.add_argument("--in_channels", type=int, default=4, + help="BraTS=4, BTCV=1") + parser.add_argument("--out_channels", type=int, default=3, + help="BraTS=3 (TC/WT/ET), BTCV=14 (13 organs + bg)") + parser.add_argument("--img_size", type=int, default=96, + help="UNETR input cube size") + parser.add_argument("--feature_size", type=int, default=16, + help="UNETR feature_size; SwinUNETR uses 48 internally") + + # 训练选项配置 + parser.add_argument("--epochs", type=int, default=100, + help="epochs for training") + parser.add_argument("--lr", type=float, default=1e-4, + help="learning rate") + # CODE-13:DataLoader batch_size 默认值(BraTS 显存大用 2,BTCV 96³ 用 1-2 即可) + parser.add_argument("--batch_size", type=int, default=2) + # CODE-6:损失类型选项 + parser.add_argument("--loss_type", type=str, default="dice_ce", + choices=["dice", "dice_ce", "dice_focal"], + help="loss function variant") + + # 推理选项配置 + parser.add_argument("--roi_size", type=int, default=96, + help='validation and inference roi size') + parser.add_argument("--data_idx", type=int, default=0, + help="index of sample for inference") + parser.add_argument("--model_root", type=str, + default="./checkpoints/best.pth") + + # Visdom 可视化选项配置 + parser.add_argument("--enable_vis", action='store_true', + default=False) + # … + + return parser diff --git a/code/ch14/snippet_06.py b/code/ch14/snippet_06.py new file mode 100644 index 0000000..d20eef0 --- /dev/null +++ b/code/ch14/snippet_06.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:709 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 前序代码略 +# … + +import torch +from torch.utils.data import DataLoader +from monai.losses import DiceLoss, DiceCELoss, DiceFocalLoss +from monai.metrics import DiceMetric +from monai.inferers import sliding_window_inference +from monai.transforms import Activations, AsDiscrete, Compose + +# 定义验证函数(CODE-17 补完整最小可运行体) +def validate(opts, model, loader, device, dataset): + """对整个 val_loader 跑一遍 sliding_window_inference + DiceMetric。""" + model.eval() + + # 按数据集区分后处理(CODE-14) + if dataset == "brats": + post_pred = Compose([Activations(sigmoid=True), + AsDiscrete(threshold=0.5)]) + post_label = lambda x: x # BraTS label 已经是 3 通道二值图 + else: # btcv + post_pred = Compose([Activations(softmax=True), + AsDiscrete(argmax=True, to_onehot=opts.out_channels)]) + post_label = Compose([AsDiscrete(to_onehot=opts.out_channels)]) + + dice_metric = DiceMetric(include_background=False, reduction="mean") + with torch.no_grad(): + for batch in loader: + x = batch["image"].to(device) + y = batch["label"].to(device) + with torch.autocast(device_type="cuda", dtype=torch.float16): + pred = sliding_window_inference( + inputs=x, roi_size=(opts.roi_size,)*3, + sw_batch_size=4, predictor=model, + overlap=0.5, mode="gaussian", + ) + pred = [post_pred(p) for p in pred] + y = [post_label(yy) for yy in y] + dice_metric(y_pred=pred, y=y) + mean_dice = dice_metric.aggregate().item() + dice_metric.reset() + return mean_dice + + +# 定义主函数 +def main(): + # 加载配置项 + opts = get_argparser().parse_args() + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # 加载数据集 + train_dst, val_dst = get_dataset(opts) + + # 数据导入(CODE-13:num_workers=4, pin_memory=True) + train_loader = DataLoader( + train_dst, batch_size=opts.batch_size, + shuffle=True, num_workers=4, + pin_memory=torch.cuda.is_available(), + ) + val_loader = DataLoader( + val_dst, batch_size=1, # 验证 batch_size 通常固定 1 + shuffle=False, num_workers=2, + pin_memory=torch.cuda.is_available(), + ) + + # 模型部分 + model_map = { + 'vnet': get_models('vnet', in_channels=opts.in_channels, + out_channels=opts.out_channels), + 'segresnet': get_models('segresnet', + in_channels=opts.in_channels, + out_channels=opts.out_channels), + 'unetr': get_models('unetr', in_channels=opts.in_channels, + out_channels=opts.out_channels, + img_size=opts.img_size, + feature_size=opts.feature_size), + 'swin_unetr': get_models('swin_unetr', + in_channels=opts.in_channels, + out_channels=opts.out_channels, + feature_size=48), + } + model = model_map[opts.model].to(device) + + # 损失函数(CODE-6:补 DiceCELoss / DiceFocalLoss 分支) + if opts.loss_type == 'dice': + if opts.dataset == 'brats': + criterion = DiceLoss(smooth_nr=0, smooth_dr=1e-5, + squared_pred=True, + to_onehot_y=False, sigmoid=True) + else: + criterion = DiceLoss(to_onehot_y=True, softmax=True) + elif opts.loss_type == 'dice_ce': + if opts.dataset == 'brats': + criterion = DiceCELoss(sigmoid=True, squared_pred=True, + lambda_dice=1.0, lambda_ce=1.0) + else: + criterion = DiceCELoss(to_onehot_y=True, softmax=True, + lambda_dice=1.0, lambda_ce=1.0) + else: # dice_focal + if opts.dataset == 'brats': + criterion = DiceFocalLoss(sigmoid=True, lambda_dice=1.0) + else: + criterion = DiceFocalLoss(to_onehot_y=True, softmax=True, + lambda_dice=1.0) + + # 优化器(4 个网络的推荐 lr/weight_decay 见 14.4 节末对照表) + optimizer = torch.optim.AdamW(model.parameters(), lr=opts.lr, + weight_decay=1e-5) + + # 调度器:朴素 cosine 衰减;SwinUNETR / UNETR 推荐带 warmup 的版本: + # from monai.optimizers import LinearWarmupCosineAnnealingLR + # scheduler = LinearWarmupCosineAnnealingLR( + # optimizer, warmup_epochs=opts.epochs // 20, max_epochs=opts.epochs) + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=opts.epochs) + + # CODE-7:混合精度训练初始化(PyTorch 2.x 推荐写法,旧写法见 14.11 节) + scaler = torch.amp.GradScaler('cuda') + + # 训练部分 + best_metric = -1.0 + for epoch in range(opts.epochs): + model.train() + epoch_loss = 0.0 + for batch_data in train_loader: + inputs = batch_data["image"].to(device) + labels = batch_data["label"].to(device) + optimizer.zero_grad() + with torch.autocast(device_type="cuda", dtype=torch.float16): + outputs = model(inputs) + loss = criterion(outputs, labels) + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + epoch_loss += loss.item() + scheduler.step() + + # 验证部分(CODE-17:BraTS / BTCV 都走 validate 函数,不再分支) + if (epoch + 1) % 5 == 0: + mean_dice = validate(opts, model, val_loader, + device=device, dataset=opts.dataset) + print(f"Epoch {epoch+1}: loss={epoch_loss:.4f}, mean Dice={mean_dice:.4f}") + if mean_dice > best_metric: + best_metric = mean_dice + torch.save({"model_state": model.state_dict(), + "epoch": epoch, "best_metric": best_metric}, + opts.model_root) diff --git a/code/ch14/snippet_07.py b/code/ch14/snippet_07.py new file mode 100644 index 0000000..afc7422 --- /dev/null +++ b/code/ch14/snippet_07.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:883 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入相关模块 +import numpy as np +import torch +import nibabel as nib +from monai.inferers import sliding_window_inference +from monai.transforms import Activations, AsDiscrete, Compose +from monai.apps import DecathlonDataset +from datasets.dataset import get_transform +from models.models import get_models + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# CODE-24:root_dir 用 opts.data_root,避免 hard-coded "../" +_, val_transform = get_transform("brats") +val_dst = DecathlonDataset( + root_dir=opts.data_root, + task="Task01_BrainTumour", + transform=val_transform, + section="validation", + download=False, + cache_rate=0.0, + num_workers=1, +) + +# 模型映射,略 +model_map = ... +model = model_map[opts.model].to(device) + +# CODE-25:兼容 model_state / state_dict / 直接 state_dict 三种 checkpoint 结构 +ckpt = torch.load(opts.model_root, map_location="cpu") +state_dict = ckpt.get("model_state", ckpt.get("state_dict", ckpt)) +model.load_state_dict(state_dict, strict=True) +model.eval() +print('model loaded.') + +# CODE-14:模型预测结果后处理 —— 按数据集区分 +if opts.dataset == "brats": + # BraTS 多标签:sigmoid + 阈值 + post_trans = Compose([Activations(sigmoid=True), + AsDiscrete(threshold=0.5)]) +else: # btcv 多类:softmax + argmax + onehot + post_trans = Compose([Activations(softmax=True), + AsDiscrete(argmax=True, to_onehot=opts.out_channels)]) + +# CODE-5:显式将 autocast 包到 sliding_window_inference 调用处 +# 注意:sliding_window_inference 不原生支持 val_amp 参数,此前版本用 inference() +# 自定义包装器,本版改为直接显式 wrap,更符合 MONAI 当前 API +with torch.no_grad(): + sample = val_dst[opts.data_idx] + test_input = sample["image"].unsqueeze(0).to(device) + with torch.autocast(device_type="cuda", dtype=torch.float16): + test_output = sliding_window_inference( + inputs=test_input, + roi_size=(96, 96, 96), + sw_batch_size=4, + predictor=model, + overlap=0.5, + mode="gaussian", + ) + # 取 batch 第 0 个,再做后处理 + test_output = post_trans(test_output[0]) # [C, D, H, W] + +# CODE-15:从输入 MetaTensor 读真实 affine(保留原始 spacing/orientation) +affine = sample["image"].meta["affine"].numpy() \ + if hasattr(sample["image"], "meta") else np.eye(4) + +# CODE-16:BraTS 三通道(TC/WT/ET)合并为 0/1/2/4 单通道标签图(BraTS 官方提交格式) +if opts.dataset == "brats": + pred_np = test_output.cpu().numpy() # [3, D, H, W] + seg = np.zeros(pred_np.shape[1:], dtype=np.uint8) + seg[pred_np[0] == 1] = 1 # ET(增强肿瘤)→ 1 + seg[pred_np[1] == 1] = 2 # TC(含 ET) → 2 + seg[pred_np[2] == 1] = 4 # WT(含全部) → 4 + nii_img = nib.Nifti1Image(seg, affine=affine) +else: # btcv:argmax 后已是单通道整数标签 + seg = test_output.argmax(dim=0).cpu().numpy().astype(np.uint8) + nii_img = nib.Nifti1Image(seg, affine=affine) +nib.save(nii_img, "test_seg.nii.gz") + +# 提示:更稳妥的做法是用 MONAI 的 SaveImaged transform 或 Invertd +# 把推理结果反向 transform 回原始空间后保存,可保证 spacing/orientation/origin 完全对齐。 diff --git a/code/ch14/snippet_08.py b/code/ch14/snippet_08.py new file mode 100644 index 0000000..7fddf93 --- /dev/null +++ b/code/ch14/snippet_08.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:1033 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入相关模块 +import torch +from models.models import get_models + +# 定义模型 +model = get_models('unetr', in_channels=1, out_channels=14, + img_size=96, feature_size=16) + +# 导入模型文件(兼容多种 checkpoint 键名) +ckpt = torch.load('best_unetr_btcv.pth', map_location="cpu") +state_dict = ckpt.get("model_state", ckpt.get("state_dict", ckpt)) +model.load_state_dict(state_dict) +model.to('cuda') +model.eval() +print("model loaded.") + +# 通过 jit.script 进行模型转换 +try: + scripted = torch.jit.script(model) + scripted.save("models/best_unetr_btcv.ts") +except Exception as e: + # CODE-26:UNETR 内含 nn.MultiheadAttention 与可变长度 transformer 块, + # 1.4+ 引入的 Flash Attention 路径含动态分支,部分版本 jit.script 会失败; + # 此时回退到 jit.trace 是更稳妥的备选方案 + print(f"jit.script failed: {e}; fallback to jit.trace") + dummy = torch.randn(1, 1, 96, 96, 96, device="cuda") + traced = torch.jit.trace(model, dummy) + traced.save("models/best_unetr_btcv.ts") diff --git a/code/ch14/snippet_09.py b/code/ch14/snippet_09.py new file mode 100644 index 0000000..a1b1f36 --- /dev/null +++ b/code/ch14/snippet_09.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:1072 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 导入相关模块 +from monai.deploy.core import (AppContext, ConditionType, Fragment, + Operator, OperatorSpec) +from monai.deploy.operators.monai_seg_inference_operator import ( + InMemImageReader, MonaiSegInferenceOperator, +) + +# 定义分割运算符 +class SegOperator(Operator): + # CODE-8:__init__ 必须把 fragment / app_context / model_path 等字段保存到 self + # 否则 compute() 中读取这些属性会立即 AttributeError + def __init__(self, fragment, app_context, model_path, + *args, **kwargs): + self.fragment = fragment + self.app_context = app_context + self.model_path = model_path + # 调用父类 __init__(Holoscan-based v2.x 必需;0.6.x 兼容) + super().__init__(fragment, *args, **kwargs) + + def compute(self, op_input, op_output, context): + # 读取输入影像(pre_transforms / post_transforms 应在本节其他位置定义) + # … + # 推理运算符 + infer_operator = MonaiSegInferenceOperator( + self.fragment, # 已修正拼写:fragement → fragment + roi_size=(96, 96, 96), + pre_transforms=pre_transforms, + post_transforms=post_transforms, + overlap=0.5, + app_context=self.app_context, + model_path=self.model_path, + ) + + # 前处理 + def pre_process(self, img_reader, out_dir): + pass + + # 后处理 + def post_process(self, out_dir): + pass diff --git a/code/ch14/snippet_10.py b/code/ch14/snippet_10.py new file mode 100644 index 0000000..2a468c6 --- /dev/null +++ b/code/ch14/snippet_10.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:1121 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 定义分割应用 +class AISegApp(Application): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # compose 方法将各运算法链接起来 + def compose(self): + # … + # DICOM 导入运算符 + study_loader_op = DICOMDataLoaderOperator( + self, CountCondition(self, 1), input_folder=app_input_path, + name="dcm_loader_op") + # 序列选择运算符 + series_selector_op = DICOMSeriesSelectorOperator( + self, rules=Sample_Rules_Text, name="series_selector_op") + # 体数据转换运算符 + series_to_vol_op = DICOMSeriesToVolumeOperator( + self, name="series_to_vol_op") + # 分割运算符 + seg_op = SegOperator(self, app_context=app_context, + model_path=model_path, name="seg_op") + # STL 文件转换运算符 + stl_conversion_op = STLConversionOperator( + self, + output_file=app_output_path.joinpath("stl/mesh.stl"), + keep_largest_connected_component=False, + name="stl_op", + ) + # … + # DICOM 分割结果写入运算符 + dicom_seg_writer = DICOMSegmentationWriterOperator( + self, segment_descriptions=segment_descriptions, + output_folder=app_output_path, name="dcm_seg_writer_op", + ) + + # CODE-28:按顺序逐个链接 + # 端口连接形式:0.6.x 接受 Set[Tuple[str,str]](如下); + # Holoscan-based v2.x 推荐 Dict[str,str] 形式:{"output_a": "input_b"} + self.add_flow(study_loader_op, series_selector_op, + {("dicom_study_list", "dicom_study_list")}) + self.add_flow( + series_selector_op, series_to_vol_op, + {("study_selected_series_list", "study_selected_series_list")}, + ) + self.add_flow(series_to_vol_op, seg_op, {("image", "image")}) + self.add_flow(seg_op, stl_conversion_op, {("seg_image", "image")}) + self.add_flow( + series_selector_op, dicom_seg_writer, + {("study_selected_series_list", "study_selected_series_list")}, + ) + self.add_flow(seg_op, dicom_seg_writer, {("seg_image", "seg_image")}) + # … diff --git a/code/ch14/snippet_11.py b/code/ch14/snippet_11.py new file mode 100644 index 0000000..479e615 --- /dev/null +++ b/code/ch14/snippet_11.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# 来源:《深度学习图像分割》第 14 章 — (无标号片段) +# 原始位置:book-review/drafts/ch14-v4.md:1337 +# 本文件由 book-review/scripts/extract_code.py 自动抽取生成。 +# 修改请回写源稿后重新生成,避免代码与书稿失同步。 +# 旧写法(PyTorch 1.x,仍可用但有 deprecation warning) +from torch.cuda.amp import autocast, GradScaler +scaler = GradScaler() +with autocast(): + outputs = model(inputs) + loss = criterion(outputs, labels) +scaler.scale(loss).backward() + +# 新写法(PyTorch 2.x 推荐,需 PyTorch >= 2.3) +scaler = torch.amp.GradScaler('cuda') +with torch.autocast(device_type='cuda', dtype=torch.float16): + outputs = model(inputs) + loss = criterion(outputs, labels) +scaler.scale(loss).backward() diff --git a/code/ch15/README.md b/code/ch15/README.md new file mode 100644 index 0000000..c29d788 --- /dev/null +++ b/code/ch15/README.md @@ -0,0 +1,22 @@ +# 第 15 章 — 全书总结与展望 · 配套代码 + +> 抽取自《深度学习图像分割》第 15 章修订稿 `book-review/drafts/ch15-v1.md`。 +> 抽取脚本:`book-review/scripts/extract_code.py`。 +> 验证脚本:`book-review/scripts/validate_code.py`。 + +## 概览 + +全书总结、方法谱系梳理与发展展望,无配套代码。 + +## 文件清单 + +(本章为概念性 / 综述章节,正文无独立可运行代码。可参考 `inventory.md` 中对源稿 fenced block 的盘点。) + +## 状态说明 + +- **可独立运行**:单文件 `python file.py` 通过语法校验且无 undefined name。实际跑通仍可能依赖外部数据/模型权重。 +- **教学片段**:源自书稿讲解,依赖书稿上下文中已 `import` 或定义的模块/类。可作为参考阅读,需结合书稿前后文方能运行。 +- **语法骨架**:源稿用 `def f(self, ...):` 等 Ellipsis 占位、或在 module top-level 出现 `return` 等纯教学截断写法,本仓库保持与书稿一致,**不修改源码**;标注此状态以提示读者。 +- **需 OpenCV 4.x / LibTorch**:C++ 示例,依赖外部 SDK 才能编译。 + +详细 fenced block 盘点见 [`inventory.md`](inventory.md)。 diff --git a/code/ch15/inventory.md b/code/ch15/inventory.md new file mode 100644 index 0000000..35e77e7 --- /dev/null +++ b/code/ch15/inventory.md @@ -0,0 +1,9 @@ +# 第 15 章代码清单 + +- 源稿:`book-review/drafts/ch15-v1.md` +- 自动抽取生成:`book-review/scripts/extract_code.py` + +## 全章 fenced block 一览 + +| # | 原稿行 | 语言 | 抽取后文件 | 来源标号 | +| - | ----- | ---- | ---------- | -------- |