(三:2020.07.10)nnUNet附录解析(7.27更新认识)


1.本文是对该论文涉及的Methods进行解读和理解,参考前两篇的解读论文主题解读主要方法解读
更新于7.27,添加对于推理中的预处理部分的理解,请务必参考第二篇解读来理解。


A. Dataset details

       table A提供了包含数据集来源的手稿,这里记录的数值都是通过这些对应的数据集计算出来的。
           1.打 * 的代表数据集中有多重标注(还没有具体看一下有什么差别)
           2.MSD的肝脏数据集做了一些小的修改


B. nnU-Net Design Principles(启发式规则的设计)

       这里提到了一些nnUNet的principles,阐述了他们的概念。可以根据网上的代码来具体了解下这些是怎么实现的。


B.1 蓝图参数

1. 网络架构设计决策

  • ① 形如UNet的网络架构,只要设置足够好管道参数,就能达到SOTA水平。根据我们的经验,花里胡哨的网络结构的变异对于提升模型表现并不是必要的。
  • ② 我们的网络仅仅使用了平面的卷积、instance_normalization和Leaky_Relu,每一个采样块的操作就是卷积 ----> instance_normalization -----> Leaky_Relu。
  • ③ 我们在同一像素的stage(对称位置)的编码区域和解码区域都使用这个采样块(需要看下拓扑图确定有几块)
  • ④ 下采样是有步长的卷积(新的分辨率层的第一块的卷积的步长 > 1)。上采样采用的是转置卷积。我们注意到,并未观察到该方法与其他方法(如最大池法、bi/ triinear上行采样法)在分割精度上的实质性差异。

2. 选择最好的UNet配置

       很难确切的说在某种数据集上哪种配置最好。为了达到这个目的,nnUNet设计了以下三个独立的网络配置,同时,nnUNet可以根据交叉验证(看下后面的推理参数)的结果为你自动选择一个最好的网络配置。预测什么数据集需要什么配置是未来的一个研究方向。

  • ① 2D_Unet:在全像素数据上运行。对于具备各向异性的数据,我们期待这个发挥更大的功效;
  • ② 3D_full_resolution_Unet:在全像素数据上运行。patch_size被GPU的显存所限制,但这个基本在所有的数据上的都保证了好的表现。但是,对于一些大的数据来说,patch_size可能会有点小,不足以获得足够的上下文信息。
  • ③ 3D_UNet_cascade:专门为一些大体积的数据而设计,首先,用一个3D_Unet在低分辨率上进行一次粗糙的图像分割,然后通过第二个3D_Unet对之前生成的分割图像进行一次在高分辨率上的操作。

3. 训练计划

  • ① 首先使epoch:所有的训练都按照初始的1000epoch在跑,每一轮要进行250次的迭代(使用nnUNet)。经验之谈,训练的时间越短可能效果越好。
  • ② 现在说一下优化阶段:经验之谈,0.01的初始学习率和nesterov动量规则会有最好的效果。训练之中使用"poly_Learning_Rate"来进行学习率的衰减,几乎使学习率线性下降为0。
  • ③ 数据增强:数据增强对于实现最好的效果十分有必要,但是在整个训练过程中运用动态的数据增强会更好,同时. with associated probabilities to obtain a never ending stream of unique examples (参考Section D )
  • ④ 样本类别平衡问题:对于医学图像领域来说,这是一个棘手的问题。对于前景的过采样可以很好的解决找个问题,但是也别采样的太过分,因为网络也会注意到背景数据的一些变化。
  • ⑤ Dice损失函数对于处理样本类别平衡问题也很合适,但是也有它自己的缺点。Dice损失直接可以对评估算法进行优化,但是因为patch是基于训练的,所以实际中仅仅是近似它。而且,实验中发现类别的过采样会使得类别分布有一定的倾斜。因此,经验之谈,我们将Dice loss和CE loss进行组合,以此来增加训练的稳定性和分割的精度。

4. 推理

  • ① 每一折的验证集都会被这一折独立训练出来的模型进行验证。每一折训练一个模型只是为了之后做组合来预测。
  • ② 推理阶段的patch和训练阶段的patch是一致的,不建议使用全卷积的推理方式,因为这样会导致0填充卷积和Instance_normalization的问题
  • ③ 为了避免拼接出现的伪影,通过设置1/2的patch_size大小的距离来进行临近点预测。边缘部分的预测将更不精确,这就是为什么我们要为softmax的聚类使用高斯重要性加权(中心点的权重比边缘的权重更高)。

