简介这是基于无人机航拍数据的三维场景重建模型训练完整工程面向毕业设计、课程设计及项目开发场景适合有一定Python基础、希望深入三维视觉与NeRF方向的中高级学习者。整体流程覆盖数据建设、位姿估计、深度图生成与模型训练通过采集地面多角度图片借助COLMAP估计相机位姿作为基准再经Behindthesences算法获取航拍图像深度图最终输出三维重建结果。压缩包共55个文件包含41个Python脚本用于数据预处理、日志解析、模型训练与评估2个Jupyter Notebook便于交互式调试YAML文件配置训练参数MP4/GIF直观展示重建效果PNG呈现结果图另有项目说明文档与数据集总大小约20.65MB。源码已经严格测试目录结构清晰可放心参考并在此基础上扩展算法或调整应用场景。目前已有247人学习是课程论文或创新项目的有力参考。1. 从无人机航拍到三维场景为什么这是最容易出成果的毕设方向用 Python 和 Jupyter 把一批无人机航拍影像变成带纹理的三维场景模型是摄影测量和三维视觉方向里最典型的毕业设计、课程设计和项目开发题目。它听起来门槛高实际输入只要一组重叠率合格的航拍图片核心链路是 SfM运动恢复结构加 MVS多视角立体匹配跑完之后得到相机位姿、稀疏点云、稠密点云和三角网格。这个方向适合两类人想接触三维视觉但不想啃 C 的人以及需要快速出可视化成果去答辩的学生。你在 Jupyter 里逐步看到点云从散落到成面的过程本身就是一个很有说服力的展示。2. SfM 到 MVS无人机影像三维重建的技术链路与关键选型2.1 影像到网格的四步链路为什么 SfM 先算位姿、MVS 再算深度无人机拍回来的是一堆带 EXIF 信息的 JPG没有深度信息也没有相机在空中的精确位置。三维重建要解决的核心问题是如何从二维像素反推三维坐标。整个链路可以拆成四步每一步都有明确的输入输出不能跳步。第一步是特征提取与匹配。算法在每张图上找角点和描述子常用 SIFT然后在不同影像里找同名点。这里的核心难点不是“找到特征”而是“匹配正确”——天空、水面、纯色屋顶都会产生大量误匹配。第二步是稀疏重建也就是 SfM 部分根据同名点的像素坐标交替估计每张照片的相机位姿旋转和平移和稀疏三维点坐标再用光束法平差Bundle Adjustment把所有参数联合优化一遍。光束法平差是三维重建里的数学核心它把每个三维点在每张影像上的重投影误差加起来统一最小化这一步直接决定相机位姿的全局一致性。第三步是稠密重建也就是 MVS 部分在已知相机位姿的约束下为每个视角的像素估计深度再融合成稠密点云。稀疏点云只有几千到几万个点而 MVS 能产出百万级点这之间的差距就是“能不能看清屋顶结构”的关键。第四步是把点云计算成三角网格并把原始影像的颜色或纹理贴回到网格表面。网格化常用 Poisson 重建或 Delaunay 三角化纹理映射则是把每个三角面对应到最合适的原始影像上裁剪、融合成纹理图集。这个顺序不能乱。SfM 输出的位姿越准MVS 的深度匹配范围就越小稠密重建就越快越稳。很多新手把 SfM 当黑匣子一上来就想直接生成密集点云结果因为位姿不准深度图全是噪声出来的模型像一团棉絮。我的习惯是把稀疏重建当做一个独立的检查点先确认相机轨迹和稀疏点云是连续的一整块再做后续这能省下大量翻车时间。2.2 COLMAP / OpenMVG / OpenMVS三维重建工具链怎么选开源工具链里围绕 SfM 和 MVS 的组合其实很固定。下表是我常用的选型对照按无人机航拍这个场景排序。工具负责环节优势注意点COLMAP特征提取、匹配、稀疏建模、稠密重建全流程在一个工具里GPU 加速文档和社区最全纹理映射环节较弱对中文路径不友好OpenMVG稀疏重建支持多种相机模型输出到 OpenMVS 方便自身不包含稠密重建与网格化OpenMVS稠密重建、网格化、纹理映射网格质量高纹理融合自然依赖上游位姿质量编译略麻烦MeshLab后处理、简化、测量、Poisson 重建轻量可视化方便不适合做全自动批量重建无人机航拍的特点是影像数量大、重叠率高、相机内参基本固定个别影像可能过曝或移动模糊。常见做法是用 COLMAP 跑完全部稀疏和稠密流程把结果交给 OpenMVS 做网格化和纹理映射最后用 MeshLab 检查质量。如果你只想交出点云结果COLMAP 到稠密点云这一步就够了如果需要带纹理的网格模型才需要走到 OpenMVS。还有一个决策点什么时候只选 COLMAP。COLMAP 自身也提供 delaunay 和 poisson 网格化接口配合顶点色可以直接交付模型适合室外大场景、不需要精细贴图的场景。但如果客户或老师要求模型能转进 Blender、能贴材质、能导出带 UV 的 OBJ那纹理映射这步绕不开 OpenMVS。另外OpenMVS 上一步的数据接口通常是 OpenMVG 或 COLMAP 导出的稀疏模型所以上游选型不会浪费链路是通的。2.3 航拍数据准备重叠率、影像质量和坏帧甄别数据是重建的上限算法只是逼近这个上限。航拍数据准备常见做法是控制三个指标航向重叠率至少 80%旁向重叠率至少 60%地面采样间距GSD不要超过目标分辨率的两倍。GSD 可以用公式粗算像元尺寸 × 飞行高度 ÷ 焦距比如像元 3.9 微米、焦距 24 毫米、飞行高度 120 米GSD 约 1.95 厘米这个精度对多数建筑级场景足够。一个 200 米见方的校园角落飞 80 到 150 张就够了想做一个完整村落或厂区通常要 300 张以上。影像数量不足时重建区域会断裂成几块相机轨迹分成互不相连的片段后面接起来非常痛苦。另外要注意航线的旁向覆盖航线之间至少要留出 60% 的重叠否则相邻航线影像匹配对太少区域会横向分层。拿到影像后先做坏帧甄别。无人机在转弯、变高或快门速度不够时很容易产出模糊帧。模糊帧在特征匹配阶段会引入大量错误同名点直接拖垮后面的光束法平差。我一般会在 Jupyter 里跑一段拉普拉斯方差检测import cv2 import glob image_dir data/UAV/images threshold 80 # 低于该拉普拉斯方差值判为模糊 for img_path in sorted(glob.glob(image_dir /*.JPG)): gray cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if gray is None: print(f[跳过] 无法读取: {img_path}) continue fm cv2.Laplacian(gray, cv2.CV_64F).var() if fm threshold: print(f[疑似模糊] {img_path} 方差{fm:.1f}) else: print(f[正常] {img_path} 方差{fm:.1f})逻辑很简单拉普拉斯算子提取图像二阶梯度梯度方差越大边缘越锐利整幅图平均梯度接近零基本上就是糊的。threshold 需要按场景调高空航拍纹理稀疏80 可能偏低有大量植被和屋顶纹理时可以提高到 120 到 150。关键不是绝对数值而是把明显低于同批影像均值的那几张挑出来删掉。这里有个前提检查之前把影像统一放进纯英文目录比如 data/UAV/images。很多视觉库对中文路径会直接翻车这个坑在第 5 章专门说。另外如果图片里混入了手机拍的近景照片建议先移出去——不同相机内参混在一起会让 SfM 的相机模型估计互相干扰重建结果很难看。3. 用 Python Jupyter 跑通稀疏重建环境搭建与最小命令3.1 环境搭建conda、Jupyter 与 COLMAP 的安装顺序环境搭建的顺序直接影响你后面排错。我常用的链条是装 Miniconda建独立环境装 Jupyter再装 COLMAP。Miniconda 安装后如何使用 Jupyter notebook 这个问题核心是不要在 base 环境里堆包而是为项目单独建环境。conda create -n uav3d python3.10 -y conda activate uav3d pip install jupyter notebook open3d numpy conda install -c conda-forge colmap -ypython3.10 是兼容性比较好的选择Open3D、pycolmap 这些包都有对应的轮子。装完后在终端执行jupyter notebook终端会打印一条带 token 的本地地址那个地址就是 Jupyter 网页版登录入口复制到浏览器打开即可。想修改默认存放地址可以在启动时指定jupyter notebook --notebook-dirD:/workspace/uav3d也可以修改 jupyter_notebook_config.py 里的c.ServerApp.root_dir这样以后“新建”按钮创建的 notebook 会自动落到你指定的文件夹而不是默认的 C 盘用户目录。为什么要先建环境再装包我见过太多人直接在 base 里pip install一堆包然后开 Jupyter 时发现 Python 路径串了启动脚本报错都找不到是哪个环境的锅。用一个干净项目环境所有调试范围都收拢在 uav3d 里排错成本低一个数量级。装完之后先跑一个无参数的命令确认 COLMAP 能输出帮助信息再继续。3.2 COLMAP 稀疏重建最小命令特征提取、特征匹配与增量式 SfM数据准备好后稀疏重建就三句话。每个命令在 Jupyter 里用!前缀也能跑但大量日志会把 cell 刷爆所以我习惯先讲解终端里的最小命令再看如何在 Jupyter 里封装。colmap feature_extractor \ --database_path data/UAV/colmap/database.db \ --image_path data/UAV/images \ --ImageReader.camera_model SIMPLE_RADIAL \ --ImageReader.single_camera 1 \ --SiftExtraction.use_gpu 1 \ --SiftExtraction.max_image_size 4000 colmap exhaustive_matcher \ --database_path data/UAV/colmap/database.db \ --SiftMatching.guided_matching 1 \ --SiftMatching.max_num_matches 32768 mkdir -p data/UAV/colmap/sparse colmap mapper \ --database_path data/UAV/colmap/database.db \ --image_path data/UAV/images \ --output_path data/UAV/colmap/sparse第一条做特征提取。SIMPLE_RADIAL适合大多数消费级无人机相机它只估计一个径向畸变参数参数少反而比复杂的 PINHOLE 更稳single_camera 1告诉 COLMAP 所有影像用同一个内参因为这类多旋翼相机在固定焦距下内参是恒定的max_image_size 4000把超大影像缩放到长边 4000 像素既保留纹理细节又控制内存。如果你的照片是 2000 万像素这条参数建议保留。第二条做特征匹配。exhaustive_matcher会把所有影像两两匹配一次适合 500 张以内的数据集超过 500 张建议换成sequential_matcher按影像顺序只匹配相邻帧速度快很多。guided_matching 1用几何关系引导后续匹配能显著减少误匹配。max_num_matches 32768是单对影像的最大匹配对数设太大会引入噪声设太小会丢掉有效点。匹配完成后第三步mapper就是增量式 SfM。它从一对初始影像开始一边估计相机位姿一边插入新影像每插入一张就跑一次局部光束法平差。跑完后检查 data/UAV/colmap/sparse/0 目录里面出现 images.bin、points3D.bin、cameras.bin 三个文件说明稀疏重建成功。3.3 把 COLMAP 嵌进 Jupytersubprocess 调用与进度日志在 Jupyter 里直接敲!colmap mapper ...虽然能跑但日志会全部堆在 cell 底部而且一旦报错你要拖到最下面找红色信息。另一个更隐蔽的问题是如果在一个循环里连续跑多张影像处理Jupyter 一个 cell 只输出最后一轮的结果中间过程全被吞掉排查问题非常难受。我更建议写一个小函数用 subprocess 调用 COLMAP只保留日志的末尾部分import subprocess COLMAP colmap DB data/UAV/colmap/database.db IMAGES data/UAV/images def run_colmap(args, tail2000): cmd [COLMAP] args proc subprocess.run(cmd, capture_outputTrue, textTrue) if proc.stdout: print(proc.stdout[-tail:]) if proc.returncode ! 0: print(proc.stderr[-tail:]) raise RuntimeError(fCOLMAP 失败: {cmd}) run_colmap([ feature_extractor, --database_path, DB, --image_path, IMAGES, --ImageReader.camera_model, SIMPLE_RADIAL, --ImageReader.single_camera, 1, --SiftExtraction.use_gpu, 1, --SiftExtraction.max_image_size, 4000, ])capture_output 把标准输出和错误输出收进内存proc.stdout[-tail:]只打印最后 2000 个字符在 Jupyter 里不会刷屏。如果 COLMAP 返回非零退出码再把 stderr 末尾抛出来。这个封装的好处是每一个重建步骤变成 notebook 里的一个 cell参数、输出、运行时间都在同一个文档里答辩时老师看到的是一份完整实验记录。跑完 mapper 后可以用 Open3D 快速看一眼稀疏点云确认它不是一团乱线import open3d as o3d sparse o3d.io.read_point_cloud(data/UAV/colmap/sparse/0/points3D.ply) print(稀疏点数:, len(sparse.points)) o3d.visualization.draw_geometries([sparse], window_namesparse)COLMAP 的 sparse/0 里不一定直接有 points3D.ply常见的做法是用colmap model_converter --output_type PLY先生成或者直接用pycolmap读取 bin 文件。如果打印出来的点数只有几十几百说明特征匹配失败回到特征提取环节查参数如果有几千点且能看到建筑物轮廓就可以放心进入稠密重建。4. 稠密点云生成与后处理从 COLMAP 到 Open3D 可视化4.1 稠密重建命令patch_match_stereo 与 stereo_fusion 的串联稀疏重建给出相机位姿接下来要为每张影像计算深度图再融合成稠密点云。COLMAP 里这两步由 patch_match_stereo 和 stereo_fusion 完成但中间必须先用 image_undistorter 按优化后的内参矫正影像。colmap image_undistorter \ --image_path data/UAV/images \ --input_path data/UAV/colmap/sparse/0 \ --output_path data/UAV/colmap/dense \ --output_type COLMAP \ --max_image_size 3000 colmap patch_match_stereo \ --workspace_path data/UAV/colmap/dense \ --PatchMatchStereo.geom_consistency true \ --PatchMatchStereo.max_image_size 3000 colmap stereo_fusion \ --workspace_path data/UAV/colmap/dense \ --output_path data/UAV/colmap/dense/fused.ply \ --StereoFusion.min_num_pixels 5image_undistorter 会把稀疏模型和全部影像整理到 dense 工作目录这一步同时做了畸变矫正输出目录里会出现 images、sparse、stereo 三个子目录。patch_match_stereo 是稠密重建的核心geom_consistency true要求深度图在多视角间一致精度更高但时间会显著变长第一次跑可以先关掉验证流程通了再开。stereo_fusion 把多视角深度图融合成一张点云min_num_pixels 5表示一个点至少被 5 张影像确认才保留值调大到 10 可以有效过滤漂浮噪声代价是边缘的细结构会被削掉。这块是所有流程里最容易跑死的一步。200 张 3000 像素影像开几何一致性在纯 CPU 上可能要跑一个通宵有 NVIDIA GPU 的话时间能缩短到 1 到 2 小时。如果你只用来交课程设计把 max_image_size 降到 2000关掉 geom_consistency半小时能看到结果。fusion 结束后dense 目录里除了 fused.ply还能看到每个视角的深度图与法向图这些中间产物可以用来定位哪些视角贡献了噪声点。4.2 Open3D 点云后处理下采样、去噪与法线估计stereo_fusion 出来的 fused.ply 通常有几百上千万个点直接网格化会让计算量爆炸。我一般先用 Open3D 做三件事体素下采样、统计滤波去噪、估计法线。这也是 Jupyter 里最适合展示的环节每一步都有可视化反馈。import open3d as o3d pcd o3d.io.read_point_cloud(data/UAV/colmap/dense/fused.ply) print(原始点数:, len(pcd.points)) pcd pcd.voxel_down_sample(voxel_size0.05) pcd, ind pcd.remove_statistical_outlier(nb_neighbors20, std_ratio2.0) pcd.estimate_normals( search_paramo3d.geometry.KDTreeSearchParamHybrid(radius0.1, max_nn30) ) o3d.io.write_point_cloud(data/UAV/colmap/dense/fused_clean.ply, pcd) print(清理后点数:, len(pcd.points))voxel_down_sample 的 voxel_size 要跟 GSD 挂钩。如果无人机飞高 120 米、GSD 在 2 到 3 厘米5 厘米的体素已经能保住屋顶轮廓和树冠形态如果飞得更高、GSD 到了 5 厘米以上就把 voxel_size 改成 0.1否则下采样会削掉真实现状。remove_statistical_outlier 统计每个点周围 20 个邻居的距离分布把偏离均值超过 2 个标准差的点判为离群点直接抹掉。法线估计的 radius0.1 保证局部面片内有足够点数参与拟合结果才平滑max_nn30 限制邻居数量防止远处点干扰。输出 fused_clean.ply 后用 draw_geometries 旋转查看把视角切到屋顶和立面交界处看细节。这个可视化对答辩演示来说比任何文字都有说服力。如果看到点云表面有大量空洞先回 4.1 把 min_num_pixels 调小而不是在这里硬补。4.3 网格化与纹理映射把点云变成能交付的模型点云只是带坐标和颜色的点集合要交给老师或客户看得转成三角网格。OpenMVS 是这一步的常见选择它的 ReconstructMesh 和 TextureMesh 分别负责建网和贴图。如果你不想花时间编译 OpenMVS短期方案是直接把 fused_clean.ply 拖进 MeshLab用 Filters 里的 Screen Poisson Surface Reconstruction 生成网格。但要注意MeshLab 的 Poisson 重建出来的网格没有 UV 展开颜色是顶点色放大后容易糊。真正能交付的带纹理模型需要走 OpenMVS。编译 OpenMVS 有点门槛常见做法是用 vcpkg 或预编译二进制编译时依赖 Eigen、Boost、CGAL版本对不上容易报错。跑起来的命令不复杂./ReconstructMesh --input-file data/UAV/colmap/dense/fused_clean.ply \ --output-file data/UAV/colmap/model/scene_mesh.ply ./TextureMesh input_file data/UAV/colmap/dense/fused_clean.ply \ --output-file data/UAV/colmap/model/scene_textured.obj \ --resolution-level 2ReconstructMesh 默认用空间哈希和 Delaunay 方式把点云转成网格平滑参数按默认就能出不错的效果。TextureMesh 是重头戏它把每块三角面片对应的原始影像颜色投影到纹理图集上resolution-level 2表示贴图分辨率等级等级越高纹理越细内存占用也越高。如果你的数据来自多个不同光照时段的飞行架次纹理上会出现明显的接缝这时候优先检查稀疏重建的相机位姿是否够准而不是盲目调贴图参数。5. 三维重建的五个典型坑现象、原因与解决方案5.1 Jupyter 启动时报“找不到指定的程序”网页版登录入口打不开现象在终端里执行jupyter notebook提示“找不到指定的程序”或“Error loading Python DLL”浏览器打开 token 链接后一直转圈。原因conda 的 base 环境和项目环境混装导致 Python 路径错乱或者是 tornado、zmq、jupyter 三者的版本对不上。常见于先 pip 了一堆包、然后又用 conda 装了别的 Python 依赖启动脚本抓到的是旧 DLL。解决重新激活干净项目环境执行pip install --upgrade jupyter notebook把依赖拉到同一版本段。不要直接用jupyter命令改用python -m jupyter notebook确保走的是当前环境的 Python。如果还不行删除环境重建比逐个修包要快得多。我现在的习惯是每次启动前先看一眼 env 列表确认终端已经激活了正确环境这种问题基本不会再犯。5.2 相机位姿发散稀疏点云全是飞点现象mapper 跑完点云像是一团乱麻相机轨迹在空间里乱跳和真实航线完全对不上log 里大量出现 “Reconstruction failed” 或 “bad image” 提示。原因最常见是影像重叠率不够特征匹配对数量太少增量式 SfM 在添加新影像时找不到足够约束位姿估计直接漂移。另一个隐蔽原因是影像里混入了大量天空、水面反光或纯色墙体这些区域特征匹配天然弱哪怕重叠率够也匹配不到有效点。解决先看匹配质量。exhaustive_matcher 跑完后用 COLMAP GUI 打开 database.db检查每张影像的匹配对数量如果平均匹配对数不到邻接影像数的 20%就要重新飞或者补拍。对水面和天空这类低纹理区域可以裁剪掉画面边缘再重建。还有一个土办法先跑通一半影像确定位姿稳定后再把另一半加入而不是一次性灌 300 张。5.3 稠密重建在 CPU 上跑到天荒地老现象patch_match_stereo 跑了两小时进度条还在 3%风扇已经起飞Jupyter cell 一直显示运行中。原因patch match 迭代本身计算量极大每张影像要沿光线搜索深度并做多次传播CPU 版本在 3000 像素和几何一致性全开的情况下基本是灾难。解决先开--PatchMatchStereo.max_image_size 2000压掉一半像素量点数少 30% 但时间能缩到三分之一。再把geom_consistency true关掉这能再快一倍。有 GPU 就优先开 use_gpu大部分 NVIDIA 显卡都能跑。我的节奏是先用小尺寸跑通全流程确认输出正常后再放大尺寸跑最终版避免在参数没调好时浪费一整晚。5.4 纹理映射后模型发虚或者贴图拉伸现象网格几何看着正常贴上纹理后局部模糊远处的墙面出现一条条拉长的色块屋顶纹理像被风刮过一样。原因纹理投影依赖相机位姿和畸变模型。位姿有微小漂移时同一个三角面片在不同照片里的投影位置对不齐TextureMesh 只能硬选一张选错就糊。畸变没完全估计出来时建筑物边缘的纹理会被明显拉伸。解决回头检查稀疏重建把--SiftExtraction.estimate_affine_shape 1打开这会让 SIFT 对视角变化更鲁棒提升位姿质量。再确认 image_undistorter 用的内参是 mapper 优化后的模型而不是初始猜测。最后才考虑调整 TextureMesh 的贴图分辨率因为低分辨率不会导致拉伸只会变糊。每次重建完先盯着纹理接缝看 30 秒比跑一百次参数都管用。5.5 数据集路径带中文和空格COLMAP 直接读不出影像现象在终端手动敲命令能跑但换到 Jupyter 的 subprocess 封装里COLMAP 报找不到图像目录或者建成空的 database.db还没有任何红色报错。原因COLMAP 这类 C 视觉库对非 ASCII 路径支持很差Windows 下中文路径尤其致命空格在部分命令行解析里也会出问题。解决项目根目录只用英文字母、数字和下划线例如D:/workspace/uav3d。拿到含中文名的数据集后写一个脚本统一重命名为纯数字编号。把数据本身和 notebook 放在同一级目录路径统一用相对路径Jupyter 工作目录不同导致找不到文件的问题会少一大半。这个坑我每次带新人必提因为它几乎不需要技术成本却能让流程最顺畅。6. 数据集组织与精度验证答辩前必做的三件事6.1 数据集目录结构images、sparse、dense、model 怎么放项目做到后期最怕的是自己都找不到上次跑出来的模型。我一般会把一个航拍重建项目按下面的结构组织data/UAV/ ├── images/ # 原始航拍影像命名 DJI_0001.JPG 或 00001.jpg ├── colmap/ │ ├── database.db │ ├── sparse/0/ # 稀疏重建结果 │ ├── dense/ # 稠密重建工作目录 │ └── model/ # 最终导出的网格和纹理 └── ground_truth/ # 控制点坐标、卷尺测量记录images 只放真正参与重建的帧ground_truth 里是精度验证用的已知距离和控制点答辩时这是证明结论可靠的关键材料很多人忽略。6.2 重建质量验证重投影误差与已知距离测量精度验证不是可选项。我会先看 COLMAP 自己算的重投影误差colmap model_analyzer --path data/UAV/colmap/sparse/0平均重投影误差在 0.5 到 1.0 像素之间属于健康范围超过 2 像素说明匹配阶段有大量误点需要回到特征提取阶段重跑。然后再做真实尺度校验在重建场景里选两个明显目标点比如两栋房角的距离用 RTK 或卷尺量出真值在 MeshLab 里量模型上同一对点的距离。这两个距离的偏差就是你的尺度误差一般控制在 3% 以内可以在答辩里用作定量结论。6.3 成果交付导出 OBJ/GLTF 与 Notebook 演示交付时优先导出 OBJ 加 MTL 加纹理图这是最通用的三维模型格式需要网页展示就另存一份 glTF。另一份要交付的成果是 notebook 本身把数据预览、命令封装、点云可视化、精度分析放在同一个文档里导出成 HTML 或 PDF 就是完整的实验说明书。答辩时现场重跑一遍 80 张图的流程比任何 PPT 都有说服力这也是用 Jupyter 做毕设最划算的地方。我最早做这类项目时直接跳过了精度验证拿着一个看起来很漂亮的模型去答辩结果被问“模型绝对尺度是多少”时答不上来。从那以后我养成了一个习惯每次重建完先在场景里找一个已知距离量一遍再把误差记进 notebook 的第一个 cell这个习惯救了我很多次希望帮到你。本文还有配套的精品资源点击获取