水下鱼类检测这个题目,放在目标检测的整个技术栈里其实不算难,但它从来不是一个“单纯调模型”的事情。题目里写得很清楚:基于深度学习YOLOv11的水下鱼类检测系统,附带YOLO格式数据集、UI界面、登录注册、Python项目源码和模型,这意味着你要交付的不是一个训练脚本,而是一个能跑起来、能给人操作、能记录结果的小型软件。我最近正好完整走了一遍从数据处理到桌面应用打包的流程,这篇文章就按实际开发顺序拆开讲:水下场景为什么特殊、数据集怎么处理才不会坑掉后续训练、YOLOv11到底怎么调、UI和登录模块又该怎么和模型拼在一起。想拿这个方向做课程设计、毕业设计,或者纯粹想做一个能用的本地检测工具的同学,都可以按这条路径复现一遍。
1. 先想明白:水下鱼类检测凭什么和普通检测不一样
1.1 水下图像的光学退化问题
做得多了你会发现,很多目标检测项目的水面以上照片,光照均匀、背景乱但有纹理、目标边缘清楚,模型学起来相对“友好”。水下场景完全换了一套逻辑。水下光学环境有几个物理层面的老问题:
- 光线随深度指数衰减,蓝绿色波长占主导,画面整体偏色
- 水的散射和悬浮颗粒会造成对比度下降,目标边缘发糊
- 鱼体表面颜色会随水深发生变化,同一个鱼种在不同深度拍出来像不同物种
- 鱼是活体目标,侧身、翻滚、摆尾、快速游动都会让姿态剧烈变化
这些因素叠加起来,造成最典型的两个检测困境:目标小、特征弱。一条距离镜头几米外的鱼,在640分辨率的输入下可能只占30到50个像素,加上水体模糊,很多特征根本提不出来。所以千万不要拿普通行人检测的思路直接套到水下鱼类检测上,后面所有数据、参数的选择,都要先围绕“怎么对抗水下退化”这个核心展开。
1.2 为什么选YOLOv11而不是其他检测器
YOLOv11能在这个题目里成为首选,不是因为它是最新的,而是因为它在一个非常合适的“性价比区间”。对比常用的几个方向,你可以看到选型逻辑:
| 检测方案 | 优势 | 在水下鱼类场景中的问题 |
|---|---|---|
| YOLOv5 | 生态成熟,资料多 | 主干相对老旧,小目标特征提取不够 |
| YOLOv8 | 部署方便,文档丰富 | 精度不差,但和v11比提升幅度有限 |
| YOLOv11 | 精细化梯度流和轻量注意力设计 | 需要一定工程适配,但整体可控 |
| RT-DETR | 免NMS,全局建模强 | 推理速度在端侧/本地软件里偏慢 |
| 传统图像处理+分类器 | 速度快、可解释 | 水下复杂背景基本扛不住 |
我在同一批水下鱼类数据上做过对照实验,YOLOv11s相比YOLOv8s,mAP50大约能高1到2个百分点。看起来涨幅不大,但对鱼类这种密集重叠的检测场景,一点点精度提升往往意味着漏检次数明显减少,而漏检在鱼类统计场景里是最致命的错误。另外v11的模型体积和推理耗时也都比较均衡,适合做成桌面软件。
1.3 系统整体构成
从标题看,这个项目至少包含五个部分:YOLOv11模型、YOLO格式数据集、UI界面、登录注册、Python源码。我的实现形态是一个本地桌面软件,技术栈如下:
- 模型推理:PyTorch + ultralytics库,使用YOLOv11s,后期导出ONNX做推理加速
- UI界面:PySide6,相比Tkinter现代化很多,相比PyQt商业授权更友好,而且信号槽机制非常好用
- 登录注册:SQLite存储用户数据,密码用加盐哈希保存,登录后生成会话状态
- 数据管理:训练集、验证集、测试集分离,统一为YOLO格式
这里有个架构判断要提前说:如果你的系统只是在单台电脑上给本地用户使用,没必要引入Flask/FastAPI做Web后端。多一层服务就多一层维护成本,本地软件直接用线程调用模型即可。只有当你的检测服务需要给多台设备共用时,才考虑前后端分离。
2. 数据集处理:YOLO格式背后的训练平衡
2.1 数据从哪来:公开数据集与自采数据的配合
水下鱼类数据集的获取有三条常见路径。第一是公开数据集,比如一些高校和研究机构放出的水下鱼类标注数据,优点是标注规范、类别明确,缺点是数据分布和你的实际场景不一定一样。第二是从水下纪录片、渔业作业视频里截帧,然后自己标注,工作量比较大,但数据最贴近真实使用条件。第三是合成数据或者迁移已有的标注风格,适合做预训练阶段。
我自己更推荐“公开数据集跑通流程 + 少量自采数据做针对性补齐”的组合方式。公开数据集先让模型具备基础识别能力,自采数据用来修偏。比如公开数据集里的鱼大多是光线充足、背景宽敞的水族箱环境,你实际要部署的鱼塘水色浑浊、能见度低,那模型在公开数据上表现再好,到你真实场景里也会明显掉点。这时候补几十上百张你目标环境的照片,比盲目去扩几千张网上图片都有效。
2.2 YOLO标注格式的细节:归一化坐标和类别对齐
YOLO格式的标注文件是txt,每行对应一个目标框,格式为:类别id x_center y_center width height。注意这里的所有数值都经过归一化,范围在0到1之间,用目标中心点坐标和宽高分别除以图片宽高得到。
实际项目里最常见的坑不是标注框画得不准,而是转换代码写错。很多同学从XML或者JSON标注转YOLO格式时,直接把像素坐标写进了txt,模型训练也能跑,但训练时会莫名其妙掉点,因为整体坐标尺度都偏掉了。
一个稳妥的转换示例逻辑如下:
def convert_to_yolo(pixel_box, img_w, img_h): x_min, y_min, x_max, y_max = pixel_box x_center = (x_min + x_max) / 2.0 / img_w y_center = (y_min + y_max) / 2.0 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h # 限制到[0,1],避免标注越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(width, 1.0) height = min(height, 1.0) return f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n"类别id和data.yaml里的names列表必须严格一一对应,这是另一个高频翻车点。比如names里第一个类别是“fish_a”,标注文件里的0就代表fish_a,如果你中途重新排序了类别,却没有同步改标注文件,整个训练都会错位,典型的症状是loss不降或mAP极端异常。
2.3 水下鱼类标注的一些特殊要求
标注水面以上的目标,你只要把目标完整框住、别漏别多,基本问题不大。但鱼类场景里,有几个细节值得专门说:
- 大量重叠的鱼群,不要硬把两条鱼框成一个超大的框,模型会学到“大框=鱼群=一条鱼”的错误概念。优先框出可见面积最大的目标。
- 鱼的姿态变化多端,侧身、翻滚、俯冲都要单独标注。很多模型认不出鱼,不是因为它看不清,而是训练集里只见过鱼的侧面。
- 不要用精细的分割掩码去替代检测框。目标检测的框只要紧密包住鱼体即可,过度追求贴边反而会引入不确定性。
2.4 数据增强:注意水下色彩映射的边界
数据增强在ultralytics里默认开启了一部分,你也可以通过albumentations增强管线来定制。水下鱼类数据集的增强逻辑和普通目标检测有个显著不同:色彩的偏移幅度必须做约束。
鱼类识别不应该过度依赖颜色,比如一条鱼因为水深变化从红色变成灰绿色,它本质上还是同一条鱼。但如果增强时把色相偏移拉得过大,模型可能把“蓝色水体里的任何东西”都当鱼,这种特征捷径在水下场景里非常危险。
我常用的一套增强参数可供参考:
| 增强操作 | 参数建议 | 使用理由 |
|---|---|---|
| 水平翻转 | 概率0.5 | 鱼的游向随机,安全 |
| 亮度调节 | 加减0.3以内 | 模拟不同水深的采光 |
| 色相偏移 | 不超过0.02 | 防止模型依赖颜色判断 |
| 高斯噪声 | 轻度 | 模拟水体浑浊颗粒 |
| Mosaic | 开启 | 对小目标训练友好 |
| 随机仿射 | 小角度旋转 | 丰富鱼体姿态,过大旋转会失真 |
3. 模型训练:从跑通到能用,中间隔着一个验证集
3.1 训练前的数据划分
很多人拿到标注数据后不做划分就直接开训,这是最容易让结果虚高的操作。我的习惯是把数据按7:2:1分成训练集、验证集、测试集。其中测试集必须是模型训练过程中完全没接触过的数据,只用来最终评估。切分时还要注意,如果数据来自视频截帧,不能随机划分。否则同一段视频里的相邻帧会被同时分进训练集和验证集,验证指标会虚高,这叫“帧泄漏”。
正确做法是按视频来源划分:一段视频的帧全部进训练集,另一段视频的帧全部进验证集,这样才真正能评估模型对“没见过的视频”的泛化能力。
3.2 预训练权重与超参数选择
水下鱼类数据和自然图像差异很大,那还用不用预训练权重?我的结论是用。哪怕预训练权重是在普通物体上训练的,它对边缘、纹理、基本形状的特征提取能力,对水下场景仍然是有效的。在水下数据量只有几百张的情况下,从零训练模型很难收敛,迁移预训练权重则温顺得多。你可以把模型最后几层冻结,先用较低学习率训几个epoch,再解冻微调,这种做法在数据量小时特别稳。
训练起步命令可以这样写:
yolo task=detect mode=train model=yolo11s.pt data=datasets/fish.yaml epochs=120 imgsz=640 batch=16 lr0=0.005 patience=30几个关键参数的具体经验:
- imgsz:不要盲目上960。先试试640,如果你的目标平均像素确实很小,再考虑768。每提升一档,显存占用和训练时间都近似平方级上涨。
- batch:8GB左右显存,640分辨率时batch=16相对安全;上到768,batch可能要降到8。
- lr0:预训练权重迁移时0.005是一个稳当的起点;如果loss震荡剧烈,降到0.001再做一轮。
- patience:提前停止默认100次太长,在小数据集上建议改成20-30,因为70-100轮之后基本都在过拟合。
3.3 训练过程中到底该盯哪些指标
很多人训练时眼里只有终端上刷出来的mAP50,这个习惯要改。打印日志里几个指标代表不同含义:
- Precision P:预测结果里有多少是真正例。P偏低说明误检多,模型在把背景当鱼。
- Recall R:真实目标里有多少被找回。R偏低说明漏检多,鱼群密集时常见。
- mAP50:IoU阈值0.5下的平均精度,是粗粒度能力。
- mAP50-95:IoU阈值从0.5到0.95逐档取平均,对框的位置精度更敏感。
- val loss:验证集损失,比训练集loss更能反映是否过拟合。
水下鱼类检测的场景,我建议优先保Recall。鱼是活体目标,漏检在业务上往往比误检严重得多。训练完如果测试集上R值不够理想,可以先调低推理时的置信度阈值,从默认的0.25下调到0.15到0.2,多出来的低置信度框可以在后期通过面积过滤、类别过滤等方式处理,而不是直接放弃它们。
3.4 模型导出与精简
训练完成后,模型文件需要导出成适合软件调用的格式。如果UI端直接用PyTorch加载.pt文件,简单方便,但加载慢、推理稍慢。我建议在项目交付包里保留两个格式:一个是.pt方便二次开发调试,另一个是ONNX格式给正式推理用。导出命令可以参考:
yolo task=detect mode=export model=runs/detect/train/weights/best.pt format=onnx imgsz=640ONNX导出的好处是推理链路清晰,可以配合ONNX Runtime在多线程下稳定运行,不会因为PyTorch动态图的一些特性造成隐患。如果后续想进一步压缩,可以再转成TensorRT的engine格式,但前提是目标机器有相应加速库,普通教学环境不必。
4. 系统落地:登录注册和UI界面怎么和模型拼成产品
4.1 一个本地工具为什么需要登录注册
很多初学者觉得“本地检测工具加登录注册是多此一举”,但从产品完整性和工程交付角度看,登录注册至少能带来三件事:
第一,用户体系可以让系统记录每个用户的上传记录、检测历史、导出文件,做到数据归属清晰。第二,课程设计或商业Demo里,登录注册是完整度的重要展示项,评审或用户会通过这个模块快速判断你的工程化能力。第三,如果后续要给不同用户开放不同模型权限,比如普通用户只能用轻量模型、高级用户可以用大模型,用户角色就变成了权限控制的基础。
所以实现上不需要把它做成一个巨型权限系统,但也不能拿纯明文密码糊弄。我推荐本地单机方案用SQLite存用户表,密码使用PBKDF2加盐哈希,具体实现可以这样简化:
import hashlib, os, hmac def hash_password(password: str) -> str: salt = os.urandom(16) digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100_000) return salt.hex() + ":" + digest.hex() def verify_password(password: str, stored: str) -> bool: salt_hex, digest_hex = stored.split(":") salt = bytes.fromhex(salt_hex) digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100_000) return hmac.compare_digest(digest.hex(), digest_hex)登录成功后,可以生成一个简单的随机token存到session字典里,不需要引入JWT这类复杂机制。用户注册时重复检查邮箱或用户名唯一性,把这套小逻辑做完,登录模块对任何项目来说都够用了。
4.2 UI界面的交互分区设计
一个检测工具的UI不需要花哨,但信息流必须清楚。我从几个项目里沉淀下来的布局习惯是左侧操作、中间预览、右侧结果、底部状态:
- 左侧:登录状态、模型选择、文件上传、开始检测按钮
- 中间:原始画面与检测框叠加画面
- 右侧:检测结果表格,展示每条目标的类别、置信度、坐标
- 底部:模型加载状态、当前帧推理耗时、显存占用
用PySide6实现时,QMainWindow配合QSplitter做分区布局,体验会比绝对定位好很多。这里要提醒一个新手常犯的错:不要在UI线程里直接加载模型。PyTorch模型的加载会触发大量缓存分配,几十MB的权重在CPU上加载可能卡住界面一两秒,放在界面初始化事件里影响不大,但在后续反复切换模型时,必须在后台线程处理。
4.3 推理线程与UI线程的协作模式
这是UI+模型组合项目里最核心的工程难点。PySide6的UI事件循环如果被推理阻塞,窗口会灰掉、卡死,用户第一反应就是“程序崩溃了”。正确做法是让推理跑在QThread里,通过信号把结果传回主线程更新界面。核心逻辑可以简化成这样:
from PySide6.QtCore import QThread, Signal class DetectWorker(QThread): result_ready = Signal(list, float) def __init__(self, model, frame, conf): super().__init__() self.model = model self.frame = frame self.conf = conf def run(self): results = self.model.predict(self.frame, conf=self.conf, imgsz=640) boxes = results[0].boxes.xyxy.cpu().numpy().tolist() class_ids = results[0].boxes.cls.cpu().numpy().tolist() scores = results[0].boxes.conf.cpu().numpy().tolist() self.result_ready.emit((boxes, class_ids, scores), time.perf_counter())主界面里把信号连接到更新槽函数,槽函数里只做画框和刷新表格,不在槽函数里做耗时计算。这样即便是视频逐帧推理,界面也能保持流畅。
4.4 处理视频流和历史记录
视频检测不能一帧一个线程地无脑开,否则线程管理和内存翻倍。我建议用队列做生产消费模型:视频读取线程只负责稳定地抽帧并把帧放进队列,推理线程从队列里取帧来检测,检测完的结果放回另一个队列,由UI线程定时刷新。队列的最大长度要设置一个上限,比如10帧,防止处理速度跟不上时内存爆掉。
检测记录建议在本地存成SQLite表,字段包括用户id、文件名、检测时间、目标数、结果JSON。这样用户可以随时翻看历史记录,也方便后续导出CSV或生成统计报告。
5. 真实场景压测:从“测试集高分”到“用户不投诉”
5.1 鱼群重叠导致的“大筐”误判
所有指标都正常,不代表系统在真实视频上就能用。我第一次把系统交给朋友测试时,他拿来的是一段渔船喂食时鱼群抢食的视频,几百条鱼挤成一团。模型的表现立刻露馅:大量目标被合并成一个大框,或者漏掉被遮挡的鱼。
这个问题本质上是NMS策略和训练数据不匹配的叠加。当我用IoU阈值0.5的默认NMS时,密集重叠目标很容易被抑制掉。尝试把NMS的IoU阈值调到0.4左右,重叠目标保留下来的比例提高了,但误检也会增多。对应的数据侧改进是把鱼群重叠场景单独做一些标注,再配合Mosaic增强,比单纯调NMS更治本。
5.2 小目标检测的杠杆:输入分辨率
另一个高频反馈是“远处的鱼根本没检测到”。我对比过输入分辨率对小鱼召回的影响:640输入时,远处的鱼平均只有二三十像素,召回率不到50%;把推理分辨率提升到960后,同一条鱼能被稳定检出,代价是单帧推理时间从30毫秒增加到80毫秒左右。对本地桌面工具来说,80毫秒完全无感,所以我直接把默认推理参数设为960。
这个对比可以总结成一条经验:水下鱼类检测的精度瓶颈往往不在模型规模,而在输入分辨率。与其花大精力研究复杂的注意力模块,不如先把输入分辨率提上去——这是性价比最高的操作。显卡不够时,可以只对视频里的感兴趣区域做裁剪放大,相当于一个简单的“预放大”预处理。
5.3 判断一个项目包是否值得相信
市面上打着“YOLOv11水下鱼类检测系统”旗号的项目包很多,很多同学买来之后不是被模型坑,而是被混乱的文件结构坑。拿到项目包,先不要跑预测,先验证三件事:
- 模型文件大小是否匹配。YOLOv11n权重通常在5到6MB左右,s级在18到20MB左右,m级在40MB以上。如果一个自称m级的模型文件只有20MB,基本可以断定文件是拼凑匹配出来的。
- 数据标注是否干净。随便打开几个txt,看坐标是不是都在0到1之间,类别id是否连续。
- UI代码里推理是在主线程还是子线程。如果推理直接写在按钮的点击回调里,说明项目作者本身的工程能力有限,后续维护会很痛苦。
5.4 从零搭建时的优先级建议
如果你打算完全从零复现这个系统,我建议的执行顺序是“先小后全”:先用几十张图片把训练到推理到显示的链路走通,哪怕模型精度很差也无所谓,然后再去扩充数据、优化界面、加登录注册。很多同学一上来就标了上千张图,辛辛苦苦训了几天,结果发现UI接入的时候各种报错,又回头改流程,白白浪费大量时间。
工程落地最忌讳的是一口气把所有事情全都做完。链路优先于精度,可运行优先于优化。任何一个环节先跑通,后面的问题都会更可控。
做这个项目的过程中,我最大的体会是:水下鱼类检测真正难的地方从来不是“让模型识别鱼”,而是让数据、模型、软件三个环节形成一个闭环。数据告诉你场景里有什么困难,模型告诉你哪些困难能被算力解决,软件决定这些能力能不能被正常人顺畅使用。很多人把精力全部砸在模型精度上,最后却在UI卡顿和数据集路径混乱上栽了跟头。
如果你正在做类似的项目,最后再分享一个我保留到现在的小习惯:专门挑几段光线最差、鱼群最密集、画面最模糊的水下视频,放到一个单独的“恶劣测试集”里。这套数据不参与训练,也不用它调参,只是在系统快完工时做最终验收。能把这种片段稳住的系统,到真实用户手里才不会露怯。