B.2 推理参数

       这些参数在数据集中不是固定的,而是根据你自己的准备训练的数据的“数据指纹”(数据属性的低纬度表示),在训练过程中进行动态调整的。

1.网络动态自适应

  • ① 在训练过程中,网络结构需要自动适应输入patch的尺寸和spacing,以确保网络能接受的区域大小覆盖整个输入;
  • ② 不断的进行下采样来聚合信息,直到特征图达到最小值(4x4x4)。
  • 因为编码阶段和解码阶段的每个像素层的block的数量都是固定的,那么网络的深度就会与输入patch_size的大小相对应。网络中卷积层的总数(包括分割层)应该是
    5 k + 2 , 5 * k +2,
    其中,k是下采样的次数(5 per downsampling stems from 2 convs in
    the encoder, 2 in the decoder plus the convolution transpose)
  • ④ 除了解码器的最底下两层之外,所有的解码层都用了额外的损失函数,只为让更多的梯度信息注入网络中。
  • ⑤ 对于各向异性的数据来说,池化会只在平面之间进行,直到轴之间的像素值匹配为止。刚开始时3D卷积会使用一个1x1大小的卷积核来在平面外的轴(z)上进行卷积,通过这种方式来防止离得较远的切片产生信息的聚合。一旦这个轴越卷越小,下采样就会单独为这个轴停止卷积。【z轴的1x1卷积核什么时候变的,不变的话如何让z越卷越小的呢?

2.输入patch_size的配置

  • ① 在batch_size为2的基础上,同时受到GPU的限制patch_size应当越大越好,这样所能得到的上下文的信息就越大。
  • ② patch大小的纵横比应该是,训练的每一套CT重采样以后的中值形状。

3. batch_size

  • batch_size的最小值应当为2,因为如果是在minibatch的更少的样本下训练,梯度下降中的噪声将会增多。
  • 如果GPU的显存在设置完patch_size之后仍然有剩余,那么应该不断增大batch_size直到显存溢出为什么我训练的时候并不增长呢?】。

4.目标间隔和重采样

  • ① 对于各向同性的数据,所有训练集CT的体素尺寸的中位数作为默认值。然后利用三阶样条插值(对数据)和线性插值(像训练的标签那样的独热编码分割图)进行重采样,会得出一个比较好的结果。
  • ② 对于各向异性的数据,平面以外的轴(z)的目标间隔应当比这个轴上的中位数要小,这样就会生成尽量高分辨率的图像,可以减少重采样的伪影。为了实现这样的操作,我们把所有该轴上的spacing的值从小到大排列,取在第10%位置的那个数,作为最终的目标间隔。z轴的重采样无论是对数据还是对标签(one-hot),都采用最临近插值算法插值。

4.1: 更新于7.27,修改了对于推理中的预处理部分的理解,请务必仔细看第二篇解读新增部分4.2.1来理解。


5.强度归一化

  • 对除了CT外的其他模态的方法:对每张图片进行Z-Score(每个像素值减去所有像素平均值,然后除以标准差)是一个好的默认方法。
  • 对CT的方法:上面设置的Z-Score是默认的,而nnUNet对CT采用不同的方法,比如通过找到训练集中每一套CT的前景像素值进行全局归一化。

B.3 经验参数

       这些参数是不能通过数据指纹简单得到的,是需要监督训练过后的验证表现来确定的。

  • ① 模型选择:
    即使是当3D_full_resolution_UNet整体表现都不错的时候,一个特定任务的最好模型选择都不可能很精准。因此,nnUNet会生成3个UNet配置并且会在交叉验证后自动选择一个表现最好的方法(独立或者结合);
  • ② 后处理:
    医学图像数据的目标结构经常包含一个实例,所以这个先验知识经常被来进行图像分割,即通过连接分支分析算法来进行预测,同时移除除了最大组件外的所有其他组件。是否应用这个算法是由交叉验证之后的验证表现决定的。总的来说,就是通过移除最大组件之外的其他组件可以明显提高Dice系数的时候,后处理会被触发。

