简介:基于Python的肝脏CT图像分割与三维重建项目,面向医学影像处理、计算机视觉方向的在校生与开发者,适用于毕业设计、课程设计及期末大作业。压缩包共124个文件,约105.2MB,以42个Python脚本为核心,覆盖图像预处理、模型训练与三维重建全流程;另含42张结果示意图、15个编译后的pyc文件、5个模型数据tar包、VTK三维模型文件及说明文档,便于对照阅读与复现。资源已通过本地上线运行、功能测试,答辩平均分达97.5分;当前已有180人学习下载。从入门调参到替换模型、扩展解剖结构可视化均可基于源码二次开发,是兼具学习与实战价值的医学图像入门项目。
1. 基于Python的肝脏CT分割与三维重建:跑通一个源码包之前,先看清整条链路
基于Python的肝脏CT图像分割及三维重建,听起来是“源码+模型”两个词就说完的事,真跑起来是一条完整的医学影像流水线:DICOM读入、肝脏分割、三维网格生成。我见过不少人拿到压缩包,第一反应是找main.py直接跑,结果不是缺依赖,就是权重路径不对,或者分割出的“肝脏”把脾脏也包了进去——坑不在算法,在工程链路。
它解决的问题很具体:把CT序列里肝脏的每一层切出来,再把掩膜堆成可旋转、可测体积、可导出OBJ/STL的三维模型。适合刚入门Python的医学影像从业者、影像组学研究生,以及想验证分割效果的技术人员。
我的建议是别把它当黑匣子,跑通就算完。把DICOM读取、预处理、分割、重建、评估拆开,每段单独验证,参数才不会变成玄学。下文按这条链路讲。
2. 把链路拆开选型:HU值窗口、U-Net与marching cubes的取舍
动手写代码之前,先回答三个问题:肝脏在CT里靠什么和周围组织区分?分割用什么网络?掩膜用什么算法变成网格?这三个答案决定了后面的脚本结构和绝大多数参数。我见过太多人跳过这一步,最后在“为什么别人能跑通我跑不通”里反复折腾。
2.1 肝脏在CT里长什么样:HU值、窗宽窗位和边界模糊的根源
CT图像里每个像素存的不是灰度,而是亨氏单位HU,它以水的吸收系数为0、空气为-1000。肝脏平扫大约在40到60HU,增强门脉期因为造影剂充盈,肝实质普遍升到100到130HU,而脾脏、胰腺、右肾皮质和它的范围高度重叠——这是肝脏分割难的根本原因:不是对比度不够,是重叠太多,边界区域的灰度差异很小。
放射科医生阅片时靠“窗宽窗位”来压制不关心的组织。腹部平扫常用窗宽400、窗位40,显示范围从-160到240;专门看肝脏时会收窄到窗宽150、窗位30,突出肝实质和病灶的差别。深度学习模型不需要人眼那样的窗宽窗位,但输入仍然要做裁剪和归一化,常见做法是把HU裁剪到[-100, 200]再做线性归一化。裁剪范围不是随便给的:下限要去掉空气和大部分脂肪,上限要保留增强后的肝实质和骨骼对比。
这个边界模糊还有解剖层面的原因:肝左叶贴近心脏,膈肌运动带来呼吸伪影;肝与脾、胃壁之间的薄膜在CT上经常显示不出来,分割网络很容易把边界“涨”出去。门脉期(注射造影剂后约60到70秒扫描)是肝分割的首选相位,这个时期肝实质强化均匀,与脾脏的差异相对稳定。源码包里如果注明“需要门脉期序列”,你要先确认自己的数据是增强还是平扫,拿平扫喂给门脉期训出来的权重,分割质量会明显下降。
| 组织 | 典型HU范围(门脉期) | 对分割的影响 |
|---|---|---|
| 空气/肺 | -1000 ~ -500 | 预处理中应被clip掉 |
| 脂肪 | -100 ~ -50 | 与肝脏边界清晰,常用作负样本 |
| 脾脏 | 90 ~ 130 | 与肝灰度重叠,是假阳性主要来源 |
| 肝脏 | 100 ~ 130 | 目标区域 |
| 骨骼 | 300+ | 裁剪后成为最高值,不属于分割目标 |
提示:预处理里对HU的裁剪范围,必须和训练脚本保持一致。这是模型输出全零之外最常见的一个坑。
2.2 分割模型选型:为什么U-Net是默认答案,nnU-Net什么时候更值得
医学影像分割的默认答案就是U-Net,几乎不需要争论。它由编码器和解码器组成:编码器不断下采样,把全局上下文压缩成特征;解码器逐步上采样恢复分辨率;中间的skip connection把浅层的边缘纹理直接拼到深层特征上,让弱边界的小目标恢复得更好。肝脏在CT里边界弱、形状随呼吸变化,U-Net的这三点正好对症。
选2D还是3D,看体数据的各向异性。如果原始CT是层厚5mm、层内分辨率0.7mm,z轴的间距远大于x/y,直接用3D卷积会浪费大量显存去学没什么信息的片间关系,这种数据用2D U-Net逐层分割,再把2D结果堆成3D掩膜是常见做法。如果层厚已经是1mm的各向同性数据,3D U-Net的片间连贯性优势才值得用显存去换。
损失函数上,肝脏分割常用Dice loss加BCE的组合:Dice loss直接优化预测与金标准的重叠率,对前景占比小的切片更公平;BCE保证预测极差时梯度不会消失。拿到别人训好的权重时,先看两处配置:输入尺寸和归一化方式。输入是全图resize还是patch裁剪,归一化是min-max还是mean/std,这两处和预处理脚本对不上,后面全是黑匣子一样的玄学问题。
nnU-Net是当你有多余算力时的升级项,它会自动配置预处理、网络拓扑、patch大小和训练策略,在公开的LiTS肝脏数据集上经常刷到接近人类标注的水平。但对于一个“源码+模型”压缩包,U-Net已经足以跑通并交付初版结果。原则是先跑通基准,再讨论换网络,而不是第一天就在两个框架之间摇摆。
2.3 三维重建选型:marching cubes与VTK管线,为什么3D高斯重建不适合CT
分割输出是0/1的体素掩膜,把它变成三角网格,经典算法是marching cubes。思路很朴素:把每个体素当作一个小立方体,8个顶点各自判断在等值面内侧还是外侧,256种顶点状态归纳成15种基本拓扑,再用插值求出三角形的顶点位置。对二值掩膜,等值面level取0.5,就是恰好穿过体素中心的那个表面。
最近热门的3D高斯三维重建和NeRF,本质是从多视角照片恢复连续表面,需要RGB图像和相机位姿,适合拍物体、拍场景。CT本身就是三维体数据,不存在“多视角拍摄”问题,直接对体素跑marching cubes又快又准。流行词要放在合适的场景里,医学体数据重建别去赶这个时髦。
落地工具通常两条路。轻量路线是scikit-image的marching_cubes加trimesh,适合在notebook里验证和快速出图;交付级路线是VTK的完整管线:vtkMarchingCubes提取表面,vtkWindowedSincPolyDataFilter平滑,vtkDecimatePro减面,最后重算法线导出OBJ或STL。平滑和减面的顺序不能反,先平滑后减面能保住大体形态,反过来会让边缘锯齿被保留下来。选型定完,下一章就落到第一个分割掩膜上。
3. 从Python安装到第一个分割掩膜:最小可复现流程
3.1 Python安装与环境依赖:先锁版本,再谈跑通
Python安装这一步本身不难,难的是版本组合。这类源码包依赖SimpleITK、VTK、PyTorch和scikit-image,它们对Python版本各有要求。我的建议是用Python 3.8到3.10之间,太新的版本如3.12、3.13,SimpleITK和旧版VTK的二进制wheel不一定跟得上,装不上就卡在最前面。先建一个干净的虚拟环境,别把依赖装进系统Python,这是所有后悔药里的第一颗。
# 创建并激活虚拟环境 python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate # 按顺序安装:先医学影像与数值计算,再深度学习,最后几何处理 pip install SimpleITK==2.2.1 numpy==1.24.3 scipy==1.10.1 # torch 的安装命令按你的 CUDA 版本在 pytorch.org 上选对应行 pip install torch==2.0.1 pip install scikit-image==0.21.0 vtk==9.2.6 trimesh==4.0.9逻辑说明:SimpleITK负责DICOM序列读取和重采样,numpy和scipy做数组运算与连通域处理,torch加载模型权重,scikit-image提供marching_cubes接口,vtk做交付级网格处理,trimesh负责OBJ导出和体积计算。用虚拟环境隔离后,这台机器上的依赖问题不会污染其他项目,换机器时也能用requirements.txt快速复现。
参数说明:torch装CPU版还是GPU版,取决于你有没有英伟达显卡,没有就装CPU版,否则会报找不到CUDA runtime。版本号不要随手升级:scikit-image 0.21的marching_cubes接口稳定,新版对返回值做过调整;trimesh升到4.x后部分平滑接口参数变了。锁版本能省下大量翻车时间,尤其当你看的是别人半年前写的代码。
3.2 预处理:重采样、窗宽窗位与归一化的参数设置
预处理是整个流程里最值得花时间的地方。一个5mm层厚的序列直接喂给模型,即使网络再好,重建出来也是千层饼;clip范围不对,模型看见的数值分布和训练时完全两样。下面这段脚本是这类源码包里最常见的预处理逻辑,也建议你作为自检基准:
import SimpleITK as sitk import numpy as np def load_and_preprocess(series_path, target_spacing=(1.0, 1.0, 1.0), clip_range=(-100, 200)): # 1) 读取整个DICOM目录为一个3D体数据 reader = sitk.ImageSeriesReader() reader.SetFileNames(reader.GetGDCMSeriesFileNames(series_path)) img = reader.Execute() # 2) 重采样到各向同性体素,避免z方向拉伸成千层饼 resampler = sitk.ResampleImageFilter() resampler.SetReferenceImage(img) resampler.SetOutputSpacing(target_spacing) # (dz, dy, dx) orig_size = np.array(img.GetSize()) orig_spacing = np.array(img.GetSpacing()) new_size = (orig_size * (orig_spacing / np.array(target_spacing))).astype(int) resampler.SetSize(new_size.tolist()) resampler.SetInterpolator(sitk.sitkLinear) img = resampler.Execute(img) # 3) 转numpy,裁剪HU范围并归一化到[0,1] arr = sitk.GetArrayFromImage(img) # 形状 (Z, Y, X) arr = np.clip(arr, clip_range[0], clip_range[1]) arr = (arr - clip_range[0]) / (clip_range[1] - clip_range[0]) return arr.astype(np.float32), img逻辑说明:ImageSeriesReader会自动按切片位置排序,把几百张二维CT拼成三维体数据,这一步是很多新手卡住的地方——直接读单张PNG是拼不出正确的三维结构的。ResampleImageFilter把层厚5mm的序列重采样成1mm各向同性,让后续marching cubes拿到的体素近似立方体。numpy数组形状是(Z,Y,X),z在最外层,这个顺序后面和spacing参数严格对应。
参数说明:target_spacing=(1,1,1)是内存和质量的折中,0.5mm各向同性会让体素数量变成8倍,内存和显存可能吃不住。clip_range=(-100,200)覆盖肝实质在门脉期的典型范围,同时清掉了空气、脂肪和高密度的骨。如果你的模型权重用的是别的范围,比如(-200,300)或mean/std归一化,这里必须改——这是分割结果好坏的第一决定因素,也是源码包内置脚本里最容易被忽略的行。
3.3 模型推理与后处理:把网络输出变成干净的肝脏掩膜
预处理完成后就是推理。模型输入通常是(B,1,H,W)或(B,1,Z,H,W),输出是一个概率图,需要经过sigmoid和阈值才能变成掩膜。大部分源码包里的模型权重是2D U-Net,下面这段按逐层推理加三维连通域后处理来写:
import torch import numpy as np import SimpleITK as sitk def predict_clean_mask(arr, model, device='cuda', threshold=0.5): # arr 来自预处理,形状 (Z, H, W),值域 [0,1] model.eval().to(device) slices = torch.from_numpy(arr[:, None, :, :]).to(device) # (Z,1,H,W) with torch.no_grad(): probs = torch.sigmoid(model(slices)).cpu().numpy() # 注意:切层很多时建议改成for循环逐层推理,避免一次batch太大爆显存 mask = (probs[:, 0] > threshold).astype(np.uint8) # 三维连通域分析,只保留最大目标,去掉脾脏、胃壁等假阳性 itk_mask = sitk.GetImageFromArray(mask) cc = sitk.ConnectedComponent(itk_mask) # 26邻域连通 relabel = sitk.RelabelComponent(cc, sortByObjectSize=True) largest = (relabel == 1).astype(np.uint8) # 1号是最大连通域 return largest逻辑说明:用sigmoid把网络原始logits变成0到1的概率,threshold取0.5是把概率图二值化的默认方式。sitk.ConnectedComponent在三维空间里做26邻域连通分析,把每一层单独看时连不上、堆起来却是一个整体的区域合并成同一个目标;RelabelComponent按体积排序后,最大连通域编号是1,那一大片把脾脏包进来的假阳性直接被去掉,这是后处理里最有效的一刀。
参数说明:threshold默认0.5,想减少假阳性可以提到0.6到0.7,代价是肝边缘的薄层会被切掉一点;想保住边缘可以把阈值降到0.4以下,但碎片会变多。调阈值的最快方法是用matplotlib把概率图叠在原始CT切面上目测,挑中间层和肝顶、肝底各看几层,比看统计数字直观得多。如果模型是3D的,输入要改成(B,1,Z,H,W)的patch,用滑窗推理并做overlap融合,显存不够时优先把patch从96降到64。
4. 三维重建落地:marching cubes参数、VTK管线与OBJ导出
4.1 marching cubes的三个关键参数:level、平滑迭代与减面比例
掩膜跑marching cubes是最直接的一步,但skimage和VTK两套路线在参数上的取舍不一样。先用skimage加trimesh讲轻量路线,你在验证阶段会反复用到它:
import numpy as np from skimage import measure import trimesh def mask_to_mesh(mask, spacing=(1.0, 1.0, 1.0), level=0.5, smooth_iter=50, decimate_ratio=0.7): # mask 形状 (Z, Y, X),spacing 是 (dz, dy, dx),单位 mm verts, faces, _, _ = measure.marching_cubes( mask.astype(np.float32), level=level, spacing=spacing) mesh = trimesh.Trimesh(vertices=verts, faces=faces) mesh = mesh.smoothed(iterations=smooth_iter) # 去体素阶梯 if decimate_ratio > 0: target = int(len(mesh.faces) * decimate_ratio) # 保留比例 mesh = mesh.simplify_quadric_decimation(target) return mesh逻辑说明:marching_cubes的spacing必须和体数据的体素尺寸一致,z轴用层距,y和x用DICOM里的PixelSpacing。如果不传spacing,默认按1:1:1处理,5mm层厚的数据重建出来就是被压扁的肝脏模型。trimesh的smoothed做的是保形平滑,能在降低阶梯感的同时尽量保留肝裂和静脉切面的凹陷。
参数说明:level对二值掩膜固定取0.5,不需要调;smooth_iter在30到80之间够用,太大会把这些凹陷磨平;decimate_ratio是保留比例,0.7表示删掉30%的面。很多新手以为面越多越好,实际一个肝脏掩膜直接跑marching cubes可能有几百万个三角面,预览和配准都卡,减到十几万面在视觉上几乎没有损失。要提醒的是,不同trimesh版本对smoothed的参数名有细微差异,你用的版本如果报TypeError,去查一下当前版本的签名,别在这上面死磕。
注意:要算体积必须在减面之前的掩膜上算,不要拿减面后的三角网格积分,误差会到5%以上。
4.2 用VTK渲染并导出OBJ/STL:坐标系、spacing和单位别搞错
skimage路线适合验证,交付给临床或3D打印我一般用VTK,因为它有更成熟的平滑和减面算法,结果更接近工业级。难点在数据格式转换:numpy的轴顺序和VTK不一样,先转哪一个、再翻哪一个,错了模型就翻转。
import vtk import numpy as np def mask_to_vtk(mask_np, spacing_mm, out_file="liver.obj"): # mask_np 形状 (Z, Y, X);VTK 的 ImageData 顺序是 (X, Y, Z) data = np.transpose(mask_np, (2, 1, 0)).astype(np.uint8) vtk_data = vtk.vtkImageData() vtk_data.SetDimensions(*data.shape) # (X, Y, Z) vtk_data.SetSpacing(*spacing_mm) # (dx, dy, dz) importer = vtk.vtkImageImport() importer.SetImportVoidPointer(data.tobytes()) importer.SetDataScalarTypeToUnsignedChar() importer.SetNumberOfScalarComponents(1) importer.SetDataExtent(0, data.shape[0]-1, 0, data.shape[1]-1, 0, data.shape[2]-1) importer.Update() mc = vtk.vtkMarchingCubes() mc.SetInputData(importer.GetOutput()) mc.SetValue(0, 0.5) # 等值面取0.5 smoother = vtk.vtkWindowedSincPolyDataFilter() smoother.SetInputConnection(mc.GetOutputPort()) smoother.SetNumberOfIterations(15) smoother.SetPassBand(0.001) smoother.NormalizeCoordinatesOn() decimate = vtk.vtkDecimatePro() decimate.SetInputConnection(smoother.GetOutputPort()) decimate.SetTargetReduction(0.7) # 删掉70%的面 decimate.PreserveTopologyOn() writer = vtk.vtkOBJWriter() # 需要STL就换vtkSTLWriter writer.SetFileName(out_file) writer.SetInputConnection(decimate.GetOutputPort()) writer.Write()逻辑说明:np.transpose把(Z,Y,X)变成(X,Y,Z),这是numpy和VTK之间最容易出错的一步。vtkImageImport把裸字节交给VTK,必须同时声明数据类型、通道数和数据范围,否则内存视图错乱,出来的模型是噪声。vtkWindowedSincPolyDataFilter是窗口正弦平滑,相比普通拉普拉斯平滑不容易让模型越磨越小,适合医学表面。vtkDecimatePro在保持拓扑的前提下减面,PreserveTopologyOn防止表面出现洞。
参数说明:SetSpacing的单位是毫米,这是后面3D打印和测体积的地基;如果把spacing设成1而真实层厚是5mm,导出的模型就是扁的。平滑迭代15次和PassBand 0.001是VTK社区里常见的保守组合,想更光滑可以把迭代加到30,但肝裂等凹沟会变钝。目标减面率0.7对三百万面的模型能压到九十多万面,预览足够;如果只做网页展示,0.85也行,但细节最丰富的血管切面会被磨掉。
5. 避坑与排查:分割翻车与重建变形的5个常见原因
这一章写的是我反复筛过的经验。每个问题按“现象→原因→解决”来写,你遇到时可以按图索骥。
5.1 分割结果把脾脏或胃壁一起包了进来
现象:模型输出的肝脏掩膜明显偏大,左叶方向拖着一大块灰度接近的组织,看起来像“肝和脾脏连体”。
原因:门脉期肝脏和脾脏的HU范围重叠严重;预处理没做窗宽窗位裁剪,或者裁剪范围跟训练脚本不一致,网络就会把脾脏也判为肝脏。后处理如果漏了最大连通域这一步,散落的假阳性还会连成片,问题更难定位。
解决:先确认预处理和训练脚本一致,再把阈值从0.5提到0.6走一遍连通域,最后用形态学开运算(先腐蚀后膨胀)断开肝脏和脾脏之间的细连接。开运算的kernel用半径2到3个voxel的球,太大会把肝边缘也磨掉。
5.2 模型输出全零或全一,跟输入毫无关系
现象:无论喂什么CT序列,输出的概率图都是一整块黑或者一整块白,没有任何解剖结构。
原因:八成是归一化不匹配。训练时用mean/std归一化,你用的是min-max,数值域完全不同;其次是权重加载失败,load_state_dict时某些键名对不上,等于权重根本没进来;还有一种是模型在train模式下推理,dropout还在工作。
解决:加载权重时打印missing_keys和unexpected_keys,确保模型真正吃进了权重。推理前固定三件事:model.eval()、torch.no_grad()、输入数组的值域和训练数据一致。建议写一个自检:把一张输入图可视化,再把概率图叠加到原图上,一眼就能看出网络在看什么。
5.3 CUDA out of memory,12G显存也不够
现象:3D模型一跑就爆显存,batch设成1也爆。
原因:3D卷积的中间激活量是逐层累积的,patch太小没有临床意义,patch太大显存顶不住;很多人忽略了3D输入的Batch维度同样占显存,batch为1时中间特征图照样吃掉几GB。
解决:patch从96³降到64³或48³,滑窗推理加25% overlap,overlap部分取平均而不是直接覆盖;还不行就换成2D U-Net逐层推理,这是吃显存最少、最稳的退路。不要在每个batch后调torch.cuda.empty_cache(),频繁清理会严重拖慢推理速度。
5.4 重建出来的模型像千层饼,一层一层台阶
现象:OBJ在Meshlab里打开,表面横向沟槽清晰可见,像是把切片一张张叠出来的。
原因:原始数据层厚5mm且没重采样,或者marching_cubes没传spacing,默认1:1:1抹掉了真实的层距信息。
解决:回到预处理,重采样到1mm各向同性;marching_cubes的spacing参数必须与实际体素一致。如果数据已经是1mm各向同性还出台阶,把平滑迭代加上去,VTK路线用WindowedSinc迭代30次。做完之后在软件里转一圈,确认台阶真的消失再进入下一步。
5.5 导出的模型被压扁、翻转或上下颠倒
现象:肝脏模型左右反了,或者高宽比例明显不对,丢进3D打印切片软件里没法用。
原因:numpy的(Z,Y,X)和VTK的(X,Y,Z)之间少了一次transpose;spacing的单位和轴序没对上;有的扫描重建时z轴是反向的,方向余弦信息没有被沿用。
解决:导出前把transpose和spacing写清楚,spacing元组顺序固定为(dx,dy,dz),并打印一个真实病例的掩膜做方向自检。如果模型左右反了,在VTK模型上做一次关于Y轴的镜像变换;上下颠倒则翻转Z轴。改完把模型和一张轴位CT切面叠加看一次,别只凭数字判断。
6. 验证与交付:用Dice和体积量化结果,交付前检查三件事
6.1 Dice系数与体积计算:给分割结果一个量化指标
分割模型做得对不对,不能靠“看起来像不像”,要有量化指标。Dice系数是医学影像分割里最常用的重叠指标,肝脏分割的常见水平在0.90到0.95之间,低于0.85基本可以判定预处理或权重有问题。
import numpy as np def dice(pred_mask, gt_mask): inter = np.logical_and(pred_mask > 0, gt_mask > 0).sum() denom = (pred_mask > 0).sum() + (gt_mask > 0).sum() return 2.0 * inter / (denom + 1e-6) def liver_volume(mask, spacing=(1.0, 1.0, 1.0)): voxel_mm3 = spacing[0] * spacing[1] * spacing[2] return (mask > 0).sum() * voxel_mm3 / 1000.0 # cm³,即 ml逻辑说明:Dice把预测和标签的交集乘2,除以两者面积之和,完全重叠是1,完全不重叠是0。体积计算直接在掩膜上做,用spacing算出单个体素体积再乘体素数,单位从mm³换成cm³后就是临床常说的毫升。成人肝脏体积通常在1200到1500毫升,超出这个范围太多,先怀疑预处理或阈值。
参数说明:只靠Dice不够,它会被大面积重叠掩盖局部漏分割。建议加一个HD95即Hausdorff距离的95分位,它专门抓“边界上漏了一坨”这种稀疏大误差。验证集至少要10个病例,有金标准标注的优先对比公开的LiTS数据集,没有金标准就拿人工修正过的病例当参考。
6.2 交付给临床之前的三件检查
第一件,确认坐标系和单位。OBJ和STL文件的顶点坐标单位是毫米,导出时spacing必须写对;给3D打印或手术规划软件之前,用一个边长已知的体模验证尺度,别让医生拿着放大或缩小的模型做规划。第二件,切面复核。用matplotlib把分割掩膜叠加到原始CT的轴向、冠状和矢状三个切面上,肝静脉汇合处和肝顶是漏分割的高发区,重点看这几层。第三件,可复现记录。把输入序列ID、预处理参数、模型版本、阈值、平滑次数和减面率写成一个txt,和输出文件放在同一个目录。
我的习惯是每个病例跑完都写一条这样的记录,看起来简陋,但临床追问时只有它能救你。曾经有个病例三个月后被要求复查结果,我靠当时记下的阈值和spacing一秒钟复现了当时的模型,而没有记录的那一版数据最后只能重新跑一遍,这种后悔药一个txt加几个数字就够了。如果你正要拿这个源码包做自己的肝分割验证,把DICOM读取、预处理、推理、重建、评估这一段链路完整走一遍,参数逐个试、结果逐个看,比收集多少技巧都管用。希望帮到你。
本文还有配套的精品资源,点击获取