简介:本资源是一套完整的基于Python机器学习的舌苔检测系统实现方案,面向计算机视觉初学者、中医信息化研究者及毕业设计/课程设计实践者,旨在解决传统舌诊主观性强、标准化不足的问题,通过深度学习技术辅助体质辨识与健康状态初步评估。压缩包共113个文件,含26个核心Python脚本(模型训练、数据预处理、UI交互等)、6个预训练EfficientNet权重.pth文件、7张典型舌象样本jpg、4份Word版论文文档(含开题、正文、答辩稿)、2个PyQt设计的.ui界面文件,以及日志events文件和配置json等,整体大小为105.15MB。已有311人学习下载,资源结构完整,涵盖数据集构建逻辑、模型调优过程、中医舌象标签体系说明及可直接运行的GUI检测程序,特别适合需要复现医学AI项目、理解跨学科(中医+CV)落地路径的学习者。
1. 舌苔不是“拍张照就能认出来”的黑匣子:为什么用 Python 做机器学习舌苔检测,得先过数据关、标注关、泛化关
你手上有一堆舌象照片——有手机拍的、有内窥镜采的、有医院设备导出的 TIFF,光照不一、舌头伸展角度各异、背景杂乱、甚至带牙印或唾液反光。这时候直接扔进 ResNet 或 YOLO 训练,模型在测试集上准确率 92%,一到社区卫生站新拍的图就掉到 63%。这不是模型不行,是“舌苔检测”根本不是标准图像分类任务:它本质是多尺度纹理+局部色域+解剖结构约束下的细粒度语义分割问题。真正落地的系统,必须同时解决三件事:怎么把“薄白苔”“黄腻苔”“剥落苔”这些中医术语,映射成可计算的像素级特征;怎么让模型不把饭粒当腐苔、不把阴影当剥脱;怎么在没 GPU 服务器的基层诊所里,用 4GB 内存笔记本跑通推理。这篇笔记不讲论文框架怎么搭、不列参考文献格式,只讲我用 Python 在三个真实项目(社区中医辅助诊断系统、舌诊仪嵌入式模块、高校教学实训平台)中反复验证过的最小可行路径:从原始舌图采集规范,到训练时必调的 4 个 DataLoader 参数,再到部署时绕不开的 OpenCV 预处理陷阱。适合正在写毕设、做课设、或想快速验证中医 AI 落地可能性的工程师和研究生。
2. 数据不是越多越好,而是“够准、够稳、够糙”:舌苔图像采集与标注的硬性约束
2.1 采集端必须守住的三条红线:光照、构图、设备一致性
舌象图像质量直接决定模型上限。我们曾用同一台 iPhone 12,在不同时间段、不同室内灯光下采集 200 张舌图,仅因白平衡偏移 5%,模型对“淡红舌+薄白苔”的识别 F1 就下降 18%。因此,采集阶段必须强制执行:
- 光照:禁用闪光灯;使用双 LED 柔光灯箱(色温 5000K±100K),左右 45°夹角布光,避免中心高光与边缘阴影;
- 构图:舌体需占画面 60%~75%,舌尖居中,舌面完全展开无卷曲,背景统一为哑光深灰(RGB 40,40,40);
- 设备:固定机型(推荐华为 P50 Pro 或 iPhone 13,因 CMOS 传感器白平衡稳定性高),关闭自动 HDR 和 AI 场景增强。
提示:不要用“手机拍完传电脑再标注”这种流程。我们实测发现,微信压缩、iOS HEIC 转 JPEG、Windows 照片查看器自动色域转换,会引入不可逆的色偏。正确做法是:手机直连电脑,用
adb pull(Android)或imageCaptureAPI(iOS)获取原始未压缩图像,后缀必须为.png。
2.2 标注不是画框,而是定义“舌苔区域”的解剖学边界
舌苔检测 ≠ 舌体检测。很多开源项目把整个舌头框出来就叫“舌苔标注”,这是致命错误。中医舌诊中,“苔”特指舌面上附着的苔状物,其边界由舌乳头分布密度突变处界定,而非舌体轮廓。我们采用三级标注法:
| 标注层级 | 标注内容 | 工具要求 | 典型错误 |
|---|---|---|---|
| Level 1:舌体掩膜(Tongue Mask) | 手动勾勒完整舌体轮廓(含舌下静脉) | LabelMe 或 CVAT | 把牙龈、嘴唇误纳入 |
| Level 2:苔区掩膜(Coating Mask) | 在舌体掩膜内,精确圈出苔质覆盖区域(避开舌根部生理性厚苔) | 放大至 300% 逐像素修正 | 将舌面裂纹、沟壑当作无苔区剔除 |
| Level 3:苔质属性标签(Coating Attribute) | 对每个苔区标注:颜色(R/G/B 均值)、颗粒度(Laplacian 方差)、润燥(HSV 中 V 分量标准差) | Excel 表格关联 PNG 文件名 | 用“黄苔”“白苔”文字标签代替数值,导致无法回归分析 |
实际操作中,我们用 Python 脚本批量校验标注一致性:
# validate_annotation_consistency.py import cv2 import numpy as np import os def check_mask_overlap(tongue_mask_path, coating_mask_path): """检查苔区掩膜是否完全在舌体掩膜内""" tongue = cv2.imread(tongue_mask_path, cv2.IMREAD_GRAYSCALE) coating = cv2.imread(coating_mask_path, cv2.IMREAD_GRAYSCALE) # 计算重叠像素数 overlap = np.sum(np.logical_and(tongue > 0, coating > 0)) coating_total = np.sum(coating > 0) if overlap != coating_total: print(f"[ERROR] {os.path.basename(coating_mask_path)}: 苔区超出舌体边界!") return False return True # 批量校验 for root, _, files in os.walk("dataset/annotations"): for f in files: if f.endswith("_coating.png"): coating_path = os.path.join(root, f) tongue_path = coating_path.replace("_coating.png", "_tongue.png") check_mask_overlap(tongue_path, coating_path)这段代码会在每次标注后自动运行,只要苔区像素有任何一个落在舌体掩膜外,就报错中断。这比人工抽检可靠得多——我们团队曾因漏检 3 张越界标注,导致后续训练中模型学会把牙龈纹理当成“黄腻苔”。
2.3 数据增强不能“无脑加”,舌象有不可破坏的生理约束
常规图像增强(随机旋转、裁剪、色彩抖动)对舌象极易翻车。比如:
- 随机水平翻转 → 舌下静脉左右颠倒,违反解剖事实;
- 随机裁剪 → 切掉舌尖(主心肺区)或舌根(主肾区),丢失辨证关键信息;
- HSV 色调偏移 → “淡红舌”变“绛红舌”,中医判读逻辑崩塌。
我们只保留四类安全增强,并全部用 OpenCV 原生函数实现(避免 PIL 的色彩空间转换误差):
# safe_augmentation.py import cv2 import numpy as np def safe_brightness_contrast(img, alpha=1.0, beta=0): """仅调整亮度/对比度,不改变色调饱和度""" return cv2.convertScaleAbs(img, alpha=alpha, beta=beta) def safe_gaussian_noise(img, mean=0, std=5): """添加均值为 0 的高斯噪声,模拟手机传感器噪点""" noise = np.random.normal(mean, std, img.shape).astype(np.uint8) return cv2.add(img, noise) def safe_jpeg_compression(img, quality=85): """模拟手机上传压缩失真""" encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality] _, encimg = cv2.imencode('.jpg', img, encode_param) return cv2.imdecode(encimg, 1) def safe_local_contrast_enhance(img, clip_limit=2.0, tile_grid_size=(8,8)): """CLAHE 增强,专用于提升苔质纹理可见度""" clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size) lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) lab[...,0] = clahe.apply(lab[...,0]) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)注意:safe_local_contrast_enhance中的clip_limit=2.0是血泪经验——超过 2.5,苔面颗粒会被过度锐化,模型开始拟合噪点而非真实苔质;低于 1.2,则舌面湿滑感、干燥感等关键润燥特征消失。这个参数必须在验证集上用网格搜索确定,不能凭感觉设。
3. 不是换 backbone 就能提点,舌苔检测的核心瓶颈在“小目标+低对比度+类内差异大”
3.1 为什么 U-Net 比 Mask R-CNN 更适合作为舌苔分割基线
很多人第一反应是上 DETR 或 Mask R-CNN,但我们在 3 个数据集(共 1276 张标注图)上的实测表明:U-Net(with ResNet34 encoder)在 Dice Score 上稳定高出 5.2~7.8 个百分点。原因很实在:
- 小目标问题:典型苔质斑块直径仅 15~40 像素(在 512×512 输入下),Mask R-CNN 的 RoIAlign 会严重模糊边缘;
- 低对比度问题:薄白苔与舌质色差常 <15 ΔE(CIEDE2000 色差),U-Net 的跳跃连接能保留浅层纹理细节,而 R-CNN 的特征金字塔在 P2/P3 层已丢失关键梯度;
- 类内差异大:同一“黄腻苔”在不同患者身上,可能呈现颗粒状、糊状、油状三种形态,U-Net 的密集预测头比 R-CNN 的 box + mask 两阶段更适应形态连续变化。
我们最终选用的结构是U-Net++ with SCSE attention(而非 CBAM),因为 SCSE 的通道注意力 + 空间注意力组合,对舌面局部色域变化更敏感。PyTorch 实现关键片段如下:
# unet_pp_scse.py import torch import torch.nn as nn from torchvision.models import resnet34 class SCSEBlock(nn.Module): def __init__(self, channel, reduction=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.channel_excitation = nn.Sequential( nn.Linear(channel, int(channel // reduction)), nn.ReLU(inplace=True), nn.Linear(int(channel // reduction), channel), nn.Sigmoid() ) self.spatial_excitation = nn.Sequential( nn.Conv2d(channel, 1, kernel_size=1), nn.BatchNorm2d(1), nn.Sigmoid() ) def forward(self, x): chn_se = self.avg_pool(x).view(x.size(0), -1) chn_se = self.channel_excitation(chn_se).unsqueeze(2).unsqueeze(3) chn_se = torch.mul(x, chn_se) spa_se = self.spatial_excitation(x) spa_se = torch.mul(x, spa_se) return chn_se + spa_se class EncoderBlock(nn.Module): def __init__(self, in_c, out_c): super().__init__() self.conv1 = nn.Conv2d(in_c, out_c, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_c) self.conv2 = nn.Conv2d(out_c, out_c, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_c) self.scse = SCSEBlock(out_c) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.bn1(self.conv1(x))) x = self.relu(self.bn2(self.conv2(x))) x = self.scse(x) return x注意SCSEBlock中reduction=16是经验值:设为 8 时,通道注意力过强,模型忽略空间位置信息,把舌根厚苔误判为全舌覆盖;设为 32 时,空间注意力衰减过快,无法聚焦苔质边缘。这个值必须与你的输入分辨率匹配——若用 1024×1024 输入,需同步调高 reduction 至 32。
3.2 损失函数不能只用 Dice,必须叠加“解剖约束损失”
标准 Dice Loss 对舌苔分割效果平平,因为它不区分“误判舌尖”和“误判舌边”。而中医认为:舌尖对应心肺,舌中对应脾胃,舌根对应肾。若模型把舌尖的薄白苔判成无苔,比把舌边误判成黄腻苔严重得多。
我们设计了Anatomy-Aware Dice Loss(AADL),在 Dice 基础上,按解剖分区加权:
# anatomy_aware_loss.py import torch import torch.nn.functional as F def anatomy_aware_dice_loss(pred, target, weight_map): """ weight_map: 与 pred/target 同尺寸的 tensor,值为 0.3(舌尖)、0.5(舌中)、0.2(舌根) 依据《中医诊断学》舌区划分标准生成 """ smooth = 1e-5 pred = torch.sigmoid(pred) # pred 是 logits,需 sigmoid intersection = (pred * target * weight_map).sum(dim=(1,2,3)) union = (pred * weight_map).sum(dim=(1,2,3)) + (target * weight_map).sum(dim=(1,2,3)) dice = (2. * intersection + smooth) / (union + smooth) return 1 - dice.mean() # 构建解剖权重图(示例:512x512 输入) def build_anatomy_weight_map(H, W): y = torch.linspace(0, 1, H).view(-1, 1) x = torch.linspace(0, 1, W).view(1, -1) weight = torch.zeros(H, W) # 舌尖:y < 0.3 weight[y < 0.3] = 0.3 # 舌中:0.3 <= y < 0.7 weight[(y >= 0.3) & (y < 0.7)] = 0.5 # 舌根:y >= 0.7 weight[y >= 0.7] = 0.2 return weight.unsqueeze(0).unsqueeze(0) # [1,1,H,W]这个weight_map不是超参,而是固定规则——它来自《中医诊断学》教材中舌区划分的量化坐标(舌尖占舌长前 30%,舌中占中间 40%,舌根占后 30%)。我们实测 AADL 相比纯 Dice,使舌尖区域 Dice 提升 12.3%,整体 Dice 提升 4.1%,且模型不再出现“全舌判为无苔”的灾难性错误。
3.3 推理时别信 softmax,用 CRF 后处理才是舌苔边缘救星
U-Net 输出的 logits 经过 sigmoid 后,边缘常呈“毛边状”,尤其在薄白苔与舌质交界处。直接阈值化(如 0.5)会导致苔区收缩或膨胀。我们采用轻量级 DenseCRF(Dense Conditional Random Field),仅需 3 行代码集成:
# crf_postprocess.py import pydensecrf.densecrf as dcrf from pydensecrf.utils import unary_from_softmax, create_pairwise_bilateral def apply_crf(image, prob_map, n_iters=5): """ image: uint8, [H,W,3] prob_map: float32, [1,C,H,W], C=2 (background, coating) """ H, W = image.shape[:2] d = dcrf.DenseCRF2D(W, H, 2) # Unary potential U = unary_from_softmax(prob_map[0]) # [2,H,W] d.setUnaryEnergy(U) # Pairwise potential feats = create_pairwise_bilateral(sdims=(80, 80), schan=(0.1,), img=image, chdim=2) d.addPairwiseEnergy(feats, compat=10) Q = d.inference(n_iters) return np.argmax(Q, axis=0).reshape((H, W)) # 使用示例 # pred_logits = model(input_img) # [1,2,512,512] # prob = torch.softmax(pred_logits, dim=1).cpu().numpy() # crf_result = apply_crf(original_img, prob) # [512,512]关键参数说明:
sdims=(80,80):空间尺度,设为图像短边的 15%(512×512 下即 80),太小则平滑不足,太大则抹平真实苔质纹理;schan=(0.1,):颜色尺度,0.1 是经验值——设为 1.0 时,CRF 过度依赖颜色,把同色饭粒也拉进苔区;设为 0.01 时,退化为纯空间平滑,失去边缘保持能力;compat=10:兼容系数,大于 5 即可,过高会导致欠平滑。
我们对比过:不用 CRF,边缘 Dice 为 0.68;用 CRF 后升至 0.83,且医生反馈“更接近肉眼观感”。
4. 避坑:舌苔检测系统上线前必须跨过的 4 个真实雷区
4.1 现象:模型在训练集 Dice 0.91,验证集 0.85,但部署到安卓手机上推理结果全黑
原因:训练时用torch.float32,部署时 TensorRT 或 ONNX Runtime 默认用float16,而舌象图像动态范围窄(RGB 值集中在 80~180),FP16 下大量像素被截断为 0,导致输入全黑。
解决:导出 ONNX 时强制指定opset_version=12,并在torch.onnx.export()中加入keep_initializers_as_inputs=True,同时在推理端用np.float32加载图像,不做任何 dtype 自动转换。
4.2 现象:同一张舌图,在 Windows 和 Linux 上推理结果差异达 15%
原因:OpenCV 的cv2.resize()在不同平台插值算法默认不同(Windows 用INTER_LINEAR,Linux 可能用INTER_AREA),且 Pillow 的Image.resize()与 OpenCV 不兼容。
解决:统一用 OpenCV 的cv2.INTER_LANCZOS4(高质量抗锯齿),并在所有平台显式指定:
resized = cv2.resize(img, (512,512), interpolation=cv2.INTER_LANCZOS4)4.3 现象:模型能识别“黄腻苔”,但把“染苔”(喝橙汁后舌面染黄)误判为病理性黄腻
原因:训练数据未包含染苔样本,且模型学到的是“黄色+颗粒感”联合特征,而非“苔质附着状态”。
解决:在数据集中强制加入 10% 染苔样本(用食用色素+水模拟),并设计辅助任务:训练一个二分类分支,判断“该黄色区域是否为可擦除染色”,监督信号来自医生标注的“染苔/真苔”标签。我们用共享 encoder + 独立 head 实现,多任务 loss 权重设为 0.7(主分割):0.3(染苔判别)。
4.4 现象:基层诊所用华为平板拍摄,模型输出“剥落苔”,但医生肉眼确认是正常舌象
原因:平板摄像头自动美颜开启,平滑了舌面纹理,模型将平滑区域误判为剥落。
解决:在预处理 pipeline 中加入“美颜检测模块”:计算图像 Laplacian 方差,若< 250(512×512 图像),则触发警告并启用增强版 CLAHE(clip_limit=3.0)强行恢复纹理。该阈值经 200 张真实美颜图标定得出,误报率 < 2%。
5. 不靠论文灌水,靠“舌象报告生成”让系统真正可用:从像素到辨证结论的闭环
5.1 把分割结果翻译成中医语言:不是“检测到黄腻苔”,而是“脾胃湿热证候支持度 87%”
模型输出只是像素掩膜,临床需要的是可解释的辨证结论。我们构建了一个轻量级规则引擎,将分割结果量化为中医证候指标:
| 舌象特征 | 计算方式 | 中医意义 | 权重 |
|---|---|---|---|
| 苔色偏黄度 | mean(HSV[:,:,0][coating_mask])(H 通道均值) | 黄为热,越黄热越盛 | 0.3 |
| 苔质颗粒度 | cv2.Laplacian(coating_mask.astype(np.uint8), cv2.CV_64F).var() | 颗粒粗大为湿浊壅盛 | 0.25 |
| 苔面润燥比 | np.mean(HSV[:,:,2][coating_mask]) / np.std(HSV[:,:,2][coating_mask]) | 润为津未伤,燥为津亏 | 0.2 |
| 舌质底色 | mean(RGB[tongue_mask])[0](R 通道均值) | 淡红为平人,绛红为热入营血 | 0.15 |
| 舌形胖瘦比 | area(coating_mask) / area(tongue_mask) | 苔厚而舌胖为痰湿,苔薄而舌瘦为阴虚 | 0.1 |
Python 实现核心逻辑:
# diagnosis_engine.py import numpy as np import cv2 def generate_tcm_diagnosis(coating_mask, tongue_mask, hsv_img, rgb_img): """ coating_mask: [H,W], 0/1 tongue_mask: [H,W], 0/1 hsv_img: [H,W,3], uint8 rgb_img: [H,W,3], uint8 """ # 1. 苔色偏黄度(H 通道,0~179) h_vals = hsv_img[:,:,0][coating_mask.astype(bool)] yellow_score = np.clip(np.mean(h_vals) / 179.0, 0, 1) # 归一化到 [0,1] # 2. 苔质颗粒度(Laplacian 方差) laplacian = cv2.Laplacian(coating_mask.astype(np.uint8), cv2.CV_64F) texture_score = np.clip(laplacian.var() / 1000.0, 0, 1) # 3. 苔面润燥比(V 通道均值/标准差) v_vals = hsv_img[:,:,2][coating_mask.astype(bool)] moisture_score = np.clip(np.mean(v_vals) / (np.std(v_vals) + 1e-6), 0, 5) / 5 # 4. 舌质底色(R 通道均值) r_vals = rgb_img[:,:,0][tongue_mask.astype(bool)] redness_score = np.clip(np.mean(r_vals) / 255.0, 0, 1) # 5. 舌形胖瘦比 coating_area = coating_mask.sum() tongue_area = tongue_mask.sum() thickness_ratio = coating_area / (tongue_area + 1e-6) # 加权综合得分(示例:脾胃湿热证) pi_weishire_score = ( 0.3 * yellow_score + 0.25 * texture_score + 0.2 * (1 - moisture_score) + # 润为阴,燥为阳,湿热需燥 0.15 * redness_score + 0.1 * np.clip(thickness_ratio, 0, 1) ) # 生成自然语言报告 if pi_weishire_score > 0.75: conclusion = "高度支持脾胃湿热证:苔黄腻、质粗、偏燥、舌质偏红、苔厚" elif pi_weishire_score > 0.5: conclusion = "部分支持脾胃湿热证,建议结合问诊进一步确认" else: conclusion = "暂不支持脾胃湿热证" return { "pi_weishire_score": round(pi_weishire_score, 3), "conclusion": conclusion, "details": { "yellow_score": round(yellow_score, 3), "texture_score": round(texture_score, 3), "moisture_score": round(moisture_score, 3), "redness_score": round(redness_score, 3), "thickness_ratio": round(thickness_ratio, 3) } } # 使用示例 # result = generate_tcm_diagnosis(crf_mask, tongue_mask, hsv_img, rgb_img) # print(result["conclusion"])这个引擎不依赖大模型,所有计算在毫秒级完成,且每项指标都有明确中医经典依据(如《中医诊断学》中“黄苔主热,腻苔主湿”)。医生看到的不是“模型置信度 0.89”,而是“苔黄腻、质粗、偏燥——高度支持脾胃湿热证”,这才是真正能进诊疗流程的输出。
5.2 用 Flask + Vue 做最小闭环系统:不追求炫酷界面,只保核心链路通
我们交付给社区卫生站的系统,前端只有 3 个按钮:拍照、上传、生成报告;后端用 Flask 承载模型,全程离线运行。关键在于规避常见 Web 部署陷阱:
- 图像传输:禁用 base64 编码(体积膨胀 33%),改用 multipart/form-data 上传原始 PNG;
- 内存控制:Flask 启动时预加载模型到 GPU,但每个请求用
torch.no_grad()+torch.cuda.empty_cache()清理显存,防止多并发时 OOM; - 超时保护:Nginx 配置
proxy_read_timeout 120;,因为舌象预处理(CLAHE + resize)在低端 CPU 上需 8~12 秒。
后端核心路由:
# app.py from flask import Flask, request, jsonify import torch import numpy as np import cv2 from model import TongueSegModel # 你的 U-Net++ 模型 from diagnosis_engine import generate_tcm_diagnosis app = Flask(__name__) model = TongueSegModel().eval().cuda() model.load_state_dict(torch.load("weights/best.pth")) @app.route('/analyze', methods=['POST']) def analyze_tongue(): if 'image' not in request.files: return jsonify({"error": "No image uploaded"}), 400 file = request.files['image'] img_bytes = np.frombuffer(file.read(), np.uint8) img = cv2.imdecode(img_bytes, cv2.IMREAD_COLOR) # 预处理 img_resized = cv2.resize(img, (512,512), interpolation=cv2.INTER_LANCZOS4) img_hsv = cv2.cvtColor(img_resized, cv2.COLOR_BGR2HSV) img_norm = (img_resized.astype(np.float32) / 255.0 - [0.485,0.456,0.406]) / [0.229,0.224,0.225] img_tensor = torch.from_numpy(img_norm.transpose(2,0,1)).unsqueeze(0).cuda() # 推理 with torch.no_grad(): pred = model(img_tensor) prob = torch.softmax(pred, dim=1).cpu().numpy() crf_mask = apply_crf(img_resized, prob) # 见 3.3 节 # 生成诊断 tongue_mask = get_tongue_mask(img_resized) # 简单阈值法获取舌体粗略掩膜 diagnosis = generate_tcm_diagnosis(crf_mask, tongue_mask, img_hsv, img_resized) return jsonify(diagnosis) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, threaded=False, processes=1)注意:
threaded=False, processes=1是关键——PyTorch 多线程在 Flask 下易引发 CUDA 上下文冲突,单进程最稳。并发靠 Nginx 反向代理 + 多实例部署解决。
最后说句实在话:做舌苔检测,最难的从来不是调参,而是让医生愿意每天用。我们最初做的炫酷三维舌象重建,医生说“看不出和肉眼有啥区别”;后来砍掉所有花活,只留“拍照→3秒→出报告”,使用率从 12% 跃升到 89%。技术要往土里扎,而不是往云上飘。希望帮到你。
本文还有配套的精品资源,点击获取