C. Analysis of exemplary nnU-Net-generated pipelines(可效仿的nnUNet自生成管道的分析)

       在这个部分,我们会简短的介绍以下D13(ACDC)和D14(LiTS)两个数据集的实验结果,这样对于nnUNet怎么设计“管道”以及为什么设计“管道”就会有一个直观的理解。

C.1 ACDC

Figure C.1是nnUNet为ACDC生成的“管道”:

pipe


1. 数据描述

       ACDC是MICCAI在2017年举办的竞赛,这是数据集地址。在这个竞赛中,参赛者被要求从心脏的MRI中分割出右心室、左心肌和左心室的腔。每个病例对应着两个标签,所以100个病例对应的标签的个数总共是200。电影磁共振成像的一个关键特性是切片采集在多个心跳周期和屏气中进行。这将导致有限的切片数量,从而导致低的平面外分辨率(z)以及切片图像失调的可能性。图C.1提供了nnUNet为这个数据集生成的“管道”的一个摘要。这个典型的图片形状是(每个轴上的中位数)9x237x256,而体素大小是10x1.56x1.56mm。


2. 强度归一化

       对于MRI图像来说,nnUNet的归一化方式是:图片的像素值(强度值)先减去他们的均值,再除以他们的标准差。


3. 2D_UNet

       1.56x1.56mm是确定的平面内的目标间隔的大小,这一点上是和3D_full_resolution_UNet一样的。因为2D_Unet仅仅对每一层的切片进行操作,所以它的平面外的轴(z)的像素值不会发生变化,这就导致不同的数据集往往这个值都是不同的。按照线上的方法,2D_Unet有一个256x224大小的patch_size,能够完全覆盖典型的图像重采样之后的尺寸(237x208)。


4. 3D_UNet

  • 这个数据集尺寸和间隔的各向异性导致了,在3D_full_resolution_UNet的情况下,平面外轴(z)的目标间隔被设定为5mm(这个值依旧是按照之前所述的规则,即将所有病例的目标间隔进行排序,然后选择处于第10%这个位置的目标间隔的值)。在这个ACDC数据集中,因为层与层的间隔很大,所以层与层的分割边缘也会很大。选择更小的目标间隔就会使得上采样用于训练和下采样用于输出分割的图片更多。选择这个变量而不是中值变量的原因正是如此,产生更多的图片用于上采样和下采样,自然也就能够有利于消除插值伪影。
  • 同时,注意这个z轴的重采样要用最邻近插值,3D_full_resolution在重采样之后的中值图片形状是18x237x208.而线上的方法所述的这个nnUNet进行网络训练的patch_size是20x256x224,对用的能够适应GPU的batch_size是3。
  • 注意在3D_UNet中,卷积核是如何从1x3x3(2D的3x3卷积核对于本身是十分有效的)开始计算的。原因是因为,体素间隔的差距很大并且每一层有太大的差距,所以图像信息的聚合可能并不是那么有用(卷积核的z轴方向大小为1,即使是换成2,因为z轴方向上的体素大小不同,信息差距也大,这样卷没有意义)。类似地,平面内的池化也用1x2x2的卷积核,直到平面内轴和平面外轴的间距小于1/2,仅仅在间隔大小和池化的大小大致相当时,卷积核才变得各向同性

5. 3D_UNet_cascade

       由于上一个3D_UNet已经覆盖了整个中值图像尺寸,因此UNet_cascade是没必要的。


6. 训练和后处理

  • 在训练过程中,为3D_UNet在平面内使用空间增强的手段(例如缩放和旋转),仅仅是为了消除不同的切片进行重采样以后造成的插值伪影。
  • 对于每个UNet配置都使用五折交叉验证,我们分别进行推理,以确保病例被合适地分层(因为每个病例有两张图片)。幸亏有交叉验证这种手段,让nnUNet可以在整个数据集上进行验证和结合。最后,着五个交叉验证被合在一起。nnUNet通过计算所有病例所有分割出来地前景地平均dice,得出一个标量值,从而来衡量模型的表现。详细的信息在这里不做赘述(附录F里有提到)。根据这个评估的方法,2D_UNet的得分是0.9165,3D_full_resolution的得分是0.9181,结合推理的得分是0.9228。因此,结合推理的方法将会用来进行预测测试集的效果。
  • 后处理在结合推理中进行了配置,去小分支算法对于分割右心房和左心腔十分有用。

C.2 LiTS

Figure C.1是nnUNet为LiTS生成的“管道”:

LiTS


1. 数据描述

       LiTS也是2017的MICCAI的竞赛项目,并且提供了质量相当高的数据集,是用来分割肝脏和肝脏肿瘤的数据集。有131套CT用来训练,70套用来测试,同时测试的标签只有举办方知道。 中值图像尺寸为432x512x512,对应的体素间隔是1x0.77x0.77。


2. 强度归一化

       CT的每个体素的强度值是和每层切片的定量物理属性有关系的,因此我们期望得到的强度值是相对连续的。nnUNet将这个属性加以利用,所以采用了全局强度归一化的方法(与上一个ACDC相反)。
       最后,nnUNet将归一化以后的强度信息作为数据指纹的一部分:所有的样本中,属于任何前景(肝脏和肝脏瘤)的归一化后的强度值被收集起来,然后,Then, the mean and standard deviations of these values as well as their 0.5 and 99.5 percentiles are computed. Subsequently, all images are normalized by clipping them to the 0.5 and 99.5 percentiles, followed by subtraction of the global mean and division by the global standard deviation.(这些强度值的均值和标准差,以及均值的0.5%和标准差的99.5%都会用来计算。随后,所有的图像会被归一化到0.5%和99.5%,之后减去全局均值再除以全局标准差。)


3. 2D_UNet

       2D_UNet的目标间隔被设定为NAx0.77x0.77mm(通过所有训练案例中的中值体素间隔来确定)。因为2D_UNet仅仅在切片上进行操作,所以z轴不用管。对训练案例的重采样会导致一个NAx512x512的中值图像尺寸(NA说明这个轴没有进行重采样)。由于这个尺寸是中值尺寸,所以在训练集中的CT尺寸可能大于它也可能小于它。2D_UNet配置patch_size是512x512,batch_size为12。


4. 3D_UNet

  • 1.3D_UNet的目标间隔被设定为1x0.77x0.77mm(通过所有训练案例中的中值体素间隔来确定)。因为中值间隔几乎是各向同性的,nnUNet在此不会使用ACDC中的10th_percentile的方法来确定z轴上的间隔。
  • 2.重采样的策略是由每张切片决定的:
    • 如果该切片是各向同性的(间隔最大的轴的间隔 / 间隔最小的轴的间隔 < 3),就会用三阶样条插值对原始训练数据切片进行插值,然后用线性插值对数据切片对应的标签切片进行插值(对标签的插值,要在重采样之前将标签切片转换成独热编码,之后插值完了以后再转换成标签格式)。
    • 如果该切片是各向异性的,nnUNet在z轴的重采样应该像ACDC那样做。(这里说明了一点:对于CT图像的3D_UNet来说,xyz的插值都要进行,如果一个切片是各向同性的,在三个轴上的插值算法都一样,均为三阶样条插值(data)和线性插值(seg),如果一个切片是各向异性的,xy仍然采取之前的策略,而z轴的插值就要采用ACDC的方法进行插值
  • 3.在重采样以后,中值图像尺寸为482x512x512。因为要在GPU允许的情况下尽可能给大patch_size,在这个大的patch_size情况下尽可能增大batch_size。因此3D_UNet的patch_size的大小为128x128x128,对应的batch_size为2(启发性规则限定下允许的最小值)。由于输入的patch基本上都有各向同性的间隔,所以所有的卷积核尺寸和下采样的步长都是各向同性的(3x3x3和2x2x2)。

5. 3D_UNet_cascade

       尽管nnUNet优先选择较大的patch,3D_full_resolution_UNet对于覆盖更多的上下文信息来说仍然太小了(仅仅覆盖了重采样后的中值图片尺寸的1/60),这可能会导致分类的错误,因为缩小的太严重了,比如说,这样就会很难区分脾脏和肝脏。nnUNet就是为了应对这种问题,通过首先用3D_UNet训练一个下采样的数据,然后提取出低分辨率的输出作为第二个UNet的输入。使用我们线上描述的步骤(比如方法4图E.1 b ),low_resolution_3D_UNet的目标间隔被设定为2.47x1.9x1.9,会生成一个尺寸为195x207x207的中值图像。3D_UNet_low_resolution的patch为128x128x128,batch_size为2。注意这些配置和3D_UNet是完全一致的,但是其他对于其他数据集来说不一定是这样。如果3D_full_resolution_UNet的数据是各向异性的,那么nnUNet会优先对高分辨率的轴进行下采样,从而生成一个不同的网络结构、patch_size和batch_size。在3D_low_resolution_UNet进行完五折交叉验证之后,每次交叉验证的验证集的分割图像就会上采样至3D_full_resolution_UNet的目标间隔。而这个级联方式中的full_resolution_UNet(和一般的3D_full_resolution_UNet一样)就会被训练用于修正粗糙的分割图像,并且改正遇到的错误(通过把上采样分割图的独热编码和网络的输入联系起来)。


5.训练和后处理

       所有的网络配置都依赖于五折交叉验证,nnUNet会一次次地计算所有类别地前景的dice分数,由此生成一个标量,从而估计所该有的配置。基于这个评估方法,2D的score是0.7625,3D_full_resolution_UNet是0.8044,cascade的low_resolution的分数为0.7796而full_resolution的分数为0.8017,组合后的最好分数是0.8111。后处理会在组合模式的基础上进行,会通过对前景使用去小连接分支算法,通过这样的方法可以比较好的提升模型的表现。


D Details on nnU-Net’s Data AugmentationnnUNet(数据增强的细节)

1. 一般数据增强

       训练期间应用了很多数据增强的手段,所有的数据增强都在CPU上进行计算。数据增强的“管道”使用了我们之前分享的一个数据增强包:batchgenerators。不同数据集之间的数据增强参数不变。
       采样的patch比刚开始的用于训练的patch_size要大,这导致在应用旋转和缩放时,在数据增强期间引入的边界值(这里是0)更少。所以,要在旋转和缩放的时候,将patch从中心开始抠图成最后的patch_size大小。为了确保原始数据的边界出现在最终的patch中,最开始的抠图部分可能会延伸到图像的边界外。
       空间内的增强(旋转、缩放、低分辨率模拟)被应用在3D_Unet的3D、2D_Unet的2D、或者带有各向异性patch_size的3D_UNet(patch_size的最长边比最小边大三倍)。
       为了增强生成patch的可变性,大多数的增强都因为一些参数而不同(取自好的某个范围之内)。比如,x ∼ U(a, b)就代表x是从a和b的均匀分布间进行取值。而且,所有的增强都是根据预先设定的概率随机地应用的。
       下面是nnUNet运用的数据增强方法(按照标出的顺序)

  • 1. 旋转和缩放
    • 旋转和缩放的一起应用有助于加快计算的速度,这个方法将需要的数据插值的次数减少到1。使用缩放和旋转的概率各为0.2(只缩放的概率为0.16,只旋转的概率为0.16,两个都触发的概率为0.08)。
    • 旋转:如果是要处理各向同性的3D patch,应该让x、y、z三个轴分别在(-30,30)之间均匀随机取值。如果这个patch是各向异性的(或者2D)旋转的角度采样范围应为(-180,180)。如果2D的patch_size是各向异性的,角度应当在(-15,15)采样。
    • 缩放:缩放是通过将坐标与体素网格中的缩放因子相乘实现的。因此,比例因子小于1会产生“缩小”效果,而数值大于1会产生“放大”效果。对于所有的patch类型,尺度因子从U(0.7, 1.4)中采样。
  • 2. 高斯加噪
    • 将零中心的加性高斯噪声独立地添加到样本中的每个体素中。这个增加的概率为0.15。噪声的方差是从U(0, 0.1)提取的(注意,由于强度归一化,所有样本中的体素强度均值和单位方差都接近于零)。
  • 3. 高斯模糊
    • 每个样本使用高斯模糊的概率是0.2。但是如果在一个样本中应用了这个模糊,与之相关的模态的应用概率变为0.5(单一模态为0.1)。
    • 高斯核的宽应当从每一个模态在(0.5,1.5)均匀随机采样。
  • 4. 亮度处理
    • 体素强度要以0.15的概率与在(0.7, 1.3)均匀随机采样的值相乘。
  • 5. 对比度处理
    • 体素强度以0.15的概率与在(0.65,1.5)均匀随机采样的值相乘,乘完之后把这个值裁剪到他们原始强度范围内。
  • 6. 低像素仿真
    • 这种增强以每个样本0.25和每个相关模态0.5的概率应用。触发的模态需要采用最近邻插值以U(1,2)向下采样,然后使用三次插值将其采样回原始大小。对于2D patch或各向异性3D patch,这种增强只应用于2D中,而使平面外轴(如果适用)保持其原始状态。
  • 7. 伽马增强
    • This augmentation is applied with a probability of 0.15. The patch intensities are scaled to a factor of [0, 1] of their respective value range. Then, a nonlinear intensity transformation is applied per voxel: inew = iγold with γ ∼ U(0.7, 1.5). The voxel intensities are subsequently scaled back to their original value range. With a probability of 0.15, this augmentation is applied with the voxel intensities being inverted prior to transformation: (1 1 inew) = (1 1 iold)γ. 【32页】
  • 8. 镜像
    • patch的所有轴都按照0.5的概率进行镜像。

2. 特别数据增强

       对于UNet_cascade的full_resolution_UNet来说,额外对low_resolution_3D_UNet生成的mask采用下面的增强方法。注意这个mask是按独热编码进行保存的。

  • 1. 二值操作
    • 对所有的labels以0.4的概率进行这个二值操作,这个操作是从膨胀、腐蚀、开操作、闭操作中随机选取的。结构元素是一个半径为r ~ U(1,8)的球体。该操作以随机顺序应用于标签。因此,独热编码特性被保留。例如,一个标签的膨胀会导致膨胀区域的所有其他标签被移除。
  • 2. 小连接分支移除
    • 小于15% patch大小的连通分支以0.2的概率从独热编码中移除。

E Network Architecture Configuration(网络结构配置)

图E.1 为在线方法中描述的架构配置的迭代过程提供了可视化的帮助。
图E.1


F Summary of nnU-Net Challenge Participations(nnUNet参加过的一些竞赛的摘要)

       这一块的总结我会另开一篇文章来讲,会设计到具体的实践项目分析。


G Using nnU-Net with limited compute resources(在有限的资源下使用nnUNet)

       降低计算复杂度是驱动网络设计的关键动机之一。对于大多数用户和研究人员来说,运行由nnU-Net生成的所有配置应该是可管理的。然而,在计算资源极其稀缺的情况下,也有一些捷径可走。

G.1 减少网络训练的数目

       nnUNet总共有四种网络类型:2D、3D_full_res、3D_lower_res、3D_cascade。每个进行五种交叉验证,就是共有20个模型要训练,每个模型都需要几天时间训练,所以必然会面对计算资源的匮乏。我们提出了两个策略去解决接下来的问题。

  • 1.人工的选择UNet的网络模板配置
           总的来说,3D_full_res表现出最好的效果。因此,这个配置是一个好的起点,并且能很简单的作为一个默认设置。使用者能够决定是否需要进行这么多个训练,只训练一个nnUNet的配置也是可行的,比如只用3D_full_res的五折交叉运算。
           要学会利用一些专业知识来大致估计你最适合的网络模板是什么。比如对于高度各向异性的图片,就很有可能最适合跑2D_Unet,但这也不是绝对的。对于一些非常大的图片,3D级联的方法就很有可能表现最好,因为包含了足够的上下文信息,但是这也只是在目标需要一个大的接受野时才成立。比如你需要检测神经突触这种结构,就只需要关注局部信息,这时候3D_full_res效果最好。
  • 2.不要用五折交叉验证跑所有的模型。
           选一个可能不错的来跑五折交叉验证,但是对于级联的网络一定要用五折交叉验证,因为他会生成一些可靠的mask去输入下一个网络,而这个mask就是依赖五折交叉验证来得到的。

G.2 减少GPU的显存

       【笔者经验】我的nnUNet框架在RTX8000(48g)上运行速度,占用8g左右显存,运行时间450s一轮,时间和2080Ti基本相同。在1070上也可以运行,一轮650s左右(只要小于8g都可以)。
       但是,当我在一个GPU上同时运行nnUNet的两项训练任务,计算时间成线性增加,比如两个nnUNet训练进程在一张卡上,每个的速度都会降低一倍,三个就会降低三倍。
       加入准备用于个人学习,20系列只要显存满足都可以,假如作为研究使用,建议多购置几张2080ti(11g),每张卡上跑一个。

猜你喜欢

转载自blog.csdn.net/weixin_42061636/article/details/107201170