C#结合ONNX Runtime部署DDColor实现老照片上色完整指南

📅 发布时间:2026/8/28 1:30:42
C#结合ONNX Runtime部署DDColor实现老照片上色完整指南 简介深度学习模型落地到桌面端应用时推理引擎的选择直接影响部署效率和运行体验。ONNX Runtime作为跨平台高性能推理引擎支持多种硬件加速为图像处理类任务提供了轻量级解决方案。DDColor凭借双解码器结构在图像上色领域实现更真实自然的色彩还原但其C#部署资料较少。本文从PyTorch权重转换出发详细介绍将DDColor模型集成到C#桌面应用的全流程涵盖ONNX导出、输入输出对齐、预处理后处理实现及性能优化策略并给出可直接复用的代码骨架。适用于老照片修复、老电影画质增强等Windows客户端场景帮助开发者快速实现高性能本地推理。 最近在做一个老照片上色的Windows桌面工具模型选来选去最终定了DDColor。原因很简单效果真实、细节保留好而且在ONNX Runtime上跑起来非常顺。说到C#部署DDColor网上资料大多停留在Python调用或者命令行demo真正把完整流程串起来、能直接塞进WinForms/WPF工程里的参考很少。这篇博客就把我自己从PyTorch权重转ONNX、到C#端完整推理上色的全过程讲清楚包括模型怎么转、输入输出怎么对齐、预处理后处理怎么写、有哪些坑必须避开以及一套可以直接拿来改的C#代码骨架。如果你正在做图像上色、老照片修复、老电影画质增强之类的应用并且想把模型部署到Windows客户端而不是服务器上那这篇内容应该能帮你少踩不少坑。1. 项目概述与DDColor原理简析1.1 DDColor是什么能做什么DDColor是阿里云、港中文等团队在2023年提出的图像上色模型全称是DDColor: Towards Photo-Realistic Image Colorization via Dual Decoders。跟传统上色模型最大的区别是它不只在灰度图上生成一个比较自然的大色块而是通过双解码器结构同时保留图像纹理细节和颜色语义信息最终输出的上色结果更接近真实照片颜色也更饱满、更有层次感。我拿一堆黑白老照片实测过DDColor对肤色、天空、植被、建筑这些常见场景的处理效果相当稳尤其是边缘处不会出现大范围溢色。相比早期基于CNN的模型DDColor在色彩丰富度和真实感上确实有明显提升。如果你做的是面向C端的照片处理工具这个效果完全够用。从技术角度快速过一下DDColor的核心就是双解码器设计一个图像解码器负责保留空间结构另一个颜色解码器内部带Transformer结构用来建模颜色之间的语义依赖关系。两个解码器的特征融合后再通过上采样恢复到输入分辨率。这个结构的好处是颜色预测不再只看局部像素而是能参考整个图像的语义信息所以上色结果会更全局、更协调。1.2 为什么选ONNX Runtime而不是PyTorch直接部署很多做算法的人首选是PyTorch直接推理但如果目标是交付一个Windows桌面程序PyTorch的部署方式很痛苦需要安装Python环境、装PyTorch相关依赖、打包体积大、启动还慢。客户端机器不可能要求用户先装好Python再跑你的程序。ONNX Runtime直接解决了这个问题它是微软开源的跨平台推理引擎提供C#官方NuGet包集成进Visual Studio工程非常简单不需要额外安装运行时环境。除了部署方便ONNX Runtime的推理性能也很能打。同一份DDColor权重导出成ONNX后在相同硬件上用CPU推理速度跟PyTorch相差不大而GPU上如果用CUDA或者DirectML执行提供程序还能再拉一截速度。对于图像上色这种计算密集型任务CPU处理一张512x512的图大概需要1到3秒取决于具体机器这个体验在桌面工具里是可接受的。所以我最终的技术路线是Python端负责模型转换和权重导出C#端用ONNX Runtime加载模型做推理。这样既拿到了深度学习模型的效果又保持了Windows桌面应用的原生体验和分发便利性。2. 环境准备与模型获取2.1 开发环境与NuGet依赖先说整个开发链路需要的环境节省大家摸索时间Visual Studio 2022安装时勾选“.NET 桌面开发”工作负载Windows 10/11 64位系统Python 3.8以上环境用于导出ONNX模型NuGet包Microsoft.ML.OnnxRuntime最新稳定版即可有一个细节需要特别注意如果你要用GPU推理得安装Microsoft.ML.OnnxRuntime.Gpu这个包而且它的版本要跟CUDA版本匹配。比如Gpu包的1.16.x版本对应CUDA 11.8新版可能要求CUDA 12.x。建议先确认目标用户机器上的显卡驱动能支持哪个CUDA版本再决定用哪个NuGet版本。如果不想折腾CUDA也可以用DirectML执行提供程序Windows 10以上系统基本都自带DirectX 12驱动兼容性好很多。在NuGet包管理器里搜索OnnxRuntime能看到一堆相关包记得选带Microsoft.ML前缀的那个。安装完成后项目bin目录下会自动带上onnxruntime.dll和原生依赖不需要手动拷贝。这一点实测很省心比早期版本手动配置原生库要方便得多。提示如果你的目标机器是Win7老系统ONNX Runtime高版本可能不兼容建议用1.15或更早的版本并选VS2017编译的运行时。这个细节在项目规划阶段就要确认否则后期分发时会很被动。2.2 模型转换PyTorch转ONNXDDColor官方开源仓库提供了训练好的权重但给的是PyTorch的.pth文件。要把模型跑在ONNX Runtime上第一步就是把权重导出成ONNX格式。这里贴一个我实际可用的导出脚本基于DDColor官方仓库路径按你自己的目录调整。import torch from ddcolor import DDColor device torch.device(cpu) model DDColor().to(device) checkpoint torch.load(ddcolor_pretrain.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict] if model_state_dict in checkpoint else checkpoint, strictTrue) model.eval() dummy_input torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy_input, ddcolor.onnx, input_names[input], output_names[output], opset_version17, dynamic_axes{ input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width} } ) print(ONNX export done.)这里我用了dynamic axes把batch、height、width三个维度设成动态。实际使用中batch固定为1但height和width最好做成动态的因为输入照片尺寸不固定。如果你不想处理动态形状带来的内存不确定性也可以固定成512x512输入但那样就得在预处理时做比例裁剪或者变形会损失一部分内容。我个人建议开动态形状操作并不复杂C#端只是多设置一个DenseTensor的维度而已。导出时如果遇到算子不支持的情况比如某些自定义算子需要改模型的forward或者查一下ONNX支持的算子版本。DDColor官方导出的完整模型我没有遇到这类问题可能是仓库里做了兼容处理。如果你有一个特殊版本先检查opset_version太老可能缺算子太新可能C#端老版Runtime不支持折中选17比较稳。2.3 验证模型输入输出模型导出以后先用Python的ONNX Runtime做一次推理验证确保权重没问题再进入C#开发省得两头排查。import onnxruntime as ort import numpy as np from PIL import Image sess ort.InferenceSession(ddcolor.onnx) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name img Image.open(test.jpg).convert(RGB).resize((512, 512)) img_np np.asarray(img, dtypenp.float32) / 255.0 img_tensor img_np.transpose(2, 0, 1)[None, ...] # (1,3,512,512) output sess.run([output_name], {input_name: img_tensor})[0] out_np np.clip(output[0].transpose(1, 2, 0), 0.0, 1.0) * 255.0 out_img Image.fromarray(out_np.astype(np.uint8)) out_img.save(test_out.jpg)这段代码跑通以后把ddcolor.onnx拷贝到C#项目的输出目录或者放到一个指定路径下。C#端就不需要任何Python环境了。这里我踩过一个坑PNG图片如果带Alpha通道读取时一定先convert(RGB)去掉Alpha否则通道数不对输入模型会被拒绝C#端也一样后面会详细说。3. 核心实现C#推理代码3.1 加载模型与创建推理会话C#端加载模型核心是InferenceSession这个类。创建会话时还可以指定执行提供程序即硬件加速方式。如果用户机器支持DirectML可以用DmlExecutionProvider否则用CPU。我实际项目里的做法是默认优先尝试GPU/DirectML失败就回退到CPU。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class DDColorInferencer : IDisposable { private InferenceSession _session; public DDColorInferencer(string modelPath, bool useGpu true) { var sessionOptions new SessionOptions(); if (useGpu) { try { sessionOptions.AppendExecutionProvider_DML(); } catch { // DML不可用时静默回退CPU } } sessionOptions.OptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; _session new InferenceSession(modelPath, sessionOptions); } // ... }上面这段代码里我特意把OptimizationLevel设成了ORT_ENABLE_ALLONNX Runtime在加载模型时会做计算图优化包括算子融合、常量折叠等这些优化对推理速度影响很大。实测开启全部优化后CPU推理时间能降低20%到40%这个比例相当可观。AppendExecutionProvider_DML()是DirectML执行提供程序的入口它在支持DirectX 12的Windows设备上都能用不用额外装CUDA。缺点是第一次运行会有一小段初始化时间而且部分机器上内存占用偏高但整体推理速度比CPU快很多尤其是遇到大尺寸图片时。如果不确定用户硬件建议做成配置项让用户自己选。3.2 图像预处理从Bitmap到TensorC#端拿到一张图片后要先转成模型需要的张量格式。DDColor模型的输入要求是形状[1, 3, height, width]数据类型float32取值范围0到1不要用0到255通道顺序RGB不是BGR所以预处理流程是读取图片 → 确保是RGB三通道 → 缩放尺寸 → 转float32并除以255 → 按CHW顺序填入Tensor。public static DenseTensorfloat Preprocess(Bitmap bitmap, int targetHeight, int targetWidth) { using var resized new Bitmap(bitmap, new Size(targetWidth, targetHeight)); var tensor new DenseTensorfloat(new[] { 1, 3, targetHeight, targetWidth }); for (int y 0; y targetHeight; y) { for (int x 0; x targetWidth; x) { var pixel resized.GetPixel(x, y); int baseIdx y * targetWidth x; tensor[0, 0, y, x] pixel.R / 255f; tensor[0, 1, y, x] pixel.G / 255f; tensor[0, 2, y, x] pixel.B / 255f; } } return tensor; }这段代码用到了Bitmap.GetPixel性能不是最优的如果图片尺寸很大比如2000x2000以上逐像素调用会比较慢。优化方案是用Bitmap.LockBitsMarshal.Copy把像素数据一次性拷贝到byte数组然后操作数组速度能快一个数量级。代码会复杂一些但对用户体验影响很大。我建议先用上面的简单版本跑通流程再考虑优化。尺寸缩放方面DDColor官方训练时一般输入是512x512但实际推理可以放大一些。我的经验是如果你想看细节更好的上色效果可以试试把短边放到512到1024之间。分辨率越高上色越细致但耗时也成倍增加。需要找一个平衡点这在3.4节展开。注意很多图片实际是灰度图只有单通道。处理时必须先复制成三个通道把相同的灰度值分别填到R、G、B里否则张量维度对不上。这个在读取文件时就要判断比如用PixelFormat.Format24bppRgb强制转换。3.3 推理与后处理从Tensor到Bitmap预处理完成后调用ONNX Runtime的Run方法执行推理然后把输出Tensor转换回Bitmap。public Bitmap Infer(Bitmap input, int targetHeight 512, int targetWidth 512) { var inputTensor Preprocess(input, targetHeight, targetWidth); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; using (var results _session.Run(inputs)) { var outputTensor results.First().AsTensorfloat(); return Postprocess(outputTensor, targetHeight, targetWidth); } } private Bitmap Postprocess(Tensorfloat output, int height, int width) { var bmp new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb); for (int y 0; y height; y) { for (int x 0; x width; x) { int r (int)(Math.Clamp(output[0, 0, y, x], 0f, 1f) * 255f); int g (int)(Math.Clamp(output[0, 1, y, x], 0f, 1f) * 255f); int b (int)(Math.Clamp(output[0, 2, y, x], 0f, 1f) * 255f); bmp.SetPixel(x, y, Color.FromArgb(r, g, b)); } } return bmp; }后处理的关键是必须对输出值做0到1的裁剪。神经网络输出理论上应该在0到1范围内但实际推理时可能因为浮点误差出现个别像素略微越界直接乘以255再转byte会导致颜色异常比如出现噪点或者灰阶跃迁。用Math.Clamp包一层就能避免。这里还有个细节_session.Run(inputs)返回的result是一个DisposableNamedOnnxValue对象它持有原生内存。如果不using释放长时间跑推理会导致内存持续增长最终占用几个GB。建议所有推理结果都用using包起来这是C#端最容易忽视的性能隐患。关于输出尺寸我这边直接把输出尺寸固定成和预处理输入一致。如果你输入是512x512输出就是512x512。如果你后续要拿原始分辨率的上色结果需要额外做一个超分辨率或者尺寸放大的步骤这不属于DDColor模型本身的功能。3.4 性能优化策略DDColor模型参数体量不算特别大但注意力机制部分计算密集CPU推理速度仍然有优化空间。我实测了几种优化策略效果排序如下CPU线程数调整ONNX Runtime默认会使用所有CPU核心但对于桌面应用来说把所有核心吃满会导致界面卡顿。建议把线程数设置为物理核心数减一或者提供一个线程数配置项。设置方式sessionOptions.AddSessionConfigEntry(session.intra_op.num_threads, 4);输入尺寸控制模型推理时间跟输入分辨率基本是平方关系。512x512大约1到2秒1024x1024可能要5到8秒。如果应用面向普通用户默认用512x512是性价比最高的选择如果想提升画质可以加一个“高清上色”的开关让用户自行选择。推理结果缓存如果你的工具是批量处理多张图片相同的图片尺寸和预处理参数下可以缓存输入图像的灰度特征即输入tensor避免重复转换。不过这个收益有限主要瓶颈在模型推理本身我建议把精力放在前面两项。使用DirectML/GPUCPU推理和使用DirectML的GPU推理速度差距可以达到3到5倍。但DirectML第一次创建session会有一段编译时间大概几秒到十几秒不等这个初始化过程适合放到后台线程里提前执行。另外如果图形驱动比较老DirectML运行可能会报错需要做好回退逻辑。4. 实操过程与完整代码4.1 完整调用流程把上面几段拼起来一个完整的DDColor推理调用流程是这样的using System.Drawing; var inferencer new DDColorInferencer(models/ddcolor.onnx, useGpu: true); using (var src new Bitmap(old_photo.jpg)) { using (var result inferencer.Infer(src, 512, 512)) { result.Save(colored_photo.jpg, System.Drawing.Imaging.ImageFormat.Jpeg); } }这个流程适用于WinForms、WPF、ASP.NET Core等各类C#项目。WinForms里你可以在点击按钮的事件里调用WPF里要留意UI线程阻塞问题建议封装成Task.Run异步执行。我这里再贴一个完整版的DDColorInferencer类包括构造函数、预处理、推理、后处理和资源释放你可以直接复制到项目里改改就能用。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; public class DDColorInferencer : IDisposable { private InferenceSession _session; private bool _disposed; public DDColorInferencer(string modelPath, bool useGpu true, int threads 0) { var sessionOptions new SessionOptions(); sessionOptions.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; if (useGpu) { try { sessionOptions.AppendExecutionProvider_DML(); } catch { // 回退CPU } } if (threads 0) { sessionOptions.AddSessionConfigEntry(session.intra_op.num_threads, threads.ToString()); } _session new InferenceSession(modelPath, sessionOptions); } public Bitmap Infer(Bitmap input, int targetHeight 512, int targetWidth 512) { var inputTensor Preprocess(input, targetHeight, targetWidth); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, inputTensor) }; using (var results _session.Run(inputs)) { var outputTensor results.First().AsTensorfloat(); return Postprocess(outputTensor, targetHeight, targetWidth); } } private static DenseTensorfloat Preprocess(Bitmap bitmap, int targetHeight, int targetWidth) { using var resized new Bitmap(bitmap, new Size(targetWidth, targetHeight)); var tensor new DenseTensorfloat(new[] { 1, 3, targetHeight, targetWidth }); var rect new Rectangle(0, 0, targetWidth, targetHeight); var bmpData resized.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); int stride bmpData.Stride; byte[] pixels new byte[stride * targetHeight]; Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length); resized.UnlockBits(bmpData); for (int y 0; y targetHeight; y) { for (int x 0; x targetWidth; x) { int idx y * stride x * 3; // 注意BGR顺序 tensor[0, 0, y, x] pixels[idx 2] / 255f; tensor[0, 1, y, x] pixels[idx 1] / 255f; tensor[0, 2, y, x] pixels[idx] / 255f; } } return tensor; } private static Bitmap Postprocess(Tensorfloat output, int height, int width) { var bmp new Bitmap(width, height, PixelFormat.Format24bppRgb); var rect new Rectangle(0, 0, width, height); var bmpData bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); int stride bmpData.Stride; byte[] pixels new byte[stride * height]; for (int y 0; y height; y) { for (int x 0; x width; x) { int idx y * stride x * 3; pixels[idx] (byte)(Math.Clamp(output[0, 2, y, x], 0f, 1f) * 255f); pixels[idx 1] (byte)(Math.Clamp(output[0, 1, y, x], 0f, 1f) * 255f); pixels[idx 2] (byte)(Math.Clamp(output[0, 0, y, x], 0f, 1f) * 255f); } } Marshal.Copy(pixels, 0, bmpData.Scan0, pixels.Length); bmp.UnlockBits(bmpData); return bmp; } public void Dispose() { if (!_disposed) { _session?.Dispose(); _disposed true; } GC.SuppressFinalize(this); } }注意上面代码里我用了LockBits来加速像素读写这一步能把预处理时间从几百毫秒降低到几十毫秒尤其处理大图时感知非常明显。另外预处理时我把RGB读成BGR的reverse操作是因为Format24bppRgb实际内存布局是BGR顺序这是Windows位图的老规矩必须手动翻转。4.2 参数说明与内存管理推理时最关键的两个参数是targetHeight和targetWidth它们直接决定输出图片尺寸和推理耗时。我习惯的做法是保留原图的宽高比把短边对齐到目标尺寸然后再用模型推理。这样做的好处是上色后图片不容易变形。但注意DDColor模型内部有下采样和上采样操作输入尺寸不是任意值都可以。某些尺寸可能会导致输出边缘出现黑边最稳妥的方式是让宽高能被8整除。你可以写一个简单的取整函数int AlignDown(int value, int align 8) { return value / align * align; }推理前把原始Bitmap缩放到对齐后的尺寸。如果你原图是1080x1440短边1080对齐到1080长边1440对齐到1440两者都能被8整除直接缩放就行。如果原图是1000x1300就会稍微裁剪或者轻微拉伸这种极端情况在真实项目中并不多见按需处理即可。内存管理方面ONNX Runtime本身是原生代码它的内存不受.NET GC控制。InferenceSession和每个Run返回的结果都要妥善释放。我在实际项目中发现如果不释放每个推理结果处理几百张图片后内存占用轻易突破2GB。用using包裹是必须的不要偷懒。5. 常见问题与排查技巧5.1 模型加载失败或提示找不到onnxruntime.dll这个问题在把项目部署到其它机器时最常遇到。原因通常是目标机器没有安装VC运行库或者NuGet包自带的原生DLL没有被复制到输出目录。排查思路确认项目bin\Release目录下是否存在onnxruntime.dll和onnxruntime_providers_shared.dll如果缺少右键NuGet包看“属性”里的“Copy Local”是否设为True或者手动把runtimes\win-x64\native下的文件拷贝到exe同目录检查目标机器是否安装了VC 2019/2022 Redistributable还有一个坑如果你的项目是AnyCPU编译ONNX Runtime会加载x64还是x86的原生库取决于系统位数。建议把项目平台直接设成x64避免在32位进程中加载64位原生库导致崩溃。5.2 输出全黑或者全灰这是最常见的后处理错误。原因基本可以锁定为输出像素值没有正确从0-1标定到0-255或者通道顺序搞反了。我用一个快速自查方法输出图像如果整体偏灰、像蒙了一层雾多半是没做Math.Clamp导致负值或大于1的值被强转成0或255附近的异常值。如果图像看起来是负片效果那就是RGB和BGR顺序反了。检查一下预处理和后处理的通道交换代码是否对应。5.3 推理速度慢或者界面卡死如果调用推理的代码直接跑在UI线程里界面必然卡死。务必用async/await包一层Task.Run或者在后台线程执行推理。同时不要在推理过程中操作UI控件把结果Bitmap传回UI线程后再显示。卡死还有一个潜在原因是CPU线程数设置过高。如果用户机器是8核16线程ONNX Runtime默认使用16个线程推理Windows桌面端调度会变得很激进。推荐把线程数设置为物理核数的一半或减一推理时间略微增加但系统整体流畅度明显提升。5.4 输入图片有透明背景或16位色深PNG带Alpha通道时Bitmap.GetPixel返回的R、G、B实际上是去除了Alpha的背景色直接转会有问题。更安全的做法是统一转换成Format24bppRgb再处理我这边的代码里LockBits已经指定了PixelFormat.Format24bppRgb系统会自动丢弃Alpha。16位色深的图片比如部分扫描仪输出的TIFF在.NET里解码可能变成Format48bppRgb直接LockBits 24bpp会报错。可以先调用bitmap.Clone(new Rectangle(0,0,w,h), PixelFormat.Format24bppRgb)转换一次再走预处理流程。5.5 模型输出尺寸与输入不一致如果你在导出ONNX时没有设置动态维度模型会固定输出训练时的尺寸。比如你导出时固定了512x512那不管输入多大输出都是512x512。这时候C#端Postprocess里使用targetHeight和targetWidth就会出错。解决方案有两个一是重新导出带动态维度的模型二是C#端读取输出Tensor的实际维度动态创建Bitmapvar dims outputTensor.Dimensions; int outH (int)dims[2]; int outW (int)dims[3];这个方法对两种模型都适用建议直接用这个稳妥版本。6. 实际体验与扩展建议整套流程跑通之后我把这个推理引擎接进了一个老照片批量上色工具里配合线程池处理多张图片。实际使用下来CPU推理一张512x512照片约1.5秒用DirectML切到独立显卡后能压到0.4秒左右。对于批量处理几百张老照片的场景这个速度完全能接受。我还发现一个提升体验的小技巧如果图片本身是黑白但带有轻微噪点或划痕可以先做一次简单的降噪再上色DDColor上色效果会更干净。如果图片本身已经有颜色只是褪色发黄可以直接把Saturation饱和度提高不一定需要走模型。后续可以扩展的方向包括接入摄像头实时上色、把模型量化到INT8减少体积、或者用ONNX Runtime的C# API做异步推理管线。DDColor这个模型本身还有视频上色的变体如果你做老电影修复也可以尝试。我在实际使用中还有一个体会DDColor模型处理人像时肤色表现很好但处理偏色的老照片时如果原图有很多噪点或者JPEG压缩痕迹上色后这些瑕疵也会被放大。建议在预处理阶段增加一个轻量去噪算法比如中值滤波或者快速非局部均值效果会更稳定。最后说一个容易被忽略的细节模型文件本身大概有300MB左右首次加载到内存有几百毫秒延迟。如果工具启动时需要立即响应建议在后台线程预加载模型而不是等到用户点击处理时才创建InferenceSession。整个项目做下来最大的心得就是深度学习模型的落地难点往往不在模型本身而在工程化过程中的各种细节。把输入输出格式对齐、内存释放、异常回退这些事情处理好C#结合ONNX Runtime部署DDColor这件事一点都不复杂。希望这篇博客能帮你把坑提前踩平。本文还有配套的精品资源点击获取