尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

OpenCV+Qt+YOLO视觉检测系统实战:从模型部署到界面开发完整教程

OpenCV+Qt+YOLO视觉检测系统实战:从模型部署到界面开发完整教程 简介在计算机视觉工程化落地中目标检测是最常见的需求之一而如何将深度学习模型高效集成到可交互的桌面应用中则是开发者普遍面临的技术难点。OpenCV作为图像处理与视频分析的基础库提供了从采集、预处理到DNN推理的全链路支持Qt则凭借成熟的信号槽机制和跨平台界面能力成为实时可视化监控与参数交互的理想选择。YOLO系列模型以精度与速度的均衡性在工业检测、安防监控、智能交通等场景中广泛应用。本文从零开始讲解如何基于OpenCV的DNN模块加载YOLO ONNX模型结合Qt构建多线程实时检测系统涵盖环境配置、CMake构建、图像增强、NMS后处理、界面渲染等核心环节并针对常见编译错误与性能调优给出工程级解决方案。无论是入门深度学习部署还是快速搭建可复用的检测框架这套实践路径都具有直接参考价值。 一直在做视觉检测相关的项目手头攒了一套基于 OpenCV Qt YOLO 的检测系统源码整套代码已经整理好开箱即用。这个项目的作用很直接把摄像头、视频或图片输入进来经过 YOLO 模型推理把检测结果实时显示在 Qt 界面上同时支持设置检测参数、切换输入源、导出检测结果。如果你是刚接触 OpenCV 和深度学习部署的新手或者正在找一套能直接在本地跑起来的检测框架这份源码正好可以当作业界标准模板来用。这套系统不是一个“实验性 Demo”而是按实际项目结构组织的采集、预处理、推理、后处理、界面交互、线程管理全都分开换模型、换数据源、加功能都比较顺手。下面我把整套系统的设计思路、关键代码、踩过的坑全部讲一遍代码在文末对应的路径里都能找到照着编译就能跑。1. 项目整体思路与架构设计1.1 为什么选择 OpenCV Qt YOLO 这套组合先解释一下选型问题。视觉检测系统的技术栈看起来可选项很多实际落在工程上组合其实非常固定OpenCV Qt YOLO 是其中最稳的一套。OpenCV 负责的是图像采集和预处理。摄像头读取、视频解码、颜色空间转换、几何变换、直方图增强、边缘检测这些操作OpenCV 一路包到底生态最成熟踩坑资料也最多。Qt 负责的是界面和交互。实时显示画面、按钮控制、参数调节、状态提示这些都是 Qt 的强项而且它自带的信号槽机制天然适合做多线程 UI 刷新。YOLO 负责的是目标检测推理。这个不需要多说单阶段检测器里 YOLO 是目前工程落地最广泛的精度和速度的平衡做得最好而且模型导出成 ONNX 之后可以直接用 OpenCV 的 DNN 模块加载不需要额外引入深度学习框架。这套组合还有一个隐藏优势全链路 C没有 Python 解释器开销也没有包管理器的依赖泥潭。你要做的就是 OpenCV 和 Qt 两个库加上一个 ONNX 模型文件编译完就能跑。相比之下Python 方案虽然写起来快但打包分发、嵌入式部署、帧率控制都有明显瓶颈。1.2 系统模块划分与数据流整个系统按功能拆成五个模块采集模块、预处理模块、推理模块、界面模块、结果输出模块。模块之间用明确的接口衔接尽量做到“替换一个模块不影响其他模块”。数据流是这样的图像采集摄像头/视频/图片 → 预处理缩放/增强/边缘检测 → YOLO ONNX 推理 → 后处理置信度过滤/NMS → Qt 界面显示 ↓ 可选结果导出图片/文本/UDP 转发采集模块封装了 OpenCV 的 VideoCapture统一对外暴露getFrame()接口上层不关心数据到底来自摄像头还是视频文件。预处理模块做的事情包括 resize 到网络输入尺寸、BGR 转 RGB、归一化以及一些可选的图像增强比如直方图均衡、Canny 边缘检测。推理模块加载 ONNX 模型执行前向计算返回原始输出张量。后处理模块解析输出过滤低置信度目标执行 NMS 去重最终生成带类别和坐标的检测结果列表。界面模块负责把每一帧画面画出来把检测框叠加到画面上并在侧边栏显示检测到的目标列表。线程模型是这个系统的关键。我用了三个线程采集线程、推理线程、UI 线程。采集线程只管取帧推理线程做模型计算UI 线程只负责绘制三者之间通过 Qt 信号槽通信。这样能避免推理耗时导致界面卡死的问题。1.3 这套源码解决的三个核心痛点为什么我把这份源码打包出来因为在实际开发中新手最容易卡在这三个地方。第一是环境搭建。OpenCV 和 Qt 的版本组合、编译器位数、CMake 配置稍不注意就是一堆编译错误。我在项目里提供了完整的 CMakeLists.txt并且标注了验证过的版本组合照着配基本不会出问题。第二是模型封装。YOLO 的输出张量不是一个可以直接用的格式它需要解析坐标、类别、置信度再送进 NMS 去重。很多初学者下载模型文件之后不知道 forward 之后那一堆数字该怎么处理。源码里把这段逻辑完整封装好了你只需要传入网络输出张量直接就能拿到目标框。第三是界面和推理的并发问题。如果直接在 UI 线程里做模型推理画面会卡成 PPT。源码用 QThread 信号槽做了标准的多线程刷新方案这套代码可以直接迁移到其他项目里。2. 环境准备与源码结构说明2.1 开发环境与依赖清单先说环境版本这是最容易被坑的地方。我验证过的组合是Windows 10/11 Visual Studio 2019/2022 Qt 5.15.2 OpenCV 4.8.0。Linux 下用 GCC Qt 5.15.2 同样版本 OpenCV 也能编过需要微调的只有 CMake 里的路径。依赖其实就三个OpenCV 4.8.0 或更高版本需要包含 core、imgproc、imgcodecs、videoio、dnn、highgui 这些模块。官方预编译包默认都带。Qt 5.15.2需要 Widgets 模块。如果你用 Qt 6代码层面几乎不用改CMake 里把 find_package 的版本号改一下就行。模型文件源码里默认用的是 YOLOv8n 导出的 ONNX 模型也可以换成其他 YOLO 系列模型后处理逻辑已经做了兼容。提示OpenCV 吃内存和磁盘都比较猛建议下载安装版之后把opencv/build/x64/vc16/bin加进系统 PATH或者把需要的 DLL 复制到可执行文件同目录避免运行时找不到库的报错。2.2 源码目录结构与关键文件项目打包后的目录结构如下detector/ ├── CMakeLists.txt ├── README.md ├── models/ │ ├── yolov8n.onnx │ └── coco.names ├── scripts/ │ ├── download_models.sh │ └── download_models.bat └── src/ ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── mainwindow.ui ├── videocapture.h ├── videocapture.cpp ├── yolodetector.h ├── yolodetector.cpp ├── imagetools.h ├── imagetools.cpp ├── resultpanel.h └── resultpanel.cpp每个文件职责明确。main.cpp是程序入口负责初始化 QApplication 和主窗口。mainwindow是 Qt 主界面包含画布、控制按钮、参数面板。videocapture封装了所有视频源读取逻辑。yolodetector封装模型加载、推理和后处理。imagetools是图像预处理和增强工具集合包括掩膜直方图均衡和边缘检测。resultpanel负责检测结果列表展示和结果导出。2.3 “开箱即用”的构建配置解析构建系统我选了 CMake没用 qmake。原因很简单CMake 对 OpenCV 的原生支持更好find_package(OpenCV REQUIRED)一次搞定qmake 还得手写一堆 include 和 lib 路径。核心的 CMakeLists.txt 关键部分如下cmake_minimum_required(VERSION 3.16) project(SimpleDetector) set(CMAKE_CXX_STANDARD 17) find_package(OpenCV REQUIRED) find_package(Qt5 REQUIRED COMPONENTS Widgets) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) add_executable(${PROJECT_NAME} src/main.cpp src/mainwindow.cpp src/mainwindow.h src/videocapture.cpp src/videocapture.h src/yolodetector.cpp src/yolodetector.h src/imagetools.cpp src/imagetools.h src/resultpanel.cpp src/resultpanel.h ) target_include_directories(${PROJECT_NAME} PRIVATE src) target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS} Qt5::Widgets )两个点要注意CMAKE_AUTOMOC ON是 Qt 编译的必须项不然带Q_OBJECT宏的类没法生成元对象代码信号槽直接编译不过。src/mainwindow.ui也要在 AUTOUIC 开启的情况下才会被自动处理成可编译的头文件。如果你把.ui文件路径放进add_executable里CMake 会自己识别并调用 uic 处理。3. 核心模块实现与关键代码解析3.1 图像采集与预处理从 equalizeHist 掩膜到边缘检测图像采集代码很简单但有几个细节需要处理。VideoCapture打开摄像头时正确的做法是设置分辨率和帧率比如cap.set(CAP_PROP_FRAME_WIDTH, 1280)然后再cap.read()。默认摄像头参数往往很保守不设置的话可能只有 640x480画质和检测效果都会受影响。打开视频文件也是同一个接口直接传路径进去就行。唯一要注意的是视频文件解码依赖 OpenCV 编译时的 FFmpeg 支持如果你的 OpenCV 是精简版可能打不开 MP4这个问题我在第 5 章专门说。图像预处理模块里我额外实现了两个工具函数。一个是带掩膜的直方图均衡另一个是 Canny 边缘检测。先说一下直方图均衡。很多人搜资料会看到equalizeHist带掩膜的用法但注意OpenCV 原生的cv::equalizeHist是不支持 mask 参数的原型只有两个参数void cv::equalizeHist(InputArray src, OutputArray dst);它会把整张灰度图的直方图拉平这在光照不均匀的场景下会出问题。比如画面左边暗、右边亮全图均衡会让暗区起噪点。要实现“只对感兴趣区域做增强、其他区域保持原样”的效果需要自己实现带掩膜的版本。原理也不复杂只统计 mask 区域内的像素直方图然后计算累计分布函数用这个映射关系去重映射全图mask 外的像素可以保留原值也可以做衰减。核心代码如下cv::Mat equalizeHistWithMask(const cv::Mat gray, const cv::Mat mask) { std::vectorint hist(256, 0); for (int y 0; y gray.rows; y) { const uchar* p gray.ptruchar(y); const uchar* m mask.ptruchar(y); for (int x 0; x gray.cols; x) { if (m[x]) hist[p[x]]; } } // 计算累计分布忽略 mask 外像素 std::vectorfloat lut(256, 0.f); int total cv::countNonZero(mask); int sum 0; for (int i 0; i 256; i) { sum hist[i]; lut[i] (total 0) ? 255.0f * sum / total : 0.f; } cv::Mat result; cv::LUT(gray, lut, result); return result; }边缘检测就简单多了cv::Canny直接调即可。不过阈值要按场景调固定阈值在光照变化时不够用。源码里把 Canny 的两个阈值接在 Qt 界面的 SpinBox 上这样用户可以直接调不用重新编译。3.2 YOLO 模型推理与后处理逻辑推理模块是整个系统的核心。模型加载用的是 OpenCV DNN 模块不需要安装任何深度学习框架代码很简单m_net cv::dnn::readNetFromONNX(modelPath); m_net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); m_net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);这里我特别用了DNN_BACKEND_OPENCV而不是默认的DNN_BACKEND_DEFAULT。默认后端在某些 OpenCV 版本里会自动选择 OpenCL 加速但实际部署中OpenCL 在部分显卡上会出现推理结果错误或驱动崩溃尤其是核显环境。用 CPU 推理虽然慢一点但稳定YOLOv8n 在普通桌面 CPU 上跑起来也有 30ms 左右一帧够用。推理之前的预处理是重点。YOLO 输入要求是 RGB 图、归一化到 0~1、分辨率固定为模型训练尺寸比如 640x640。OpenCV 的blobFromImage一步就能完成cv::Mat blob cv::dnn::blobFromImage(img, 1.0 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false);后处理逻辑才是真正容易写错的地方。以 YOLOv8 为例输出张量形状是1x84x8400其中 84 4 个坐标 80 个 COCO 类别概率8400 是三个尺度特征图锚点数量总和。解析逻辑如下std::vectorDetection decodeOutput(const cv::Mat output, float confThresh) { std::vectorDetection detections; // output shape: [1, 84, 8400]先转成 [8400, 84] 方便遍历 cv::Mat data output.reshape(1, output.size[1]); // 84 x 8400 cv::Mat transposed; cv::transpose(data, transposed); // 8400 x 84 for (int i 0; i transposed.rows; i) { const float* row transposed.ptrfloat(i); float objConf row[4]; // YOLOv8 没有独立的 objectness直接取 80 类概率的最大值 int classId -1; float maxProb 0.f; for (int c 5; c 84; c) { if (row[c] maxProb) { maxProb row[c]; classId c - 4; } } float score maxProb; // YOLOv8 的置信度就是类别概率 if (score confThresh) continue; float cx row[0], cy row[1], w row[2], h row[3]; Detection det; det.x cx - w / 2; det.y cy - h / 2; det.width w; det.height h; det.classId classId; det.score score; detections.push_back(det); } return detections; }注意 YOLOv5 和 YOLOv8 的差异。YOLOv5 的输出维度里有 objectness 这一项所以取置信度时是objConf * classProbYOLOv8 直接就是类别概率。很多网上流传的旧解析代码拿过来解析 YOLOv8 会出问题源码里做了兼容判断。NMS 直接用cv::dnn::NMSBoxes处理std::vectorint indices; cv::dnn::NMSBoxes(boxes, scores, confThresh, nmsThresh, indices);需要说明的是坐标要从 640x640 的网络输入坐标换算回原图坐标。源码里在推理前记录了缩放比例解析完成后统一映射回去这样画框时才能对准画面。3.3 Qt 界面设计与交互线程模型界面用 Qt Designer 设计了mainwindow.ui布局很直接。左侧是画面显示区用 QLabel 承载 QImage右侧是控制面板包含输入源选择、开始/停止按钮、置信度滑块、NMS 阈值滑块、检测结果表格底部是状态栏显示当前帧率和检测到的目标数。界面设计本身不难最核心的是多线程刷新模型。我用了QThread派生一个采集线程类VideoCapture在子线程里循环读帧每读一帧通过信号发给主线程。主线程收到帧以后在槽函数里做推理并更新画面。这样只要推理耗时不超过 UI 刷新间隔界面就不会卡。关键代码在信号连接上// 采集线程 connect(captureWorker, CaptureWorker::frameReady, this, MainWindow::onFrameReady); // 界面线程 connect(this, MainWindow::updateFrame, this, MainWindow::renderFrame, Qt::QueuedConnection);Qt 的信号槽默认是 AutoConnection跨线程会自动变成 QueuedConnection所以不需要显式指定。但如果推理耗时太长信号会积压画面延迟变大。源码里做了一个简单的预处理当积压信号数量超过 3 帧时丢弃当前帧。这是因为检测系统对实时性要求高牺牲一点连续帧换取低延迟更划算。如果检测结果要跨线程传递自定义结构体需要在信号槽中使用前注册否则 Qt 会报Cannot queue arguments of type Detection。我在 main.cpp 里加了qRegisterMetaTypeDetection();这是新手很容易漏掉的一步。3.4 输入源切换与结果导出输入源支持图片文件、视频文件、USB 摄像头、RTSP 网络流四种。界面上的切换按钮本质上是切换VideoCapture的打开方式。RTSP 流的地址如果带账号密码需要 URL 编码比如密码里有符号时必须转成%40否则解析会出错。结果导出我做了两个出口。一个是保存标注画面直接把画好框的 QImage 转成 cv::Mat然后cv::imwrite保存这个最简单。另一个是导出检测结果到文本文件每行格式是class_id, score, x, y, width, height这个格式是为了方便后续接入其他系统。我可以直接把这个文本文件交给自动化测试脚本去做统计分析。源码里还预留了两个转发接口用注释标出来了一个是 UDP socket 发送检测结果另一个是 QSerialPort 串口发送。UDP 适合把结果转发到局域网其他设备串口适合发给单片机或 PLC 做执行机构触发。这两个接口需要 Qt 的 Network 和 SerialPort 模块CMake 里默认没有链接需要的时候打开注释即可。4. 实操流程从编译到跑通检测4.1 编译构建步骤我在项目根目录放了 CMakeLists.txt整个编译流程很简单。以 Windows 为例打开 x64 Native Tools Command Prompt for VS2019执行mkdir build cd build cmake .. -DCMAKE_PREFIX_PATHC:/Qt/5.15.2/msvc2019_64;C:/opencv/build cmake --build . --config ReleaseCMAKE_PREFIX_PATH指定 Qt 和 OpenCV 的安装路径如果两个库在不同盘符用分号分隔。编译成功后build/Release/SimpleDetector.exe就是可执行文件。运行前把 OpenCV 的opencv_world480.dll和 Qt 的Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll复制到 exe 同目录。如果不想手动复制可以用 windeployqt 自动处理 Qt 的依赖C:/Qt/5.15.2/msvc2019_64/bin/windeployqt.exe Release/SimpleDetector.exeOpenCV 的 DLL 还是得手动复制或者直接把C:\opencv\build\x64\vc16\bin加进系统 PATH。源码的 scripts 目录里放了一个run.bat会自动把这些路径临时加进 PATH 再启动程序解决 DLL 找不到的问题。4.2 模型文件准备与加载models/yolov8n.onnx需要单独下载GitHub 仓库因为文件大小限制没有直接放进去。下载方式有两种一是直接用 scripts 目录里的脚本Windows 下双击download_models.bat它会自动下载模型和 COCO 类别文件二是手动从 Ultralytics 仓库下载然后转换成 ONNX 格式。如果你想用自己训练好的模型转换命令很简单。在安装了 ultralytics 的 Python 环境里执行from ultralytics import YOLO model YOLO(yolov8n.pt) model.export(formatonnx, opset12, simplifyTrue)导出后的 ONNX 文件放在 models 目录下替换原来的文件即可。要注意的是如果你自定义了检测类别coco.names要同步修改一行一个类别名顺序和训练时一致。如果类别数变了比如从 80 类改成 5 类YOLO 输出张量的形状会从1x84x8400变成1x(54)x8400后处理代码里 84 这个数字要改成 9不要直接用写死的数字最好从 ONNX 输入输出维度动态读取。源码里做了一个简单的判断就是为这种情况准备的。4.3 运行验证与效果评估程序跑起来以后第一步先用图片测试选一张包含常见物体的图比如街道、办公室、宠物照片。点击“选择图片”按钮加载图片后自动执行检测右侧列表里会出现检测到的目标。如果一切正常画面上会叠加绿色的检测框和类别标签。第二步切换摄像头。摄像头打开后点击“开始检测”观察 FPS 和延迟。CPU 跑 YOLOv8n帧率一般在 25 到 40 之间这取决于 CPU 性能和输入分辨率。如果帧率太低可以调低检测帧率我加了一个“每 N 帧检测一次”的选项设置成 2画面刷新保持全速检测逻辑隔一帧执行一次流畅度会明显提升。第三步验证检测参数。把置信度阈值从 0.5 往下调到 0.25你会发现检测框变多了但误检也会变多。NMS 阈值调大重叠框会被压得更狠调小同一个物体会出现多个框。这两个参数在界面滑块上实时生效方便直观感受效果。5. 常见问题与排查技巧实录5.1 OpenCV 报错function/feature is not implemented这个报错出现的概率很高报错信息一般是cv::Exception: OpenCV(4.8.0) error: The function/feature is not implemented常见触发场景有两个。第一个是读取视频文件比如打开 MP4 时提示not implemented原因是编译 OpenCV 时没有启用 FFmpeg 支持。官方 Windows 预编译包是带 FFmpeg 的但某些第三方编译的精简版不带。解决办法是换成官方包或者自己重新编译 OpenCV 时把WITH_FFMPEG打开。第二个场景是调用某些特定模块的接口比如cv::rgbd::registerDepthFrame、cv::linemod这类扩展模块。这些接口不在 OpenCV 的主仓库里第一次跑就直接报 not implemented。我早期做结构光匹配时用过 linemod当时也卡在这里。解决方案是确认具体是哪个 API 报错如果是 contrib 模块需要重新编译带 contrib 的 OpenCV 版本如果是 DNN 模块加载某种模型格式也要检查 OpenCV 版本的编译选项是否包含该格式支持。遇到这个报错先别慌它通常不是代码逻辑问题而是 OpenCV 二进制包编译选项不满足需求最简单的办法是换一个完整的预编译包。5.2 Qt 编译与界面设计问题Qt 编译最经典的坑是 Qt 版本和编译器不匹配。比如 Qt 5.12 版本如果安装的是 msvc2015 编译套件但你用 VS2019 编译会报一堆链接错误。解决办法是安装对应编译器的套件版本或者在 CMake 里指定正确的CMAKE_PREFIX_PATH。Qt 官方安装器里可以多选几个套件建议把 msvc2019_64 和 minGW_64 都装上日常开发够用。VSCode 用户配置 Qt Designer 也有个常见误区。很多人装完 Qt 后在 VSCode 的扩展设置里找不到 Qt 路径其实是扩展要的是包含bin/qmake.exe的那个目录。设置好以后UI 文件可以直接在 VSCode 里双击打开 Designer修改保存后CMake 会在编译时自动调用 uic 处理不用手动生成头文件。之前有个朋友一直手动执行uic mainwindow.ui -o ui_mainwindow.h还问我为什么改了 UI 界面没生效其实就是忘了重新执行 uic。界面设计方面还有一个建议不要在代码里硬编码布局尽量用.ui文件加布局管理器调试时可以随时改。我从 Qt Designer 里学的经验是把左侧显示区用setMinimumSize(640, 480)固定窗口缩放时画面区域自动拉伸右侧面板宽度保持固定这样用户体验最自然。5.3 YOLO 推理速度与精度调优推理速度不达标时按优先级做三件事。第一步看输入分辨率。YOLOv8n 用 640x640 推理如果你把输入改成 320x320速度能提升大约 3 倍但小目标检测精度会明显下降。目标占画面比例大的场景可以放心降分辨率。第二步看 OpenCV 后端和线程。代码里调用m_net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV)之后还可以设置 CPU 线程数m_net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU); cv::setNumThreads(4);cv::setNumThreads默认会占用所有逻辑核心但推理时如果同时跑其他程序反而会因为线程切换导致速度下降。我实测过8 核 CPU 上设成 4 线程往往比全线程更快。第三步如果硬件允许可以用 CUDA。OpenCV DNN 支持DNN_BACKEND_CUDA和DNN_TARGET_CUDA前提是你编译 OpenCV 时开了 CUDA或者直接下载带 CUDA 的预编译包。我在一台 1060 显卡的机器上测过YOLOv8n 推理时间能从 30ms 降到 3ms提升非常明显。精度方面容易漏检的时候先别急着换大模型试试把置信度阈值从 0.5 降到 0.3配合 NMS 阈值 0.45大部分场景的效果都能接受。如果还是有漏检检查一下输入图像是否做了归一化和 RGB 转换。我见过不少人在blobFromImage里漏掉swapRBtrue参数导致画面整体偏色检测效果直接崩盘。6. 一些实际使用中的体会项目做到现在我最大的体会是检测系统最难的部分往往不是算法本身而是把算法封装成一套能稳定运行的工程。OPENCV Qt YOLO 这套组合每一个单独拎出来都有大量资料但把它们正确组合起来、处理好看似简单的线程同步、模型输出解析、环境兼容问题才是真正耗时间的部分。这套源码我花了不少心思整理就是想把这段经验直接交给你让你不用再踩一遍我踩过的坑。如果你要在这个基础上做自己的业务系统我建议从两个方向切入。第一是替换成自己训练的模型只需要重新导出 ONNX 并更新类别文件整个框架不用动。第二是扩展输入源和输出源比如接入多路摄像头、把检测结果转发到数据库或消息队列源码里预留的接口就是这么设计的。后面我有时间会继续补充基于这个框架的摄像头标定和测距功能如果你感兴趣也可以先研究一下cv::calibrateCamera和solvePnP思路是通的。本文还有配套的精品资源点击获取
返回列表