手头拿到的这套项目,全称是“基于深度学习YOLOv11的水下鱼类检测系统”,带YOLO格式数据集、带登录注册界面、带Python源码和训练好的模型权重。我最近刚好把整套代码从环境搭建跑通到界面出结果,又把训练流程重新过了一遍。这玩意儿乍看是个普通的目标检测Demo,实际做下来发现坑不少,尤其是水下图像那部分,和常规的马路行人检测完全不是一回事。这篇文章就把整个项目的拆解、实训过程、踩坑记录全部写成文字,给接下来要做类似“水下目标检测”或者想快速上手YOLOv11做毕设、课题实战的朋友一个参考。
这套系统的核心链路其实很短:YOLOv11负责干检测的脏活累活,UI界面负责让模型输出变成人话,登录注册模块负责管理使用权限,模型与源码让整个流程可以离线复现。适合什么人?一类是高校里做渔业资源调查、水生生态监测的学生,另一类是想拿深度学习做课程设计但不想只交一个训练脚本的人。也包括那类刚开始接手YOLO系列,想弄明白“模型是怎么从数据集变成UI界面上一条检测框”的工程新手。
我需要先说明白一个前提:这套系统不是那种只能跑通的玩具项目,它是一个可以改造成实际工具的框架。我按照从拆解到实操的顺序,把训练、推理、界面整合、问题排查都过一遍。
1. 项目整体设计与技术选型
1.1 为什么选用YOLOv11做水下检测
鱼这种目标,水下场景里属于典型的“小目标密集分布”,而且经常成群游动,互相遮挡,加上水体对光的吸收导致颜色偏蓝偏绿、对比度极低,普通检测器很容易出现漏检和误检。
YOLOv11相比之前的系列,头部结构做了调整,引入了一些注意力机制优化,在小目标检测和密集场景下的表现比v8、v5更稳。你要说它比v10强多少,不能一概而论,但在保持高帧率推理的同时还能维持住精度,这一点对水下视频流的实时检测非常关键。我做对比测试的时候发现,同一批水下鱼群视频,v8在遮挡严重的帧里框会跳来跳去,v11的框明显更稳定。
除了模型本身,选YOLOv11还有一个现实原因:生态成熟。ultralytics这个库把训练、验证、导出都封装得很完善,用YOLO格式数据集喂进去基本不用写太多底层逻辑。模型文件就是一个.pt权重文件,导出后可以非常方便地被UI程序调用,这让整个系统的开发周期大幅缩短。
1.2 系统架构:UI、业务逻辑与模型解耦
我拆解这套代码的时候,发现它的分层意识相当好,不是把模型嵌入到界面回调里、点一下按钮跑一次推理的粗暴写法。
大致分层是这样的:
- 界面层:登录页、注册页、主检测页,负责采集用户操作,展示结果。
- 业务逻辑层:处理登录状态、检测请求调度、保存历史记录,负责把界面操作翻译成后端能力调用。
- 模型推理层:模型加载、图片预处理、推理、后处理、结果格式化,只负责检测,不关心界面。
这种解耦带来的直接好处是:你可以把YOLOv11替换成YOLOv8或者其他检测模型,界面层几乎不用动。而且UI线程不会被推理任务阻塞——水下视频检测一帧可能要好几十毫秒,如果直接塞到界面线程里,窗口必然卡死。
我拿到这个项目源码之后,先把detect相关逻辑单独提取出来跑了一遍,确认模型推理部分可以独立工作,再去跑界面工程。这样做的好处是,如果后面UI或者依赖库出了问题,至少能确定问题不在模型推理侧。
1.3 目录结构与关键模块一览
整个项目解压后的核心目录结构大致是这样的:
project_root/ ├── app.py # 主程序入口,启动UI ├── utils/ │ ├── login_db.py # 登录注册数据库操作 │ ├── model_loader.py # 加载YOLOv11模型 │ └── detector.py # 推理封装 ├── models/ │ └── best.pt # 训练好的权重文件 ├── datasets/ │ └── Fish_Dataset/ # YOLO格式数据集(含images和labels) ├── ui/ │ ├── login_window.py # 登录/注册界面 │ └── main_window.py # 主检测界面 └── requirements.txt # 依赖清单model_loader.py和detector.py是核心。model_loader.py负责用ultralytics加载权重,detector.py接收图片路径或帧数据,返回检测框、类别、置信度。login_db.py这块很多人会忽略,觉得就是个登录功能而已,但实际上它决定了你后面能不能把日志、历史记录、不同用户权限扩展起来,值得单独写一节。
2. 数据集准备与训练细节
2.1 水下鱼类数据的获取与难点
水下鱼类检测的数据集比普通目标检测数据集难搞得多。公开数据集里,淡水鱼和海水鱼混在一起的有,类别分布不均衡的也有,有的图片还是监控视频抽帧,清晰度参差不齐。我建议的做法是:如果只是复现这套系统,直接用项目自带的YOLO格式数据集来训练;如果是做自己的课题,需要把自采图片和公开数据混合,并且严格控制数据分布。
水下图像的退化特征非常明显:由于水对不同波长光的吸收差异,图像整体偏绿或者偏蓝;悬浮颗粒造成散射,让画面像蒙了一层雾。这些特性意味着常规的ImageNet预训练权重不能直接适配,必须在训练时做针对性的数据增强。
2.2 数据增强策略:不要盲目套用常规增强
我见过很多人在水下数据集上直接套用地面目标检测的增强策略:水平翻转、缩放、亮度调整。这些有用,但远远不够,甚至某些操作会有反效果。
针对水下图像,有效的增强策略包括:
- 色调扰动:在HSV空间里把色相向蓝绿色方向偏移,模拟不同水深环境。
- 低对比度模拟:降低图像对比度,模拟浑浊水体。
- 高斯噪声/模糊增强:模拟悬浮颗粒导致的细节丢失。
- Mosaic增强:把多张图拼接,让模型见过更多小目标上下文,对水下成群的小鱼特别有效。
- 随机仿射变换:模拟相机抖动。
训练的时候我用的是640x640输入分辨率。分辨率不能太小,因为水下鱼群中很多鱼在画面里只有几十个像素,太小的输入会把目标细节直接抹掉;但也不能盲目拉到1080p,显存会先爆掉,而且训练速度呈指数级下降。
2.3 标注格式处理与数据集划分
项目用的是标准YOLO格式:每张图片对应一个同名.txt文件,每一行是“类别id x_center y_center width height”,坐标都归一化到0-1之间。
拿到别人给的数据集,第一步不是训练,而是做数据体检。我写了一个小脚本,统计每张图的目标框个数、类别分布、标注框坐标是否越界。水下数据集常见的问题是:标注框没包住整个鱼身,或者鱼尾巴露在框外,这种不干净的标注会直接拖低精度。检查出越界框后,要用脚本把坐标clip回合理范围内。
数据集划分我采用7:1.5:1.5的比例,训练集、验证集、测试集严格分开。注意划分的时候要用random seed固定随机种子,否则每次划分结果不一样,前后训练结果无法对比。还要确保按类别分层抽样,避免某一类鱼全跑到验证集里去。
2.4 关键训练参数配置与计算结果
训练参数直接决定精度和耗时。我用的关键配置见下表。
| 参数名 | 配置值 | 说明 |
|---|---|---|
| img_size | 640 | 输入分辨率,兼顾小目标与显存占用 |
| batch_size | 16 | RTX 3060及同级显卡可用,显存小则降为8 |
| epochs | 100 | 根据收敛情况可提前早停 |
| optimizer | SGD | 动量0.937,初始lr 0.01 |
| lrf | 0.01 | 余弦退火,最终学习率衰减为初始的1% |
| mosaic | 1.0 | 前80个epoch开启,最后20个epoch关闭 |
| fliplr | 0.5 | 水平翻转,水下鱼类左右对称,可用 |
| close_mosaic | 10 | 最后10个epoch关闭mosaic,让模型适应真实分布 |
每个epoch的迭代次数(step数)计算方法很简单:
step数 = 训练集图片数 / batch_size假设训练集有2100张图,batch_size为16,每epoch步数为131步,100个epoch就是13100步。按每步耗时0.2秒估算(RTX 3060水平),单次训练大约2600秒,45分钟上下。如果你用CPU训练,抱歉,时间要翻几十倍,我强烈不建议用CPU复现这个项目。
训练过程中的loss曲线要重点观察三段:box_loss代表边界框回归误差,cls_loss代表分类误差,dfl_loss代表分布式焦点损失。到了第60个epoch之后,如果val曲线出现回升而train曲线还在降,说明过拟合了,优先增加数据增强强度或者提前早停。
2.5 训练后的模型评估指标
训练跑完之后,我习惯把val结果里的mAP50-95和mAP50拿出来看。mAP50是IoU阈值0.5时的平均精度,水下检测一般能做到0.75以上就算可用的水平。mAP50-95更严格,通常在0.45-0.6之间已经不错了。
还要单独看每一类的PR曲线。鱼类数据集里很容易出现某一种鱼精度特别低,比如伪装色很强的比目鱼,还有体型细长的鱼。这时候不要盲目加训练轮数,要看是不是样本量不够,或者图片背景太复杂。
我用一个真实案例来说明:某次训练后,总mAP50是0.82,看起来不错。但按类别一拆,发现其中一个大类的mAP只有0.53,原因就是样张里这种鱼大多是小目标,最小的目标只有6x11像素,人眼都很难判断。后来针对性加了该类的裁剪放大样本,mAP才拉回0.74。
3. 登录注册模块与UI界面实现
3.1 为什么一个检测系统需要登录注册
很多人会觉得登录注册是个多余功能,反正本地跑,直接打开界面检测不就行了。从项目工程化的角度讲,登录注册至少解决三个问题:一是确认使用者的身份,方便在多用户环境下分别记录操作日志;二是避免误操作,主检测界面应该只对登录用户开放,防止无关人员乱用系统;三是为后续在线化扩展留好口子,哪怕现在是单机版,以后接数据库、接服务端,账号体系都是现成的。
这套系统用的是SQLite本地数据库存储用户信息,密码经过哈希加密存储,不是明文。我第一次看到它的校验逻辑时,发现它使用了带盐的哈希方式,这比直接存哈希更安全一点。当然,这种适合本地小系统,如果要部署到公网服务上,还是得上HTTPS、JWT这些方案。
3.2 登录界面逻辑与注册流程
登录界面是一个QWidget窗口,核心交互就是两个输入框加两个按钮。注册窗口会要求用户名和密码两次确认。用户名查重、密码长度校验、哈希写入数据库,整套逻辑走完才算注册成功。
我实际测试中发现几个容易在代码里翻车的点:
- 数据库文件路径:不要用相对路径,因为PyQt程序的工作目录会因为启动方式不同而改变,建议用
os.path.dirname(__file__)拼接出绝对路径。 - 登录状态保持:使用全局变量保存“当前登录用户”,但这只是进程级的状态,程序重启就失效。如果要做记住密码功能,需要用配置文件或者token机制。
- 密码输入框要设置
setEchoMode(QLineEdit.Password),否则密码明晃晃显示在屏幕上,这个是最基础的,但也真有人漏掉。
3.3 主界面功能拆解
主检测界面的布局相当实用。左侧是图片/视频预览区域,右侧是检测控制面板和结果列表。关键功能包括:
- 单张图片检测:选择本地文件,加载进预览区,调用推理线程,返回后画框显示。
- 批量图片检测:遍历文件夹内所有图片,批量处理,并把结果保存到指定输出目录。
- 视频检测与摄像头实时检测:读取视频流,逐帧推理,边检测边显示。考虑到推理速度和显示刷新率不同,需要做帧队列缓冲。
- 置信度阈值与IoU阈值滑杆:调整模型后处理的敏感度。阈值越高,检出目标越少但越精准;阈值越低,漏检越少但误检增多。
- 结果表格:显示每个目标在画面中的坐标、类别置信度、检测耗时,导出为Excel或CSV。
关于置信度阈值,我提供一个经验值参考:
| 场景 | 推荐阈值 |
|---|---|
| 清晰水下监控视频 | 0.5-0.6 |
| 浑浊水体、目标模糊 | 0.35-0.45 |
| 批量统计鱼群数量 | 0.3左右,宁多勿漏,靠人工二次筛选 |
3.4 检测结果可视化与日志记录
画检测框用的是OpenCV,通过cv2.rectangle和cv2.putText在图像副本上绘制。画框的时候有个细节:OpenCV的BGR通道顺序和普通图片的RGB是反的,如果用PyQt的QPixmap加载图片,必须先把BGR转成RGB,否则鱼的颜色会变得诡异,蓝鱼变橙鱼。
这套界面还加了一个“检测日志”功能,每检测一帧或一张图片,就把检测时间、图片名、目标类别、目标数量、平均置信度写入一个日志表格,同时追加到CSV文件。这个功能在渔业资源调查里用处很大。之前有位做水下生态监测的朋友告诉我,他们做鱼群多样性统计,实际上就是靠这种系统先自动识别统计,再人工抽检校核,效率比纯人工高了一个数量级。
4. 核心实操:从源码到跑通整个系统
4.1 环境搭建与依赖安装
拿到源码第一步是创建Python虚拟环境。我建议用Python 3.10,因为PyTorch和ultralytics在3.10上兼容性最好。先拉一个干净的虚拟环境,再按依赖清单安装。
核心依赖如下:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.3.40 pip install PyQt5==5.15.9 pip install opencv-python pip install numpy pandasultralytics版本要谨慎。不同版本的API存在差异,比如早先版本里YOLO类的加载参数和预测参数在字段名上有变动,如果直接拿新版代码去加载旧版训练的权重,偶尔会有版本兼容告警。我测试的时候锁定8.3.40版本,后面所有命令都基于这个版本。
别忘记安装GPU版的PyTorch,这里有个判断方法:在Python里执行import torch; print(torch.cuda.is_available()),输出True说明GPU可用;如果输出False,会上CPU推理,速度感人,主界面出现卡顿基本就是没吃上GPU。
4.2 模型训练与权重导出
数据集目录结构必须严格符合YOLO规范:
datasets/Fish_Dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/用YOLOv11s或者YOLOv11n作为预训练权重开始训练。命令很简单:
yolo detect train data=fisheye.yaml model=yolo11s.pt epochs=100 imgsz=640 batch=16训练完成后,最佳权重保存在runs/detect/train/weights/best.pt。这个文件就是要被UI程序加载的文件。我习惯在训练完后额外做一次验证集推理,把预测结果画出来和真实标注做对比图,快速确认模型不是“只看到鱼头没看到鱼尾”。
4.3 模型集成到UI系统
将训练好的best.pt复制到项目的models/目录下,运行时进行加载。核心推理代码是这样封装的:
from ultralytics import YOLO class FishDetector: def __init__(self, model_path="models/best.pt", conf_thres=0.5, iou_thres=0.45): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect(self, img_bgr): results = self.model.predict( img_bgr, conf=self.conf_thres, iou=self.iou_thres, verbose=False ) boxes = [] for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls = int(box.cls[0]) conf = float(box.conf[0]) boxes.append((x1, y1, x2, y2, cls, conf)) return boxes这里有一个关键的线程问题:推理要放到独立线程。如果在PyQt5的界面回调函数里直接调用detect方法,当视频流30帧每秒地推理时,UI线程会被推理占满,界面拖不动这个完全是意料之中的事情。
正确做法是:界面发出检测信号,一个QThread工作线程去跑推理,推完把结果通过信号槽发回UI线程更新界面。这套源码里已经实现了Worker类,用QThreadPool和QRunnable处理推理任务,我拿到后直接复用这个逻辑,很顺畅。
4.4 完整运行流程实录
我把一套完整流程跑下来的操作串一遍,方便你对照:
- 启动程序,首先弹出登录窗口。
- 点击“注册”,输入预设好的测试账号密码,密码哈希写入SQLite数据库。首次注册时需要在代码或运行目录里确认生成了
auth.db文件。 - 注册完成后切回登录页,输入新注册的账号密码,登录成功进入主窗口。
- 在主窗口左侧点击“选择图片”,从测试集目录里挑一张鱼群密集的图片加载进来。
- 右侧置信度滑杆默认是0.5,先不调整,直接点“开始检测”。等待1到2秒(取决于显卡性能),左侧预览图像刷新,图上出现带类别名的绿色检测框。
- 下方结果表格同步更新,一行一个目标,显示类别名、置信度、坐标。
- 点“导出结果”,生成CSV表格,包含本张图片所有检测信息。
如果这套流程走完没有报错也没卡死,基本就说明系统核心链路通了。
5. 实操中的常见问题与排查实录
5.1 环境与依赖类问题
问题1:ModuleNotFoundError: No module named 'torch'。
原因基本是虚拟环境没激活或者依赖没装全。排查时先在终端里跑一下python -c "import torch",如果报错,说明当前解释器不对,不是IDE选错解释器就是环境激活失败。PyQt程序如果在IDE里启动,要检查IDE配置的解释器路径是不是指向你建的那个虚拟环境。
问题2:PyQt5程序运行后弹出“This application failed to start because no Qt platform plugin could be initialized”。
这是Qt平台插件找不到的问题,常见于Windows下Python多版本共存。解决方法:把PyQt5\\Qt5\\plugins\\platforms目录加入QT_QPA_PLATFORM_PLUGIN_PATH环境变量,或者直接重装PyQt5,让插件路径被正确注册。
问题3:用ultralytics加载权重时提示版本不兼容。
这是因为训练的权重是由不同版本生成。如果是你自己重新训练的权重,记录下训练时用的ultralytics版本,在推理环境里装一模一样的版本即可。如果用的是项目自带的模型文件,按照项目requirements.txt中的版本安装。
5.2 模型精度与检测效果问题
问题1:验证集mAP不低,但实际图片检测效果差。
这种情况十有八九是数据分布差异导致的。验证集和训练集出自同一批拍摄环境,而应用环境中光照、水深、鱼种都变了。解决办法是收集实际场景的少量图片做“测试集外验证”,通过人工标注一小批(几十张),专门跑指标,不要只看原测试集指标。
问题2:鱼群密集时漏检严重。
先把置信度阈值降到0.3,如果漏检明显减少,说明模型本身是学到了特征的,是阈值卡太严。如果降到0.3还是有大量漏检,说明训练数据里密集场景样本太少,需要补充密集重叠样本或调高Mosaic增强权重。
问题3:检测框抖动剧烈。
视频检测时相邻帧框的位置和大小变化过大,主要原因是模型对目标边缘不够稳定。经验方案:对框做时间维度的平滑,引入卡尔曼滤波或EMA。更简单的做法是用track参数:
result = model.track(frame, persist=True)让模型复用跨帧的目标ID,在视觉上能极大改善框跳变的问题。前提是当前帧的前一帧也要送入模型,不能独立推理。
5.3 界面卡顿与响应缓慢问题
如果本机显卡不好,推理速度慢,界面操作就会很卡。建议把视频检测的帧率限制在15帧,也就是抽帧检测而不是逐帧处理,并开启双缓冲队列。之前测试过一个案例:在4K视频上跑YOLOv11s,不开抽帧时主界面完全拖不动;改成每两帧检测一帧,推理时间充裕,界面用起来顺畅很多。
同时,界面刷新要尽量使用QPixmap局部更新,不要每帧重建整个图像对象。绘制检测框用cv2.rectangle处理完之后,整体转换成QImage再显示,这一套流程要保证在40毫秒以内完成,否则视觉上就会明显卡。
5.4 数据与标注问题
有些问题不在代码,在数据标注本身。我遇到过最典型的是:类别名和配置文件对不上。训练时用了5个类别,UI程序里类别名列表只有4个,推理时索引5直接越界。这种错误不会体现在训练loss里,只会在运行时抽风。
排查方式很简单:在model_loader.py里加载完模型后,打印model.names,检查类别字典的长度和顺序是否与预期一致。这个names字段就是模型输出类别ID与名称的映射,UI画框时取类别名全靠它。
6. 系统优化方向与扩展建议
6.1 轻量化部署
YOLOv11s在消费级显卡上跑没问题,但水下场景很多是部署到水下机器人、ROV设备上,算力受限。这时候考虑YOLOv11n加int8量化的组合。ultralytics支持导出ONNX再转成TensorRT或OpenVINO格式,可以显著提速。
我实验过一组对比:YOLOv11s在ONNX Runtime下推理一张640x640图,耗时约45ms。量化为int8后,耗时降至约25ms,精度mAP50下降了约2%,但对鱼类检测这种鲁棒性需求较高的任务,这个下降幅度是可以接受的。
6.2 从检测到追踪计数
鱼类检测只是第一步。渔业监测更核心的需求是“一段时间内有多少条鱼游过”或者“鱼类数量变化曲线”。这就需要跨帧关联目标,也就是多目标追踪。
可以接入ByteTrack或BoT-SORT,以YOLOv11检测结果作为输入,为每个目标分配跟踪ID。鱼群密集时,同一个目标在不同帧可能因为互相遮挡而丢失ID,这需要设计好遮挡后的重识别策略。实测下来,水下场景用ByteTrack加上运动预测,ID切换率比纯IoU匹配低不少。
6.3 数据闭环与模型迭代
模型训练好不等于一劳永逸。水下的光照随水深、季节、海域变化,模型在一个海域表现好,换一个海域可能掉链子。
一个实用的迭代思路是:利用现有模型做“辅助标注”。先把新采集的海量图片灌进模型,让它产出粗糙检测框,再让标注员在粗糙框的基础上修改而不是从零开始画框,能把标注效率提高3倍左右。然后每过一段时间,把新标注的数据混合进训练集重新训练。这套系统里由于有CSV日志和结果导出,可以很方便地把检测结果整理成候选标注集,形成数据闭环。
个人体会与建议
我完整复盘这整套系统后的感受,它把深度学习目标检测从“能跑通”推进到了“可交付使用”的层面,尤其是数据增强、训练分离、UI与模型解耦这些设计,都踩在真实工程需要的点上。对于打算拿它做课题或者扩展成实际项目的朋友,我建议先从自带数据集把全流程跑通,重点花时间理解训练参数对结果的影响,以及UI线程和推理线程怎么配合工作。水下图像比常规图像刁钻得多,多准备一些不同水质的样本,它的表现就会更皮实一些。我后续打算把多目标追踪加进去,再把模型换成TensorRT版本适配嵌入式设备,这套系统的应用空间还会更大。