尧图精选

YOLO11蔬菜识别检测系统:从数据集训练到PyQt5 GUI完整工程

🕒 发布时间:2026/10/2 3:36:22 📁 来源:尧图网络
简介基于YOLO11深度学习的蔬菜识别检测系统完整项目配备Pyqt5图形界面、1026张标注好的数据集和训练好的模型可对西红柿、洋葱、土豆、胡萝卜、大白菜五类蔬菜进行高准确率识别。资源共2000个文件以jpg图像、txt标注信息为主另含Python源码、pt模型权重、yaml配置、csv评估曲线及演示视频图片压缩包约47.24MB结构清晰开箱即用。目前已有168人学习下载适合计算机、人工智能、自动化等专业学生用作毕业设计、课程设计或课后实战也适合企业开发者快速集成参考。除完整源码和模型外还附带安装使用教程与评估指标曲线可帮助复现训练过程、理解检测效果并支撑后续功能扩展或二次开发。评估曲线直观展示精度与召回率变化为模型调优提供数据支撑便于深入理解YOLO11的训练机制。1. 蔬菜识别检测系统一套把数据集、训练权重、GUI 集成完毕的 YOLO11 工程蔬菜识别不像人脸识别那样能直接找到成熟的工业级预训练权重。西红柿和青椒颜色相近、形态多变同一类蔬菜在不同生长阶段的外观差异很多时候比两个不同类别之间的均值差异还要大。用传统颜色阈值和形状模板去做换一个拍摄角度就会翻车。这套基于 YOLO11 的蔬菜识别检测系统把 1026 张标注好的图片、YOLO11 训练脚本、训练好的模型权重、PyQt5 图形界面和安装使用教程打包成一套开箱即用的工程环境装好之后选中一张蔬菜照片界面就会直接给出每个目标的类别、边界框和置信度不需要你再去网上翻数据集或者从零开始训练。它适合三类人做毕业设计或课程设计的本科生、想靠完整项目入门目标检测的 Python 开发者以及需要快速搭一个果蔬识别原型的个人或小团队。下面按选型、环境、训练、避坑、进阶的顺序拆开讲。2. 选型与架构拆解YOLO11 改了什么、目录怎么排、PyQt5 为什么不能碰推理2.1 为什么这套项目选 YOLO11 而不是 YOLOv8在 YOLO11 出来之前YOLOv8 是大多数检测项目的默认起点但这类蔬菜识别场景里YOLO11 的改动更值得用。最直接的一点是它在 backbone 里用 C3k2 模块替代了原来的 C2f 模块同样层数下参数量和计算量都更低。训练端体现在同样一张 8G 显存的卡上batch 可以开得更大训练时间肉眼可见地缩短推理端则体现在单帧延迟更低后续接到摄像头实时检测时帧率余量会大很多。第二点对蔬菜检测更关键YOLO11 在浅层特征融合上做得比 v8 更细。蔬菜数据集里经常出现的情况是一张图上有八九个目标挤在一起青椒和辣椒在颜色纹理上非常接近如果模型没有足够的浅层细节特征很容易把相邻目标合并成一个框或者把两个品种互相认错。浅层特征更充分等于给边界和纹理判断多留了一些余量。实际用下来我一般会把 imgsz 设成 640 训练识别小目标和重叠目标时 mAP 收益是实打实的。至于为什么不用 RT-DETR 这类 transformer 检测器原因也比较实际项目要给的是能一键跑起来的工程RT-DETR 对部署环境里 torch 版本更敏感而且 PyQt5 界面里做实时推理时DETR 类模型的预处理和后处理比 YOLO 系繁琐CPU 机器上延迟会明显偏高。YOLO11 的推理流水线已经被 ultralytics 封装得足够稳对初学者也好排查。2.2 整体链路GUI 负责展示推理独立成线程这套系统的链路并不复杂PyQt5 窗口接收用户选择的图片路径后台加载训练好的 YOLO11 权重对图片做前向推理得到 boxes、classes、conf再绘制标注框转成 QImage 之后显示到界面上。听起来是简单的“选图→推理→显示”但最容易翻车的点是推理跑在哪个线程里。如果直接把模型推理写在按钮的 clicked 槽函数里图片稍大或者模型稍慢窗口会瞬间卡死系统提示“无响应”。我一般会强制把推理拆到 QThread 里用信号把结果传回主线程更新 UI这样界面不会假死用户也能点“取消”之类的按钮中断操作。# inference_thread.py from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class InferenceThread(QThread): # 信号携带两个数据标注后的图像、结果对象 result_ready pyqtSignal(object, object) error_occurred pyqtSignal(str) def __init__(self, model_path, image_path, conf0.3, iou0.5, device0): super().__init__() self.model YOLO(model_path) # 这里加载权重避免每次推理都重新加载 self.image_path image_path self.conf conf self.iou iou self.device device def run(self): try: results self.model.predict( sourceself.image_path, confself.conf, iouself.iou, deviceself.device, verboseFalse, ) # results[0].plot() 返回的是 BGR 格式 ndarray annotated results[0].plot() self.result_ready.emit(annotated, results[0]) except Exception as e: self.error_occurred.emit(str(e))这段代码的关键点在于self.model YOLO(model_path)只加载一次而不是每次点击检测都重新读一遍权重文件。权重文件的加载时间通常在一秒到几秒之间放在run()外面能省掉大量重复开销。conf参数控制置信度阈值默认 0.25界面场景里我一般调到 0.3 以上避免把背景杂物切成目标iou控制 NMS 去重力度0.45 到 0.5 在蔬菜这种重叠较多的场景中比较平衡。2.3 拿到工程先看哪几个文件这种打包好的项目目录通常是下面这种分工方式即使具体文件名有出入逻辑也是相通的文件/目录作用建议打开顺序requirements.txt依赖清单版本约束第 1 个看data.yaml类别数、类别名、数据集路径第 2 个看train.py训练入口脚本第 3 个看GUI/ 或 main_window.pyPyQt5 界面代码第 4 个看runs/detect/ 目录训练产物best.pt 和指标曲线都在里面最后看我自己的习惯是先改 data.yaml再去动训练脚本。因为 data.yaml 里写的是数据集路径和类别映射路径写错的话后面所有训练都是白跑。GUI 代码放在最后看先把模型用命令行脚本跑通再连界面排查问题时能少一半干扰。3. 环境搭建与数据集核验从空 conda 环境到第一次推理3.1 创建虚拟环境与依赖安装这类工程我建议直接用 conda 建一个独立环境不要装在系统 Python 里因为 ultralytics 对 torch 的版本要求很具体系统环境里如果已经有老版本 torch排错成本会成倍上升。Python 版本选 3.9 或 3.10 都比较稳新版本 Python 偶尔会和个别依赖有兼容缝隙。# 创建并激活虚拟环境 conda create -n veg python3.9 -y conda activate veg # 升级 pip 后安装核心依赖 pip install -U pip pip install ultralytics pyqt5ultralytics安装的时候会顺带把 torch、torchvision、opencv 这些核心依赖拉下来所以这条命令之后理论上不需要再单独装 opencv。要注意的是pip install ultralytics默认会拉 CPU 版 torch如果机器有 N 卡且要训练建议先按官方给出的 CUDA 版本对应命令安装 torch再装 ultralytics避免重复下载两遍 torch。安装完之后做一个快速检查确认环境里的关键路径版本号和 GPU 状态避免后面跑训练时才发现根本没调用到显卡。# check_env.py import torch print(torch:, torch.__version__) print(cuda available:, torch.cuda.is_available()) if torch.cuda.is_available(): print(gpu:, torch.cuda.get_device_name(0)) from ultralytics import YOLO print(ultralytics ok)如果cuda available输出的是 False有一种可能是机器本身没有可用 GPU另一种可能是当前 torch 是 CPU 版或和驱动不匹配。训练小规模数据集用 CPU 也不是完全不行但 1026 张图片跑 100 个 epoch 会明显耗时建议还是先解决问题再开始。3.2 数据集结构与 YOLO 格式标签数据集这关是最值得花时间的环节。项目提供的是 1026 张标注好的图片通常已经按 YOLO 惯例分好目录结构。拿到后先确认下面这个布局是否完整datasets/vegetable/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 训练标签 txt │ └── val/ # 验证标签 txt └── data.yamlYOLO 格式的每个 txt 文件名必须和图片文件名一致比如tomato_001.jpg对应tomato_001.txt后缀不同但主名必须完全一样。txt 中每一行代表一个目标框格式是五个字段类别索引、归一化中心 x、归一化中心 y、归一化宽度、归一化高度。2 0.5123 0.6845 0.3120 0.2563 0 0.1432 0.4211 0.2215 0.3189这里第一列的类别索引从 0 开始这一点是坑中之坑后面避坑章节会细说。归一化坐标的意思是x_center / 图片宽度四个数值都应该落在 0 到 1 之间如果出现大于 1 的数训练时模型会计算出无意义的边界框位置表现为 loss 异常或者 mAP 完全乱掉。data.yaml 则是整个训练流程的数据描述文件典型内容如下# data.yaml path: datasets/vegetable # 数据集的根路径 train: images/train # 训练图片目录相对 path val: images/val # 验证图片目录相对 path nc: 6 # 类别总数 names: # 类别名称顺序必须和标注索引一致 0: tomato 1: pepper 2: potato 3: carrot 4: cucumber 5: eggplant注意path虽然在部分版本里可以写相对路径但我一般建议改成绝对路径或者把数据集放在工程目录下用相对路径。原因是一旦用 IDE 调试时工作目录不同相对路径解析会出错模型找不到数据集就真找不到了。3.3 标签文件预检脚本很多问题在训练之前就能查出来。我会写一个非常短的预检脚本遍历全部标签文件检查三类问题空标签、类别索引越界、坐标值越界。这种做法花五分钟后面能省出两小时排障。# check_labels.py import os def check_label_file(label_root, nc): empty 0 out_of_range 0 coord_error 0 for root, _, files in os.walk(label_root): for f in files: if not f.endswith(.txt): continue path os.path.join(root, f) with open(path, r) as fp: lines fp.read().strip().splitlines() if not lines: empty 1 print(空标签文件:, path) continue for line in lines: parts line.split() if len(parts) ! 5: coord_error 1 print(字段数量错误:, path, line) continue cls int(parts[0]) if cls nc or cls 0: out_of_range 1 print(类别索引越界:, path, line) coords [float(x) for x in parts[1:]] if any(c 0 or c 1 for c in coords): coord_error 1 print(归一化坐标越界:, path, line) print(f检查完成: 空标签 {empty}, 类别越界 {out_of_range}, 坐标异常 {coord_error}) # 类别数务必和 data.yaml 中的 nc 保持一致 check_label_file(datasets/vegetable/labels/train, nc6) check_label_file(datasets/vegetable/labels/val, nc6)这段脚本的意义有两个一是把可能的脏数据拦在训练之前二是确认数据集的类别索引规范和 data.yaml 一致。如果这里检测出的类别索引是 1 到 6而 data.yaml 里写的是 0 到 5那就说明标注工具生成的索引整体偏移了 1直接训练会得到一个完全错乱的模型。3.4 用训练好的权重跑通第一次推理环境没问题、标签也没问题之后先用项目自带的训练好的模型做一次推理验证这一步只验证环境链路不碰训练逻辑。命令行或是脚本两种方式都可以。# 用 ultralytics 的 CLI 直接跑推理 yolo detect predict modelruns/detect/train/weights/best.pt sourcedemo.jpg conf0.3推理成功之后同级目录会生成runs/detect/predict/里面是画好框的图片。到这一步说明环境链路已经完整权重可加载、预处理正常、后处理正常、输出能落盘。4. 训练参数与评估指标从训练命令到 mAP 曲线阅读顺序4.1 训练入口与关键参数环境跑通后如果想用自己的数据重新训练或者把模型的识别能力往特定场景方向再调一调就要走训练这条线。YOLO11 的训练入口不用写复杂的训练循环一条命令就能启动。# yolo11n.pt 作为预训练起点比自己训练的随机初始化收敛快很多 yolo detect train \ datadata.yaml \ modelyolo11n.pt \ epochs100 \ batch16 \ imgsz640 \ lr00.01 \ device0参数的选取直接决定训练效果和显存占用下面这张表是我在类似规模数据集上调整出来的经验值可以直接作为起点再根据自己机器的显存去缩减 batch。参数含义经验取值说明epochs完整训练轮数80~120蔬菜数据集 100 轮基本够收敛再多容易过拟合batch每批图片数量168G 显存用 16 较稳OOM 就降到 8 或 4imgsz训练输入尺寸640目标密集时 640 是性价比最高的起点lr0初始学习率0.01出现 loss 震荡或 NaN 就降到 0.003~0.005device训练设备编号0CPU 训练就写 cpu但速度会慢很多patience早停等待轮数30验证集指标连续 30 轮不提升就提前停4.2 训练产物哪些文件值得看训练结束之后runs/detect/train/目录下一堆文件其中真正值得关注的就几个。weights/best.pt是验证集上 mAP 最高的权重后面 GUI 和部署都用它weights/last.pt是最后一个 epoch 的权重如果训练后期过拟合了last.pt 反而可能比 best.pt 还差一般不用它。results.png把 loss 曲线、mAP 曲线、PR 曲线汇总在一张大图里是判断训练是否正常的最快入口confusion_matrix.png则能看出模型到底混淆了哪两个类别。# 查看训练目录下生成的产物 ls runs/detect/train/ # 预期会看到 weights/、results.png、confusion_matrix.png 等4.3 loss 曲线和 mAP 的判读顺序我拿到 results.png 时会按这个顺序看先看 box_loss 和 cls_loss 是不是持续下降且没有大的反弹再看验证集的 mAP50 是否稳定走高最后看混淆矩阵里对角线上的数字是不是明显大于其他位置。如果 box_loss 在下降但 mAP 一直起不来优先怀疑标签有问题而不是训练参数有问题。一个新手常犯的误区是只盯 mAP50。对蔬菜场景来说遮挡和重叠很常见只看到 mAP50 会高估模型实际能力mAP50-95 这个指标因为阈值更严格更能反映框位精度的真实水平。正常数据集上 mAP50 和 mAP50-95 的差距会在 15 到 25 个百分点左右如果差距过大说明框的位置普遍不够准。另外如果觉得整体指标还行但应用时发现某些类别一直认错建议按类别分开看 Precision 和 Recall。蔬菜类别的类间差异小单类 Pull 数据不够时个别类别指标特别低是完全正常的现象最有效的解决办法不是改参数而是补充对应类别的训练图片。4.4 把 best.pt 接进 GUI推理封装训练完成后接入 GUI核心是把模型推理封装成一个尽量少的接口界面层只关心“输入一张图得到一个带标注框的图”。我习惯把推理单独放到一个类里GUI 只调用它的predict_once方法。# detector.py from ultralytics import YOLO class VegetableDetector: def __init__(self, model_pathruns/detect/train/weights/best.pt, device0): self.model YOLO(model_path) self.device device def predict_once(self, image_path, conf0.3, iou0.5): results self.model.predict( sourceimage_path, confconf, iouiou, deviceself.device, verboseFalse, ) result results[0] # boxes 里包含 xyxy、置信度、类别索引和类别名 boxes result.boxes annotated result.plot() # 画好框的 BGR 图像 objects [] if boxes is not None: for box in boxes: objects.append({ class: result.names[int(box.cls[0])], conf: round(float(box.conf[0]), 3), xyxy: [round(float(v), 1) for v in box.xyxy[0]], }) return annotated, objectskwargs: this project includes注该 model 是否已包含 GUI 内部利用此类未知。就项目正文而言这套系统包含了 GUI它使用哪个类名/API来集成在数据内没有给出。上面这段是一个通用的封装示例符合“常见做法”。而“项目里训练好的 best.pt 和 GUI 逻辑之间通常会隔一层这样的封装GUI 只把这个类实例化后调用”这句话也是合理的。5. 避坑手记YOLO11PyQt5 跑通全程最容易踩的 5 个坑这一章把我在这类项目里遇到过的五类高频故障完整记下来每一条按现象、原因、解决三个步骤说透。5.1 CUDA 环境不一致训练或推理时突然 device-side assert现象训练命令发起后刚开始跑几个 batch控制台直接抛出RuntimeError: CUDA error: device-side assert triggered训练进程崩掉。有时推理阶段也会遇到类似报错。原因常见的两种一是当前环境里实际安装的是 CPU 版 torch但代码指定了device0二是 torch 自带的 cuDNN 与机器显卡驱动不匹配。出现这个报错的时候先不要怀疑代码逻辑十有八九是环境层面的问题。解决先执行前面检查脚本确认torch.cuda.is_available()为 True再根据自己显卡支持的 CUDA 版本从官方渠道安装对应版本的 torch。需要注意的是conda install cudatoolkit之后再pip install torch这种混装方式很容易出问题我更建议直接pip安装官方预编译好的 torch 包避免 conda 的 CUDA 库和 pip 的 torch 版本之间产生缝隙。装完之后重新跑一次推理验证。5.2 标注类别索引从 1 起算模型学到错乱的类现象训练时日志频繁出现“class ... is out of range”之类的警告训练结束后 mAP 很低GUI 里把青椒识别成西红柿而且错误类别往往整体偏移一个序号。原因标注工具生成的类别编号从 1 开始YOLO 要求从 0 开始。转换时没有做索引减一操作所有标签第一列都比真实类别多 1。PIL 这类库读取标注图像时又不报错所以这种问题在训练早期极难发现。解决用脚本批量修正所有标签文件的第一列。# fix_label_index.py import os def fix_label_index(label_root, offset1): 把类别索引整体减 1前提是当前最小的类别 id 不小于 offset for root, _, files in os.walk(label_root): for f in files: if not f.endswith(.txt): continue path os.path.join(root, f) with open(path, r) as fp: lines fp.read().strip().splitlines() new_lines [] ok True for line in lines: parts line.split() if not parts: continue cls_id int(parts[0]) if cls_id offset: # 已经是从0开始时不要再改 ok False break parts[0] str(cls_id - offset) new_lines.append( .join(parts)) if ok and new_lines: with open(path, w) as fp: fp.write(\n.join(new_lines) \n) fix_label_index(datasets/vegetable/labels/train, offset1) fix_label_index(datasets/vegetable/labels/val, offset1)注意这个脚本只在确认标签首列从 1 开始时使用。如果标签里已经有 0 开头的行脚本会中止避免把正确标签改成负数。5.3 换机器后模型权重加载失败PytorchStreamReader 报错现象在 A 机器上训练好的 best.pt 拷贝到 B 机器加载时提示PytorchStreamReader failed reading zip archive或者直接显示文件损坏。原因最常见的不是代码问题而是文件传输过程中断导致权重文件不完整。yolo 权重本质是 zip 格式归档文件缺了末尾内容就会报出这个异常。有时是网盘下载被截断文件名看起来没变文件大小却明显偏小。解决重新下载或拷贝权重文件拷贝完成后第一时间看文件大小。如果 best.pt 只有几 KB 或几十 KB基本上就是损坏文件正常的模型权重至少应该是几十 MB 这个量级。另外一个细节是不要把模型文件当作普通文本文档打开编辑那会导致文件头损坏一样无法加载。5.4 界面点击“检测”后整窗卡死推理阻塞了主线程现象PyQt5 窗口正常打开选择图片后点击“检测”窗口立刻变白标题栏出现“未响应”几分钟后可能弹出结果也可能始终恢复不过来。原因推理任务直接写在按钮的点击信号槽里执行的是同步前向计算。模型推理期间主线程被占用Qt 的事件循环没法处理重绘、点击等消息界面表现就是卡死。图片尺寸越大、模型越大卡得越久。解决把推理移动到 QThread 子线程通过信号把结果传回主线程。前面InferenceThread那段代码就是干这个用的。主线程里收到result_ready信号后再更新界面上的 QLabel 或 QGraphicsView这就不会阻塞事件循环。设计时要注意在线程里不要直接操作任何界面控件所有 UI 更新都放到槽函数里做。5.5 界面图片偏蓝OpenCV 的 BGR 与 PyQt5 的 RGB 没转换现象推理结果明明是对的框的位置和类别都正常但界面里显示的图片整体偏蓝尤其西红柿这类暖色物体颜色完全不对偏色严重时像加了蓝色滤镜。原因OpenCV 读取图片默认通道顺序是 BGR而 PyQt5 的 QImage 默认按 RGB 解释。直接把 OpenCV 的 ndarray 塞进 QImage会导致红色和蓝色通道互换画面自然发蓝。解决在构造 QImage 前把 BGR 转回 RGB。# gui_image_utils.py import cv2 from PyQt5.QtGui import QImage def ndarray_to_qimage(bgr_img): # OpenCV 默认读入 BGR转成 RGB 再给 Qt 用 rgb_img cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) h, w, ch rgb_img.shape # 注意 bytesPerLine 必须写成 3*w否则 Qt 解析时会出现行错位 return QImage(rgb_img.data, w, h, 3 * w, QImage.Format_RGB888)这里3 * w是 bytesPerLine 参数等价于每个像素 3 字节乘以宽度。如果这个参数写 0 或写错QImage 解析出来的图片会出现行偏移画面呈斜切状态。这个细节我第一次写的时候也栽过后来固定写成3 * w就再没出过问题。6. 进阶改造ONNX 导出提速、摄像头实时流与上线前验证6.1 导出 ONNX丢掉 PyTorch 的动态图开销如果已经确认模型精度可用但推理速度还差一点可以把模型导出成 ONNX再通过 onnxruntime 执行推理。ONNX 是静态图省掉了 PyTorch 在每次推理时构建动态计算图的开销在 CPU 机器上速度提升尤其明显。# 把 best.pt 导出成 ONNX yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出之后用 onnxruntime 加载和推理代码如下import cv2 import numpy as np import onnxruntime as ort class OnnxDetector: def __init__(self, onnx_path): self.session ort.InferenceSession(onnx_path, providers[CPUExecutionProvider]) def predict_once(self, image_path): img cv2.imread(image_path) # 预处理resize 到 640归一化转 NCHW img_resized cv2.resize(img, (640, 640)) blob img_resized[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 outputs self.session.run(None, {self.session.get_inputs()[0].name: blob}) return outputs导出这个 ONNX 之后验证精度是否跟.pt 模型一致在几张图上分别用两种方式推理对比框坐标和置信度。坐标允许极小误差但置信度不应有大数量级差距这部分是必做步骤。如果还有提升空间可再接 INT8 量化但蔬菜目标偏小量化后精度损失需要专门评估不要直接上生产。6.2 改用摄像头实时检测把 GUI 从“选一张图片”升级成摄像头实时检测核心改动有两个一是摄像头图像采集放进独立线程避免画面卡顿二是降低推理频率不需要每一帧都推理而是每隔几帧抽一帧走检测避免 CPU 被打满。# camera_realtime.py import cv2 from PyQt5.QtCore import QThread, pyqtSignal class CameraThread(QThread): frame_ready pyqtSignal(object) def __init__(self, detector, interval5): super().__init__() self.detector detector self.interval interval # 每 interval 帧推理一次 self.running True def run(self): cap cv2.VideoCapture(0) frame_count 0 while self.running: ok, frame cap.read() if not ok: continue frame_count 1 if frame_count % self.interval 0: # 推理时用 BGR 原图界面显示前再转 RGB annotated, _ self.detector.predict_once_from_ndarray(frame) self.frame_ready.emit(annotated) cap.release()实时场景里会把conf阈值从图片静态检测的 0.3 往上调一调比如 0.4 或 0.5。原因是视频帧里存在大量模糊背景和运动模糊低置信度阈值会带来很多闪烁的误检框实际观感非常差。6.3 上线前的快速验证清单在把系统验收或提交之前我会强制自己过一遍这张清单验证项预期表现不同拍摄角度斜上方 30°、侧面 60° 都能稳定识别光照变化室外强光、室内白炽灯、傍晚弱光下不丢目标目标重叠两个西红柿紧挨时能分出两个独立框相似类别青椒和辣椒不被交叉误检背景干扰别把背景里的圆形物体识别成蔬菜推理耗时单张 640×640 图片 CPU 环境不超过 0.5 秒仅供参考这套清单其实是给整个工程的验收兜底。我第一次拿到类似的项目时直接在 GUI 里选了一张网上找的蔬菜照片测出来效果不错就想当然地认为它能在现场同样管用。后来拿到真实拍摄的视频流里一试才发现光照一变化误检频繁。从那以后我每次都会强制走一遍上面的清单用真实环境里拍的照片再验证一轮绝不在 GUI 里测一两张图就草草收场。希望这套验证习惯也能帮到你少走这一段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →