
简介这是一份围绕 PyQt5 与 YOLOv5 构建的多目标检测图形界面实战项目面向刚接触 PyQt5 界面开发、YOLO 算法或希望用完整工程练手的初学者。项目以 PyTorch 为深度学习框架将多目标检测后端与图形界面结合重点体现界面设计与业务逻辑分离的组织方式并覆盖 PyQt5 常用控件的使用场景可作为课程设计、入门演示与二次开发的基础模板。压缩包共 112 个文件约 83.46MB主要由源码、模型权重、配置文件、界面设计文件、示例图片与演示视频以及容器、脚本和说明类辅助文件构成便于直接运行、阅读与改造。已有 8886 人学习下载读者能从中了解完整图形界面工程结构、模型加载与推理调用方式、界面与后端解耦思路和常见控件实践配合 PyQt5 开发书籍对照学习更易上手。1. 从一台没外网的机器说起这套 pyqt5 yolov5 python 桌面检测系统要解决什么实验室那台机器连不上外网评审只给十分钟看演示这时候一个双击就能打开、把视频拖进去就出框的桌面程序比任何网页 demo 都管用。这套 pyqt5 yolov5 python 的组合要解决的就是这件事把 PyTorch 训练出来的检测权重装进一个带 GUI 的本地应用支持单图、文件夹批量、视频文件和摄像头四种输入阈值能现场调、模型能现场换、结果能导出成表格。它适合正在做课程设计或毕设、需要交付一套跑得起来、讲得清楚、当场能演示的完整系统的同学也适合要做一个离线可用检测工具原型的从业者——不联网、不依赖服务端一个文件夹拷走就能跑。2. 需求拆解与技术选型四种输入源与一条不能堵的主线程需求这一步最容易被草草带过最后做出来一个能检测但不好用的东西点了开始按钮界面灰掉想停停不下来换个阈值要重启程序。把验收口径提前写死后面写代码时才知道边界在哪儿。2.1 把能检测拆成六条可验收的需求做一个目标检测 GUI这句话没法验收得拆。我的习惯是从输入源、交互、输出三条线切每一条都配上可测量的口径最后交付时拿着这张表逐条打勾。编号需求描述验收口径优先级R1单张图片检测拖入 jpg/png3 秒内出框并刷新结果表必须R2文件夹批量检测200 张图不卡界面有进度条可中途停止必须R3视频文件逐帧检测输出带框视频帧数与源文件一致必须R4摄像头实时检测预览延迟 ≤ 300 ms随时可停必须R5参数现场可调置信度、IoU、类别勾选即时生效不重启应该R6结果可导出CSV/JSON 字段完整Excel 打开不乱码应该这张表里真正决定架构的是 R2 和 R4。批量检测是长耗时任务摄像头是持续流式任务两者都必须在后台跑只要有一帧的推理留在主线程界面就会进入未响应状态——Windows 上转圈三秒答辩现场基本就凉了。R5 看着简单实际要求推理对象在运行期可以被改写参数所以 Detector 不能是每次重新加载权重的函数式写法得是一个常驻对象。2.2 为什么桌面端选 PyQt5 而不是套一层网页很多人第一反应是 Flask 前端页面看起来时髦落到这个场景里全是麻烦摄像头要额外做 WebRTC 或 MJPEG 推流本地文件要上传再回传打包成双击即用更是无从下手。PyQt5 的优势在离线本地场景里非常实在——文件对话框、拖拽、摄像头通过 OpenCV 的 VideoCapture 拿本地设备索引都是一等公民部署就是把一个 exe 或一个文件夹拷过去。跨线程通信也是选它的核心理由。Qt 的信号槽在跨线程时默认走队列连接工作线程 emit 一个信号槽函数在主线程执行天然规避了子线程直接改控件这个最常见的崩溃源。用 tkinter 做同样的事就得自己维护一个 queue 再拿 after() 轮询写起来更脏。坑在于 Designer 生成的.ui文件和手写逻辑的关系。我的做法是.ui只描述布局业务逻辑全部写在ui/main_window.py里用uic.loadUi或者pyuic5编译都不影响一旦在 Designer 里挂槽函数后面改布局就会出现信号连了两次、槽执行两遍的玄学问题。2.3 YOLOv5 权重与 PyTorch 推理后端怎么定YOLOv5 的权重是.pt文件本质是序列化的模型对象PyTorch 直接torch.load就能还原。工程里我一般用torch.hub.load的custom分支把仓库放在本地目录、sourcelocal这样首次运行不会去联网拉代码——离线机器上这一条能省掉一整天的排查。import torch class Detector: 对 YOLOv5 推理的薄封装UI 层只认这几个方法 def __init__(self, weight_path, devicecpu, halfFalse): # sourcelocal 指向本地 yolov5 仓库避免首次运行联网下载 self.model torch.hub.load( ./yolov5, custom, pathweight_path, sourcelocal, verboseFalse ) self.model.conf 0.25 # 置信度阈值 self.model.iou 0.45 # NMS 的 IoU 阈值 self.model.classes None # None 表示全部类别参与 self.device device self.half half and device ! cpu def set_threshold(self, confNone, iouNone, classesNone): # 运行期改参数不必重新加载权重 if conf is not None: self.model.conf float(conf) if iou is not None: self.model.iou float(iou) if classes is not None: self.model.classes list(classes) def infer(self, bgr_frame, size640): # OpenCV 读出来是 BGR ndarrayYOLOv5 内部转换直接传即可 results self.model(bgr_frame, sizesize) df results.pandas().xyxy[0] # xmin ymin xmax ymax confidence class name return df.to_dict(records)infer返回的是原始像素坐标不做归一化——这一点很关键。UI 层拿到坐标直接画在原图上导出 CSV 时也是原图坐标系中间任何一次缩放归一化都会让检测框画歪一点这种问题变得极难定位。size参数是推理分辨率默认 640改大到 1280 精度略升但帧率掉一半答辩演示时用 640 就够。设备选择上有独显就devicecuda并开 FP16纯 CPU 也别慌YOLOv5s 在普通笔记本上跑单张图大约几百毫秒做图片和文件夹批量完全够看只是摄像头实时会卡到 35 FPS这一点要在答辩时主动说明别等人问。2.4 依赖版本与目录约定版本冲突是这个组合里排第一的血泪经验。PyTorch、torchvision、OpenCV、PyQt5 四者互相牵制尤其是 torch 2.x 之后部分接口变动配老版 YOLOv5 仓库容易出weights_only相关的加载报错。稳妥的做法是锁版本并写进 requirements交付时连同一个能跑的虚拟环境一起打包。detect_gui/ ├── main.py # 入口只做 QApplication 与主窗口装配 ├── ui/ │ ├── main_window.py # 主界面输入区、预览区、结果表、参数面板 │ ├── widgets.py # 可复用控件拖拽区、可缩放图像查看器 │ └── main_window.ui # Designer 布局文件 ├── core/ │ ├── detector.py # YOLOv5 推理封装 │ ├── worker.py # QThread 工作线程 │ ├── video.py # 视频与摄像头读取、帧率控制 │ └── exporter.py # CSV / JSON / 截图导出 ├── config/ │ └── app.yaml # 默认阈值、设备、最近使用路径 ├── weights/ │ └── best.pt ├── fonts/ │ └── simhei.ttf # 中文标签渲染用必须随包发布 └── requirements.txt目录约定看着琐碎但它决定了后面能不能顺利打包。凡是被代码用相对路径引用的东西——权重、字体、yolov5 仓库、配置文件——都必须落在项目根目录的子目录里并且在代码中统一走一个resource_path()去解析打包后才不会出现开发机能跑、拷到别人机器上找不到 best.pt的经典事故。3. 总体架构设计UI 层、推理层、数据层的职责边界3.1 三层结构与模块划分整套系统我按三层切UI 层只负责画和收集用户意图推理层只负责给一帧返回一堆框数据层负责结果的组织、缓存和落盘。三层之间不允许反向依赖UI 不 import cv2推理层不 import PyQt5——这条纪律听起来教条但它能保证你换界面时不用动推理代码换模型时不用动界面。UI 层由主窗口和几个自定义控件组成。主窗口做四件事装配布局、连接信号、维护当前任务句柄、管理最近使用路径。可缩放图像查看器单独抽出来因为图片按比例适应窗口 支持滚轮缩放 拖拽平移这套逻辑写一次就够了图片和视频预览都要用。推理层核心是Detector和DetectWorker。Detector持有模型是常驻的、无状态的DetectWorker是一次任务的载体继承 QThread负责按输入源类型驱动循环。把模型和任务分开的好处是切换权重时只重建 Detector而任务对象每次新建不会残留上一次的状态。数据层比较轻一个DetectionRecord的字典结构加一个ResultStore负责按帧号聚合、按类别统计、按需导出。批量检测时结果可能有几万条不能全塞进 QTableWidget我的做法是表格只显示前若干条或按类别汇总完整数据留在内存的列表里导出时才遍历。这样界面刷新的开销和结果总量脱钩跑 500 张图也不会越跑越卡。3.2 信号槽协议UI 与工作线程之间只留一条通道多线程 GUI 出事九成是因为子线程碰了控件。规矩定死工作线程只 emit 信号绝不调用任何 UI 对象的方法UI 只通过worker.stop()这种设置标志位的方式影响线程不直接 terminate。线程类上挂四个信号frame_ready(object, list)每处理完一帧发一次携带原图和检测结果progress(int)发进度百分比finished_ok(dict)正常结束时发汇总统计failed(str)出异常时发错误信息。四个信号对应四种 UI 反应边界清晰。这里有个容易忽略的细节frame_ready的参数类型要写成object。PyQt 的信号签名对 numpy 数组和 Python 列表支持不好直接写pyqtSignal(np.ndarray, list)在某些版本上会报类型不匹配用object兜住最稳。另一个细节是信号频率。摄像头 30 FPS 意味着每秒 30 次跨线程投递每次携带一张 1080p 的图队列一旦堆积界面就会越跑越延迟。解决办法是 UI 端做节流槽函数里记录上次刷新时间间隔小于 33 ms 就直接丢弃这次更新只保留最新帧。人眼看不出来内存压力立刻下来。3.3 结果数据模型与字段定义结果结构统一成一个扁平字典不含嵌套这样 CSV、JSON、表格三处导出可以共用同一份定义不用写三套映射。字段类型说明frame_idint帧序号单张图片固定为 0namestr类别显示名可映射为中文confidencefloat0~1 的置信度xmin/ymin/xmax/ymaxfloat原图像素坐标未归一化sourcestr来源标识文件名或 camera坐标统一用像素值而不是比例是为了画框时零换算。代价是同一批结果不能直接跨分辨率复用但在单机检测工具里这不是问题——每一帧的框都跟着那一帧走。类别名做一层映射表放在配置里模型输出person、界面显示人员答辩时中文界面看着更完整映射表也让改个显示名不用改模型成为可能。3.4 权重、配置与资源的统一入口配置文件用 YAML只放会变的东西默认置信度、默认 IoU、设备偏好、最近打开的目录、界面字体大小。权重路径不写死在配置里而是启动时扫描weights/目录把找到的.pt全部列进下拉框。资源路径统一走一个函数解析开发环境和打包环境共用一套逻辑import sys, os def resource_path(relative): 打包后 PyInstaller 会把资源解到临时目录base 路径要动态取 base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, relative)sys._MEIPASS是打包工具解压资源时设置的临时目录非打包环境不存在所以用getattr兜底。所有open()、ImageFont.truetype()、torch.hub.load()的路径都必须过这个函数一个地方漏了打包版就在那一处崩。4. 关键模块实现从读帧、推理到画框导出的完整链路4.1 推理线程把阻塞操作全部赶出主线程工作线程是整个系统的心脏写法直接决定界面卡不卡。核心原则是 run() 里只有一个循环循环里只有读帧、推理、emit 三件事所有异常就地捕获并转成failed信号绝不让异常穿透线程边界——穿透了就是整个进程静默退出连报错都看不到。from PyQt5.QtCore import QThread, pyqtSignal import traceback class DetectWorker(QThread): frame_ready pyqtSignal(object, list) # 原图帧, 检测结果列表 progress pyqtSignal(int) # 0-100 finished_ok pyqtSignal(dict) # 汇总统计 failed pyqtSignal(str) # 错误详情 def __init__(self, detector, source, kind, parentNone): super().__init__(parent) self.detector detector self.source source # 文件路径 / 目录 / 摄像头索引 self.kind kind # image / folder / video / camera self._stop False def stop(self): # 只置标志位不 terminate避免资源半释放 self._stop True def run(self): stats {total: 0, frames: 0} try: for frame_id, frame in self._iter_frames(): if self._stop: break dets self.detector.infer(frame) stats[total] len(dets) stats[frames] 1 self.frame_ready.emit(frame, dets) self.progress.emit(self._percent(stats[frames])) self.finished_ok.emit(stats) except Exception: self.failed.emit(traceback.format_exc())stop()只置标志位是有意为之。QThread 的terminate()会在任意指令处杀掉线程此时 OpenCV 的 VideoCapture 没释放、文件句柄没关、CUDA 上下文没清第二次点开始就可能打不开摄像头。用标志位让循环自然退出最坏情况多跑一帧代价可以接受。_iter_frames()按 kind 分派图片返回单元素迭代文件夹遍历目录视频和摄像头走统一的读取循环。这样 run() 里只有一份逻辑四种输入源共用同一套推理、emit、统计代码改一处四处生效。4.2 视频与摄像头读取帧率对齐和丢帧策略视频文件和摄像头的行为完全不同。文件是能读多快读多快摄像头是按设备节奏出帧用同一套读取代码会出两种毛病视频跑得飞快导致进度条瞬间到底摄像头则因为推理慢于出帧速度缓冲区越积越多画面延迟从 0.2 秒涨到 5 秒。import cv2, time def iter_video(path, target_fps25, stop_flaglambda: False): cap cv2.VideoCapture(path) interval 1.0 / target_fps while not stop_flag(): t0 time.time() ok, frame cap.read() if not ok: break yield frame cost time.time() - t0 if cost interval: time.sleep(interval - cost) # 补齐节奏别让文件播放像快进 cap.release() def iter_camera(index0, stop_flaglambda: False): cap cv2.VideoCapture(index, cv2.CAP_DSHOW) # Windows 下指定后端打开更快 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while not stop_flag(): ok, frame cap.read() if not ok: break yield frame cap.grab() # 丢掉缓冲里的下一帧防止延迟累积 cap.release()摄像头那段里cap.grab()是关键的一行。它把缓冲区里排队的旧帧取走丢掉保证下一次read()拿到的是最新画面。没有这一行推理一慢缓冲区就变成单向增长的黑匣子你看到的人永远是几秒前的。提示cv2.CAP_DSHOW是 Windows 专用的采集后端Linux 上要换成cv2.CAP_V4L2。跨平台交付时用sys.platform判断一下否则换机器就打不开摄像头。视频那条线的time.sleep也不是装饰。不加的话处理一段 30 秒的视频可能 8 秒就跑完了进度条一闪而过用户会以为程序出 bug 了。按目标帧率补齐节奏观感和实时播放一致也方便演示时讲这段视频包含 X 帧处理耗时 Y 秒。4.3 画框与中文标签渲染OpenCV 自带的cv2.putText只支持 ASCII直接塞中文标签会变成一排问号。这是最经典的翻车点也是很多人做到最后才发现的问题。解决办法是走 PIL 中转ndarray → PIL Image → 用 truetype 字体绘制 → 转回 ndarray。import cv2, numpy as np from PIL import Image, ImageDraw, ImageFont FONT_PATH fonts/simhei.ttf _font_cache {} def _font(size20): if size not in _font_cache: _font_cache[size] ImageFont.truetype(FONT_PATH, size) return _font_cache[size] def draw_boxes(bgr, dets, color(0, 200, 120)): img Image.fromarray(cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)) draw ImageDraw.Draw(img) font _font(20) for d in dets: x1, y1, x2, y2 map(int, (d[xmin], d[ymin], d[xmax], d[ymax])) draw.rectangle([x1, y1, x2, y2], outlinecolor, width2) text f{d[name]} {d[confidence]:.2f} # 标签底板按文字实际尺寸算避免长短标签宽度不一 tw, th draw.textbbox((0, 0), text, fontfont)[2:] draw.rectangle([x1, max(0, y1 - th - 8), x1 tw 8, y1], fillcolor) draw.text((x1 4, max(0, y1 - th - 6)), text, fontfont, fill(255, 255, 255)) return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)_font_cache是必须的。ImageFont.truetype每次调用都要读磁盘、解析字体文件逐帧调用会让帧率掉一大截而且这个开销在 CPU 上特别明显。按字号缓存进程内第一次读一次就够。标签底板高度用textbbox实测而不是拍一个固定值是因为中文和数字混排时高度不一样写死 24 像素会让人员 0.87这种标签被裁掉下半截。底板 y 坐标做max(0, ...)保护是为了框贴在图像顶边时不画到画布外面去。4.4 结果导出CSV、JSON 与截图导出看着是边角功能实际是评审最容易问的地方——你这个检测结果能给别人用吗。CSV 给 Excel 用户JSON 给后续处理截图给报告三种输出共用一个记录列表。import csv, json, os, time FIELDS [frame_id, source, name, confidence, xmin, ymin, xmax, ymax] def export_csv(records, out_dir): os.makedirs(out_dir, exist_okTrue) path os.path.join(out_dir, time.strftime(detect_%Y%m%d_%H%M%S.csv)) # utf-8-sig 带 BOMExcel 直接双击打开中文不乱码 with open(path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesFIELDS, extrasactionignore) writer.writeheader() writer.writerows(records) return path def export_json(records, out_dir, metaNone): os.makedirs(out_dir, exist_okTrue) path os.path.join(out_dir, time.strftime(detect_%Y%m%d_%H%M%S.json)) payload {meta: meta or {}, records: records} with open(path, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse, indent2) return pathencodingutf-8-sig这一行是被 Excel 逼出来的。默认 UTF-8 的 CSV 在 Excel 里打开中文类别名全是乱码评审看到这个会直接怀疑数据正确性。带上 BOM双击就是正常的。extrasactionignore让记录里多余的键比如内部的颜色字段被自动忽略不用手工裁剪字典。meta里我一般塞进去权重文件名、置信度阈值、IoU 阈值、检测时间、处理帧数。这五项凑齐别人拿到 JSON 能复现出同样的结果也方便自己回头对账。4.5 打包发布PyInstaller 与首次启动自检开发机能跑不等于交付物能跑。打包时最容易漏的是数据文件——权重、字体、yolov5 仓库源码都不在 Python 的 import 图里打包工具不会自动收。pyinstaller -D -w main.py \ --add-data yolov5;yolov5 \ --add-data weights;weights \ --add-data fonts;fonts \ --add-data config;config \ --hidden-import torch \ --hidden-import torchvision \ --collect-data torch--add-data的格式是源路径;目标路径Windows 用分号Linux 和 macOS 用冒号跨平台脚本里要做判断。-D出目录模式而不是单文件启动快得多也方便把权重单独替换-w去掉控制台窗口正式交付用调试阶段一定要去掉这个参数否则你连 print 都看不到。启动自检是我后来加的习惯主窗口显示前先检查权重目录非空、字体文件存在、OpenCV 能打开默认摄像头任何一项失败就弹一个明确的中文提示框而不是让程序带着残缺状态进入界面等用户点了开始才崩。注意打包体积做不小是常态PyTorch 加上 CUDA 相关库随手就是 2 GB 以上。如果只是课程设计交付用 CPU 版 PyTorch 打包能压到 300 MB 左右代价是实时性下降这个取舍要在文档里写清楚。5. 避坑与常见问题排查五类最容易翻车的现场5.1 点开始按钮界面就未响应现象图片检测没事一切到视频或摄像头窗口立刻灰掉标题栏出现未响应几秒后系统提示强制关闭。原因推理或者读帧写在了主线程里或者虽然开了线程但把Detector的初始化放在了点击事件中。加载.pt权重本身就要一两秒加上首次 CUDA 初始化主线程直接被堵死事件循环停转Windows 就判定无响应。解决所有耗时操作一律进DetectWorker.run()包括模型加载。我的做法是在 app 启动时用一个带进度提示的初始化把 Detector 建好点击开始只做拿现成对象 起线程响应时间压到毫秒级。已经写死在点击事件里的就把它挪到线程 run() 的第一行。5.2 摄像头预览越跑越慢现象刚打开摄像头很流畅跑了二三十秒之后画面明显滞后动作和显示差了三四秒越跑越严重最后像是卡住了。原因采集端按 30 FPS 往外送帧推理端只能处理 10 FPS两者速率不匹配帧在 OpenCV 的缓冲区里越积越多。程序本身没卡它只是在一帧一帧地处理几秒前的旧画面。解决读取循环每帧之后加一次cap.grab()把排队的旧帧丢掉同时在 UI 端做刷新节流两次界面更新间隔小于 33 ms 就丢弃本次。两处一起改延迟能稳定在 300 ms 以内。另外把采集分辨率从 1080p 降到 720p推理负载直接少一半。5.3 中文类别名显示成方块或问号现象代码里明明写的是人员画到图上一排问号或者导出报表时中文全变乱码。原因绘制端用的是cv2.putText它只认 ASCII导出端用的是默认编码写 CSVExcel 按本地编码解析。两个问题的根子都是没指定字符集。解决绘制走 PIL 中转显式指定ImageFont.truetype的字体文件并且把字体随包发布——依赖系统字体会导致换台机器字体缺失又变回方块。CSV 用utf-8-sigJSON 用ensure_asciiFalse。这两条改完之后中文乱码这一类问题基本绝迹。5.4 打包后启动即崩提示找不到权重现象开发环境一切正常pyinstaller打出来的程序双击闪退命令行跑能看到FileNotFoundError: weights/best.pt。原因相对路径是相对当前工作目录解析的打包后的工作目录不是 exe 所在目录而是启动时的目录。资源文件也可能被解压到临时目录硬编码的相对路径两头都不对。解决所有资源访问统一走resource_path()用sys._MEIPASS兜底打包命令里把每个资源目录都用--add-data显式声明。交付前在一台没装过 Python 的机器上跑一遍这一步别省——本地测试永远发现不了缺文件。5.5 连续检测显存或内存一路涨现象单次任务跑完看内存正常连续跑五六次批量检测之后占用从 800 MB 涨到 4 GB最后进程被系统杀掉。原因每次新任务都新建一个Detector旧的模型对象没有被释放或者工作线程结束后没有wait()线程对象还挂在 Python 引用里又或者每帧生成的 PIL Image 没有及时回收靠 GC 慢慢收。解决Detector 全应用只建一个切换权重时才重建重建前把旧对象置 None 并torch.cuda.empty_cache()线程在finished信号里调deleteLater()逐帧生成的临时图不复用引用画完立刻返回新对象。这类问题的排查手段很土但有效——任务管理器盯着内存曲线上跑十轮曲线不回头就说明有泄漏。6. 验收与答辩用一张测试矩阵把系统讲清楚验收这一步我给自己的要求是不靠嘴说靠表打勾。功能验收矩阵按需求编号排每一行写清楚输入、预期、实测、结论答辩时直接投出来比讲十分钟架构有说服力。编号测试输入预期结果判定方式R1三张不同分辨率的 jpg都出框结果表有记录目视 表格行数R2200 张图目录进度条走完界面全程可拖动拖窗口不卡顿R330 秒 mp4输出视频帧数与源一致播放器看总帧数R4内置摄像头延迟 ≤ 300 ms可随时停止挥手对比显示R5把阈值从 0.25 调到 0.6框数量减少无需重启前后截图对比R6导出 CSV 并双击打开中文正常字段齐全Excel 打开检查性能指标要测三个数别只报 FPS。第一个是纯推理耗时循环跑 100 帧取均值这个数体现模型本身第二个是端到端延迟从读帧到界面显示的时间差这个数体现架构质量第三个是内存峰值跑完 200 张图后的占用。三个数一起报实时检测这个说法才站得住。测的时候把设备写清楚——CPU 还是 GPU、什么分辨率的输入否则数字没有意义。答辩演示脚本我一般压到三分钟第一分钟打开程序拖一张图进去讲这是单图检测框、类别、置信度都在右边是结果表第二分钟切到摄像头边挥手边把置信度滑块从 0.25 拉到 0.6讲阈值实时生效低置信度的框被过滤掉第三分钟打开批量目录跑一个 50 张的小批量中途点一次停止再重新开始讲任务可中断、可重入。这三段分别对应能用能调能管覆盖了评审最关心的三个维度。剩下时间留给追问最常被问的是为什么用 YOLOv5 不用别的和误检怎么办答案提前准备成两句话别现想。从那以后我每次交付这类系统都会在打包前强制走一遍这张矩阵哪怕时间再紧也不跳步。因为翻车从来不是因为某个模块写不出来而是因为以为它能跑——矩阵上每一个没打勾的格子都是答辩现场可能被问到的那一句。希望帮到你。本文还有配套的精品资源点击获取