上个月在做一个园区智能安管的验证项目,甲方提的需求很具体:电瓶车进电梯要报警,而自行车推进去应该被放行。第一次拿现成的通用目标检测模型直接上测试,结果电梯口十来秒的视频里,模型把推车进电梯的人连人带车一起框住,自行车倒是框出来了,真正要拦的电瓶车却因为停在暗光角落被漏检。那一刻我就意识到,通用模型认识80类日常物体,但很多实际场景要的并不是"这里有个东西",而是"这到底是一辆电瓶车还是一辆自行车"。
后来我把重心从"拿来即用"转向"训练自己的检测器",基于YOLO26这个较新的YOLO系列框架,专门做了电瓶车与自行车的两分类目标检测系统,配套PyQt5桌面界面,覆盖数据准备、模型训练、精度评估和部署打包全流程。这篇就是对这个项目从零到一的完整复盘,适合正在做目标检测相关毕设、园区安防项目或想系统上手YOLO实战的开发者参考。我会把我踩过的坑、反复验证过的参数和界面工程的细节都写在里面。
1. 为什么要单独训练一个电瓶车与自行车检测器
1.1 通用模型最大的问题是类别里根本没有电瓶车
通用目标检测模型通常是在COCO这类公开数据集上训练的,80个类别里只有bicycle(自行车)和motorcycle(摩托车),没有专门对应"电瓶车/电动自行车"的类别。电瓶车的外观恰好是自行车和摩托车的混合体:它有自行车的两轮骨架和脚踏,又有摩托车的宽座垫和踏板。模型在特征层面很难把它干净地归到某一类,于是表现就很尴尬——有些场景把电瓶车识别成摩托车,有些场景直接识别成自行车,还有些场景把它和旁边的人一起框成一个"人车混合目标"。
这个问题在真实安防业务里是致命的。物业和园区要拦截的是电瓶车进电梯、进楼道这类行为,因为电瓶车电池火灾风险远高于普通自行车;但推自行车进出是居民正常生活需求,如果模型把自行车误报成电瓶车,保安一天要被假警报折腾几十次。业务方需要的是在"电瓶车"和"自行车"之间做清晰划分,而不是让保安去看画面自己判断。
1.2 不同应用场景对检测结果的容忍度完全不同
我做了几个典型落地场景的梳理,发现同样是两分类检测,不同场景对误检率和召回率的要求差异很大:
| 应用场景 | 核心需求 | 误报代价 | 漏报代价 |
|---|---|---|---|
| 电梯阻车 | 电瓶车进电梯立刻报警,自行车放行 | 保安频繁出警,系统被嫌弃 | 电瓶车进电梯,安全隐患失守 |
| 楼道消防通道占用检测 | 识别停放的载客电瓶车 | 需要人工复核,浪费人力 | 消防隐患漏管 |
| 园区停车场统计 | 统计电瓶车/自行车数量与占比 | 统计不准确,影响管理决策 | 与误报类似 |
| 路口/出入口流量分析 | 区分骑行人群类型 | 数据偏差 | 数据不全 |
从表里能看出来,电梯阻车场景是要求最高的一类,既不能漏报警,也不能天天误报。这种情况下,拿通用模型凑合着用是不现实的。为这两类目标单独训练一个检测器,看起来是多花了一些时间,实际上是把"模型能发现目标"和"业务能接受结果"这两件事彻底分开,后者才是项目能不能交付的关键。
所以我给所有想直接抄通用模型来做的朋友一个建议:如果业务端要在两三个细分类之间做区分,而公开数据集里没有对应类别,那不要犹豫,数据采集和重新训练的投入一定小于后面无休止的误报处理。
2. YOLO26框架选型:为什么我最终用了它
2.1 我理解中的YOLO26与上一次大版本迭代的差异
客观说,YOLO系列每一次大版本更迭都不是一夜之间的革命,而是在骨干结构、特征融合、训练策略上做了相当务实的调整。我这次使用的YOLO26,给我的直观感受有三点:
第一,对"结构复杂、轮廓不规整"的目标召回更稳。电瓶车这个目标很有意思,它不像行人那样有个清晰的人形轮廓,也不像小汽车那样是一个规则的矩形块。电瓶车的车把、后视镜、脚踏、后备箱各种凸起,加上骑行者腿部与车身的遮挡,很容易让检测器的特征提取分散注意力。YOLO26在新版本里加强了对这种尺度变化大、局部特征碎的目标的关注,我在自己的数据显示,这类目标的漏检比之前用的版本少了一些。
第二,小目标表现改善明显。监控视频里目标往往离摄像头很远,一辆电瓶车在画面里可能只有二三十个像素高,尤其是园区出入口这类广角场景。25到26这轮迭代在多尺度特征融合上做了调整,对远处小目标的召回有可感知的提升。
第三,训练和部署生态高度统一。这次用的框架依然沿用YOLO系列经典的训练命令、模型配置和导出流程,无论是datasets的yaml配置、训练参数调整,还是导出onnx、engine格式,都不需要额外学习一套新东西。对做工程的人来说,这种"零迁移成本"本身就是选型理由。
2.2 与老版本对比的实测体感
我在同一批电瓶车/自行车数据上,和之前常用的旧版模型做了对比测试。不敢说精确的基准数据,只能说在完全一致的数据集和参数下,体感差别是明显的:
| 对比维度 | 之前常用的YOLO系列版本 | YOLO26 | 我的主观评价 |
|---|---|---|---|
| 电瓶车漏检率(暗光场景) | 较高,暗角易丢 | 明显下降 | 更换后的第一周就感觉到了 |
| 自行车与电瓶车混淆 | 两类混淆时有发生 | 混淆减少 | 业务上最看重的指标 |
| 小目标检测(远距离) | 一般 | 更好 | 广角画面更实用 |
| 推理速度 | 快 | 同级别相差不大 | 基本不影响帧率 |
| 部署生态 | 同一套生态 | 同一套生态 | 流程零学习成本 |
当然,我的对比不是严格控制的学术实验,只能说在"同一个项目、同一批数据、同样的业务容忍度"下,YOLO26的综合表现更符合我的预期。如果你手头有自己熟悉的版本,不需要盲目追新,但如果你想选一个当前状态下省心的框架,它确实值得试。
2.3 模型型号怎么选
YOLO系列的n、s、m、l、x五个型号,本质是在推理速度和检测精度之间做取舍,我按实际落地场景给的选型建议是:
- YOLO26n:模型最小,CPU上勉强可跑,适合边缘盒子或者对帧率要求不高的场景,精度会掉一些。
- YOLO26s:我用的是这个。精度和速度最均衡,6G以上显存的GPU就可以训练和推理,是大多数项目的稳妥起点。
- YOLO26m:显存充足、对精度有更高要求时选,需要8G以上显存会更舒服。
- YOLO26l / x:追求极致精度,但它俩更适合服务端离线分析,不适合直接部署在普通电脑上做实时检测。
我的建议是:除非你的硬件条件真的很受限,否则第一次训练从s型号入手,跑通全流程之后,再根据实际帧率和精度决定要不要换更大或更小的模型。
3. 环境搭建与数据集构建:训练前最容易翻车的两道坎
3.1 环境版本对照与安装步骤
整个项目的第一步是搭环境。很多人在这一步就卡住,大部分原因是Python版本、PyTorch版本和CUDA版本对不上。我这次用的是下面这套组合,供参考:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.10 | 太老的3.7、3.8对新版PyTorch支持不好 |
| PyTorch | 2.x | 训练效率比1.x有明显提升 |
| CUDA Toolkit | 11.8 或 12.1 | 取决于你显卡驱动支持哪个 |
| 开源YOLO训练库 | 最新稳定版 | pip直接安装即可 |
| 显卡驱动 | 尽量更新 | 老驱动经常是环境问题的根源 |
创建独立虚拟环境是必须做的,我吃过大亏——直接在系统Python里装,后面某个依赖库升级把另一个项目搞挂,排查了半天。建议这样操作:
conda create -n yolo26 python=3.10 conda activate yolo26 pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118安装完之后,先用一张图片快速验证环境是否正常:
yolo predict model=yolo26s.pt source=test.jpg如果这条命令能正常输出检测结果,说明训练库、模型加载和基础依赖都通了。这一步很值得花两分钟,能帮你把"环境问题"和"项目代码问题"分开,后面排查会省很多力气。
3.2 数据从哪来:采集、抽取与标注规范
数据是这类检测项目的命脉,我用的是"三分法":
一是自己实地采集。拿着手机或运动相机在园区出入口、电梯间、停车场拍不同角度、不同时段的视频,然后按帧抽取图片。粗抽之后再用脚本按间隔去重,比如每5帧保留一帧,避免连续帧高度相似导致训练集冗余。二是公开数据集里找自行车类别。像COCO这类公开数据集里有大量bicycle标注,可以抽取出来作为自行车的底料,但要注意它的拍摄视角偏生活化,和监控视角差异较大,只能作为补充。三是少量从网络上合法获取公开图片。天气、光照、角度都尽量多样化。
关于数据量,我建议每类至少1000张以上,两类样本量不要相差3倍以上,否则模型会天然偏向样本多的那一类。如果现场条件只能采集500张,就靠数据增强(翻转、亮度变化、随机裁剪)来凑,但增强只是补救,真实场景的多样性才是模型泛化能力的关键。
标注环节我重点提醒几个规范:
- 类别定义要提前写死。我这次定义的是:e-bike(电瓶车,包含外卖踏板车、电动自行车)、bicycle(自行车,包含共享单车、普通自行车)。三轮电瓶车和儿童滑板车一开始就约定不算,避免标注员凭感觉乱标。
- 遮挡目标要标,哪怕只露出一半车身也要标,不能只标完整目标。模型部署后遇到最多恰恰是遮挡场景。
- 模糊目标可以舍弃。画面里已经完全看不清轮廓的目标,强行标上去只会给模型送噪声。
- 同一个目标在连续帧中如果看起来变化不大,只标一帧即可,让标注资源花在更多样化的画面上。
标注完成后的目录结构长这样:
ebike_bicycle_data/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/每一张jpg图片对应一个同名txt文件,txt里每一行是"类别id x_center y_center width height",坐标是相对于图片宽高的归一化值。
3.3 数据集YAML配置文件的写法
训练依赖一个YAML文件把数据路径和类别映射关系告诉框架。我的配置文件是这样写的:
path: /home/user/ebike_bicycle_data # 数据集根目录,改成你自己的路径 train: images/train val: images/val test: images/test nc: 2 names: 0: e-bike 1: bicycle这里有两个容易出错的地方。第一个是path字段,如果用相对路径,框架会从当前工作目录去拼接,一旦你换了终端启动位置就会找不到数据。所以我都是写绝对路径。第二个是names的顺序,0和1的顺序直接对应训练出的模型输出类别顺序,如果数据和配置不一致,训练时损失会异常升高,但框架不会主动提醒你,只能靠人工反复核对。
4. 训练全流程:从命令参数到看懂日志曲线
4.1 训练指令与关键参数选择
数据准备好之后,训练本身其实是整个项目里最"省心"的部分。我的训练命令如下:
yolo train \ model=yolo26s.pt \ data=ebike_bicycle.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ device=0 \ cache=True几个参数我展开说一下,这比照抄命令更重要:
- model=yolo26s.pt:用预训练权重做迁移学习起点。不要一上来就从yolo26s.yaml这种随机初始化权重开始训,电瓶车和自行车虽然是新类别,但底层特征(轮子、车架、把手、路面)和预训练模型见过的场景高度相关,迁移学习能让收敛速度成倍提升。
- imgsz=640:YOLO系列默认输入尺寸。如果画面里目标普遍较小,可以试试imgsz=960,但训练和推理都会变慢,别盲目加大。
- batch=16:受显存限制。6G显存跑s模型时batch=16是安全的。如果显存不够,减小batch不要减少imgsz。
- patience=30:早停耐心值。如果验证集损失连续30轮不下降,训练自动结束。这个参数帮我省了很多时间,尤其后期调参时,不用干等200轮跑完。
- cache=True:把数据集缓存到内存里,能显著加快每个epoch的数据加载。磁盘够大就开着,前提是内存别太小。
4.2 训练日志怎么读,别只盯着Loss
训练启动后,终端每轮都会输出一行日志,包含box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95这些指标。新手最容易犯的错误是只看loss降了就觉得万事大吉。我的读日志习惯是:
第一,看mAP50和mAP50-95。mAP50是IoU阈值0.5下的平均精度,业务场景往往更关心它;mAP50-95更严格,对定位精度要求高,是学术上常用的指标。如果mAP50已经到0.9x而mAP50-95还在0.5左右,说明模型大致找到了目标,但框的位置不够准。
第二,看precision和recall的平衡。电瓶车检测场景,我宁可精确率稍微低一点也要保障召回率,因为漏掉一辆电瓶车进电梯的后果比误报一次严重得多。训练结束后可以根据业务倾向,通过调整置信度阈值来平衡两者。
第三,看train loss和val loss的剪刀差。如果train loss一直降、val loss却开始反弹,基本就是过拟合了。这时候回看patience是否有触发早停,若没有就说明数据多样性还不够,需要补数据,而不是盲目加训练轮数。
训练过程中框架会在runs/detect/train目录下自动生成一系列图表,包括results.png(损失和精度曲线)、confusion_matrix.png(混淆矩阵)、PR_curve.png(PR曲线)等。训练结束后我建议先看混淆矩阵,它比mAP数字更能直观暴露"电瓶车被识别成自行车"这类业务不可接受的问题。
4.3 评估模型不能只看平均值
训练完成后,用一套训练时没见过的独立测试集做最终评估,命令很简单:
yolo val model=runs/detect/train/weights/best.pt data=ebike_bicycle.yaml但评估的重点在于分场景拆开看。我会把测试图片按白天、夜间、逆光、遮挡四类分组,分别统计识别效果,而不是只看一个总mAP。很多项目总mAP很漂亮,一放到夜间场景就露馅,原因就是测试集里白天图片占了大头。这个习惯帮我提前暴露了不少问题,建议你也按自己的部署场景把测试集分层。
我这次自建验证集上,mAP50整体在0.93左右,但夜间和逆光子集的mAP50会掉到0.85附近,说明后续真正要攻坚的是低光照条件下的特征表达,这也直接决定了部署阶段的优化方向。
5. PyQt5界面开发:把模型变成能点着用的软件
5.1 界面布局与功能设计
模型训练完,还只是一堆权重文件,业务方要的是一个能双击运行、能开摄像头、能出统计结果的软件。PyQt5是这个环节最稳的选择,跨平台、控件丰富、打包方案成熟。
我的界面布局比较朴素但够用:
| 界面区域 | 功能 |
|---|---|
| 顶部工具栏 | 打开图片、打开文件夹、打开视频、打开摄像头、停止检测、导出结果 |
| 左侧主显示区 | 实时显示原始画面和检测框叠加结果 |
| 右侧信息面板 | 当前画面各类目标数量、检测耗时、帧率 |
| 底部状态栏 | 当前模型路径、推理状态提示 |
功能设计上最核心的一条原则是:所有耗时的操作都不能卡界面。打开一个大视频文件、运行模型推理、批量处理文件夹,任何一个操作如果直接放在主线程,鼠标都会转圈转到怀疑人生。
5.2 多线程推理:不让界面卡死的核心设计
PyQt5的界面是基于事件循环的,如果主线程里跑一个耗时操作,界面会直接无响应。摄像头实时检测场景尤其明显——一帧推理少说几十毫秒,多则几百毫秒,放在主线程里画面就变成幻灯片。
我的做法是单独写一个QThread子类,把采集和推理全部放进工作线程,只通过信号把结果传回主线程更新界面。核心骨架如下:
import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class DetectThread(QThread): frame_ready = pyqtSignal(QImage) result_ready = pyqtSignal(dict) def __init__(self, model_path, source=0): super().__init__() self.model = YOLO(model_path) self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) while self.running: ret, frame = cap.read() if not ret: break results = self.model(frame)[0] # 在frame上绘制检测框 boxes = results.boxes class_counts = {} for box in boxes: x1, y1, x2, y2 = box.xyxy[0].int().tolist() cls_id = int(box.cls[0]) conf = float(box.conf[0]) label = f"{self.model.names[cls_id]} {conf:.2f}" cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) class_counts[cls_id] = class_counts.get(cls_id, 0) + 1 # OpenCV的BGR图像转成QImage rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimg) self.result_ready.emit({ "e-bike": class_counts.get(0, 0), "bicycle": class_counts.get(1, 0), }) cap.release() def stop(self): self.running = False self.wait()这个线程类的关键点有三个:
- _model在run之前加载:如果放到QThread构造函数里加载,窗口起来会卡很久;放在run开头一般加载两三秒,至少界面已经能显示了。
- stop方法必须配合running标志:点击"停止检测"时先置running为False,再wait()等待线程真正退出,避免关闭软件时后台线程还在跑导致程序闪退。
- QImage和OpenCV的像素格式转换:OpenCV读取是BGR顺序,直接显示会让画面偏蓝,必须先转成RGB。
5.3 主窗口的调用逻辑
主窗口这边的工作就是监听按钮事件、启动线程、接收信号刷新界面。核心逻辑大概是:
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.init_ui() self.detect_thread = None def open_camera(self): if self.detect_thread is not None: return self.detect_thread = DetectThread("best.pt", source=0) self.detect_thread.frame_ready.connect(self.update_frame) self.detect_thread.result_ready.connect(self.update_stats) self.detect_thread.start() def update_frame(self, qimg): self.video_label.setPixmap(QPixmap.fromImage(qimg)) def update_stats(self, stats): self.ebike_label.setText(f"电瓶车: {stats['e-bike']}") self.bicycle_label.setText(f"自行车: {stats['bicycle']}")这里我踩过的一个坑:重复点击打开摄像头按钮,会new出多个线程同时访问摄像头,导致摄像头资源冲突。解决方式就是加一个线程存活判断,如果已有线程在跑就直接忽略新的请求。另外,切换图片检测和摄像头检测时,一定要先停掉旧线程再启动新线程。
6. 部署与落地:模型导出和EXE打包全流程
6.1 模型导出:pt、onnx、engine怎么选
训练得到的是best.pt权重,程序开发阶段直接用PyTorch框架加载没问题,但到了部署阶段要考虑运行效率和平台兼容性。我整理了一下:
- pt格式:最灵活,二次训练方便,但必须依赖PyTorch环境,环境体积大。
- onnx格式:跨平台最好,CPU上和GPU上都能跑,适合绝大多数业务部署,甚至可以直接接到边缘设备。
- engine格式:NVIDIA的TensorRT专用格式,推理速度最快,但只在有NVIDIA显卡的环境下能用。
导出命令很简单:
yolo export model=best.pt format=onnx dynamic=True yolo export model=best.pt format=engine device=0导出onnx时我建议加上dynamic=True,这样输入图像的尺寸不受640x640限制,界面里处理任意分辨率画面更灵活。导出完成后一定要用一张真实的测试图片验证一下:
from ultralytics import YOLO model = YOLO("best.onnx") results = model("test.jpg")注意导出后模型的检测结果理论上应该和pt几乎一致,如果发现精度明显下降,先怀疑是不是数据集里目标太小,量化或输入尺寸变化导致的,这时需要回看训练参数,而不是继续强行部署。
6.2 PyInstaller打包EXE的完整流程与暗坑
界面和模型都整理好之后,最后一步是打包成可执行的EXE。我用的是PyInstaller,命令如下:
pyinstaller -w -F main.py \ --add-data "best.pt;models" \ --hidden-import "PyQt5.sip" \ --name "EbikeDetector"几个参数逐一说明:
- -w:打包后的程序不显示命令行窗口。
- -F:打包成单个exe文件,方便分发,但启动时会先把资源解压到临时目录,启动速度略慢。
- --add-data:把模型文件打进包里,分号前面是源文件路径,分号后面是包内目标路径。Windows下分隔符是分号,Linux下是冒号,这个细节常让人困惑。
- --hidden-import:PyQt5有时会有一些动态导入的模块,比如sip,不显式声明的话打包后运行时找不到。
打包好后有四个高频问题,几乎每个项目都会遇到:
一是资源路径找不到。打包后的exe解压到临时目录,你的best.pt不在程序当前目录,而在临时目录里。正确的取资源方式是:
import sys import os def resource_path(relative_path): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) model_path = resource_path("models/best.pt")二是exe体积巨大。PyTorch和CUDA运行时本身就大,打包出来动辄四五百MB甚至更离谱,这是正常的,不是编译坏了。如果实在在意体积,就把模型导出成onnx并用轻量推理后端,能压掉一部分,但不会根本改变。
三是杀毒软件误报。PyInstaller打的exe偶尔会被某些杀毒软件标记,尤其带-F参数的单文件版。遇到这种情况只能换电脑管家类软件加白名单,或者换用不带-F的目录模式打包,误报率会低一些。
四是目标电脑缺运行库。PyQt5和OpenCV依赖一些Visual C++运行库,在没有安装过这些库的机器上会报"找不到DLL"。解决办法是在目标机器上装一次对应版本的运行库,或者用目录模式打包并把必要DLL一起带上。
7. 实测效果复盘:白天、夜间、逆光和遮挡下的真实表现
7.1 分场景实测结果
整个系统跑通后,我拿真实场景的视频做了分场景测试,不只看总体mAP,而是把表现拆到业务关心的维度上:
| 测试场景 | 表现 | 问题 |
|---|---|---|
| 白天园区出入口 | 稳定识别两类目标,框位贴合 | 偶尔把行人手中拖的小拖车框进来 |
| 夜间楼道口 | 能识别,但置信度整体偏低 | 自行车与电瓶车轮廓相似,偶发混淆 |
| 强逆光时段 | 有明显漏检 | 目标整体发暗,特征丢失严重 |
| 多目标重叠进出 | 大多数目标能分开 | 两车并肩时偶尔漏框其中一辆 |
从这张表能看到,白天场景其实早就没问题了,真正影响交付的是夜间和逆光这两个子集。这也验证了我前面的判断——白天样本充足的情况下,模型学得很好;一旦光照条件变化,样本覆盖不足的问题立刻暴露。
针对夜间场景,我的处理思路不是换模型,而是先补数据。我在夜间加拍了一批红外模式下的画面,做一个小的二次训练(在已有best.pt基础上继续微调),夜间子集的效果明显回升。这说明数据多样性的价值比调参高太多。
7.2 部署之后还能做哪些优化
模型检测能力稳定之后,系统还可以往两个方向延伸:
一是加上目标跟踪。单帧检测只能告诉你"当前画面里有什么",不能告诉你"这辆车是不是刚才那辆"、"它在画面里待了多久"。接一个轻量级跟踪算法,比如ByteTrack或DeepSORT思路的简化版,就可以统计同一辆电瓶车连续出现在画面里的帧数,从而判断它是不是停在楼道里,而不是路过的车。电梯阻车场景对"停留时间"的判定非常重要。
二是把模型量化后部署到边缘设备。onnx模型在普通电脑上做实时检测已经够用,但真正要装到电梯轿厢、楼道口这种地方,还是需要一个小算力盒子。把模型int8量化后部署到嵌入式NPU平台上,功耗和体积都更可控。这一步工程量不小,但对于真正落地的项目,几乎不可避免。
实操过程中的几点个人体会
做完这个项目,我的最大体感是:真正消耗时间精力的不是"训练模型"这一下动作,而是数据完整性和工程化这两块。模型训练本身在YOLO系列的开源生态下越来越像"填表跑命令",谁都能训出一个mAP可观的模型;但能不能把数据整理到足够贴近真实场景,能不能把多线程界面做好不卡顿,能不能把模型打包分发到不会用的用户手里,这些才是决定项目能不能真的交付的坎。
如果你们也准备做类似的目标检测系统,我会建议把一半以上的精力放在现场数据采集和标注规范上,这个投入的回报远大于后面疯狂调参。至于界面和打包,一句话总结就是:所有耗时操作都扔进线程,所有资源路径都用临时目录兼容,这两个点处理好了,项目就已经成功了一大半。