水下生物目标检测实践:基于YOLO的完整训练与PyQt推理链路
简介一套基于深度学习的目标检测方案专为水下生物识别场景设计面向希望动手实践YOLO的Python学习者与开发者可用于水下生态监测、水产养殖、海洋科研等场景中的目标检测任务。压缩包共包含1830个文件以910张水下生物图片、453个文本标注和448个XML标注为主体另附YAML配置、Python脚本、预训练权重、训练结果图表及日志文件整体大小112.1MB。其中三个Python脚本分别承担划分数据集、启动训练和PyQt可视化识别任务——01文件将原始数据转为YOLO格式并生成训练验证划分与数据配置02负责模型训练03则提供加载图片并一键检测的交互界面便于直接体验完整流程。资源已吸引111人学习内置预训练权重可直接调用也可自行重新训练适合作为水下目标检测实战演练与快速部署的参考。1. 先拆开这个水下生物目标检测项目Python、深度学习与YOLO到底装了什么拿到这个标题多数人第一反应是「水下生物目标检测又是哪个实验室的论文复现吧」。实际拆完这套基于Python深度学习的目标检测资源我才发现它比想象中务实得多——它不是一个只剩readme的空壳而是一个已经训练出权重、带完整数据集和推理界面的YOLO项目训练产物如results.csv、val_batch1_pred.jpg都在压缩包里躺着。也就是说你下载后可以立刻用PyQt界面跑识别也可以自己从零训练一遍。对刚接触目标检测的人来说这份资源最大的价值不是「看代码」而是「能跑起来」——环境装好、数据划分脚本跑完、训练命令敲下去一条链路是通的。对有经验的从业者值得看的是数据集怎么组织、训练参数在哪调、以及PyQt界面和YOLO推理是怎么衔接的。这个项目基于PyTorch环境依赖在requirement.txt里列好建议先用Python 3.8以上的环境装依赖别急着直接跑03pyqt.py。2. 数据准备才是第一道门槛01划分数据集.py如何把图片转成YOLO格式2.1 为什么目标检测项目必须先划分数据集很多第一次玩YOLO的人拿到一个检测项目第一件事就想跑训练结果报错「找不到labels」。原因很简单YOLO系列尤其是YOLOv5/v8训练时需要的不只是jpg图片还需要每一张图对应的txt标注文件txt里每一行代表一个目标框格式是「class_id x_center y_center width height」其中四个坐标都是归一化到0~1之间的浮点数。这套水下生物检测项目里01划分数据集.py干的就是这件事。它会把你原始的图片数据集转成YOLO格式的txt标注同时生成train.txt和val.txt以及训练用的配置文件data.yaml。为什么要单独做这一步因为原始数据集可能是VOC格式xml标注或者别的自定义格式YOLO不认必须统一转换。2.2 脚本逻辑拆解从原始标注到train.txt和val.txt以常见做法来说这类脚本的核心流程是扫描图片目录、读取原标注、计算归一化坐标、写txt文件、按比例划分训练集和验证集、最后生成data.yaml。下面是我按这个项目常规实现还原的脚本结构import os import random import yaml # 原始图片目录和标注目录 image_dir datasets/images label_dir datasets/labels_raw # 原始标注可能是xml或txt train_ratio 0.8 # 训练集比例 # 遍历所有图片提取文件名不含后缀 all_images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(all_images) # 打乱顺序避免类别分布不均 train_count int(len(all_images) * train_ratio) # 划分训练集和验证集 train_list all_images[:train_count] val_list all_images[train_count:] # 生成YOLO格式的txt标注 def convert_to_yolo(img_name, out_dir): img_path os.path.join(image_dir, img_name) label_src os.path.join(label_dir, img_name.replace(.jpg, .txt)) img_width, img_height get_img_size(img_path) # 读取图片宽高 with open(label_src, r) as f_src, \ open(os.path.join(out_dir, img_name.replace(.jpg, .txt)), w) as f_out: for line in f_src: # 原始标注格式class_id x_min y_min x_max y_max class_id, x1, y1, x2, y2 map(float, line.strip().split()) # 转成YOLO归一化格式 x_center ((x1 x2) / 2) / img_width y_center ((y1 y2) / 2) / img_height w (x2 - x1) / img_width h (y2 - y1) / img_height f_out.write(f{int(class_id)} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n)这段代码里最关键的参数是train_ratio 0.8意味着80%的图片进训练集、20%进验证集。如果你数据集比较小比如只有几百张建议改成0.9让训练集更充足。convert_to_yolo函数里计算坐标那四行是核心很多新手会忘记除以图片宽高导致训练时边界框全偏移这是一个非常经典的错误。2.3 data.yaml和train.txt的生成细节脚本最后会生成data.yaml这个文件是训练时告诉YOLO「去哪找数据、有几类目标」的配置文件。标准内容大致是train: train.txt # 训练集图片路径列表 val: val.txt # 验证集图片路径列表 nc: 3 # 类别数量根据你的数据集修改 names: [fish, crab, sea_turtle] # 类别名称列表注意train.txt和val.txt里存放的是图片文件的绝对路径每行一张图YOLO训练时会根据路径去找对应的txt标注。有些版本YOLO只用data.yaml里的train字段指向图片目录即可但像这个项目这样单独给txt列表的方式也常见运行环境支持就行。数据划分跑完后检查一下随机打开一个生成的txt标注文件看看坐标范围是否都在0~1之间如果有大于1或小于0的值说明转换逻辑里有坐标计算出错需要回头查原始标注格式。2.4 划分数据集的避坑类别不均衡和标注错位我实测中发现这类划分脚本最隐蔽的问题不是转换代码而是「标注和图片对不上」。比如原始数据集里图片是jpg但标注文件可能是png后缀的图片对应的xml或者图片文件名里有特殊字符导致img_name.replace(.jpg, .txt)匹配不到标注文件。跑完脚本后统计一下每张图片是否都有对应的txt文件数量不等就要查原始数据。另一个坑是random.shuffle后面没有设置random.seed导致每次运行划分结果不同复现实验时对不上结果。建议在脚本开头加上random.seed(42)固定随机种子。3. 训练这一步没那么玄02train.py的参数与训练产物解读3.1 训练脚本启动前必须确认的三件事运行python 02train.py之前先确认三件事否则大概率训练中段报错。第一PyTorch版本和CUDA是否匹配这个项目基于Python PyTorch环境如果GPU是30系以上显卡建议CUDA 11.x以上第二data.yaml里的nc是否和你的标注类别数一致经常有人标注里写了class_id 2yaml里却写的nc: 2训练时索引越界直接崩第三预训练权重是否存在如果没有train脚本通常会自动下载但国内网络环境下载可能会失败常见做法是手动下载yolov5s.pt放到项目根目录再在脚本里指定weights yolov5s.pt。启动命令很简单python 02train.py默认情况下YOLOv5会以yolov5s.pt为起点做迁移学习训练100个epochbatch size是16图片输入尺寸是640x640。训练过程中终端会打印每轮的loss、mAP、P、R等指标。这个项目的水下生物检测场景我建议epoch可以调整到150因为水下图像噪声大、目标纹理模糊需要更多迭代让模型拟合特征。3.2 训练产物怎么读results.csv和val_batch图片训练完成后回到项目根目录你会看到多出几个文件和压缩包里初始就有的results.csv、val_batch1_pred.jpg形成对照。results.csv每一行对应一个epoch里面列了train_loss、val_loss、metrics/precision、metrics/recall、metrics/mAP_0.5等指标。读这张表的技巧看val_loss是否持续下降并趋于平缓如果训练到一半val_loss开始反弹上升说明过拟合了早停是明智的选择。看mAP_0.50.5是IoU阈值水下目标检测能做到0.7以上就算不错的结果。val_batch1_pred.jpg是验证集第一批图片的预测可视化框和标签都画上去了用它做第一轮效果判断比纯看数值更直观。在results.csv里你还会看到labels.jpg这个文件——它把所有训练图片的真实目标框叠加显示在一张图上x轴是归一化坐标。这张图能帮你快速发现标注问题如果某个类别目标的中心点全部集中在图片边缘说明标注时框选有系统性偏移。3.3 训练超参数调整的边界学习率和batch size这个项目默认参数能顺利跑通但效果不一定最优。水下生物检测有个特点目标尺度和清晰度变化很大小鱼小虾可能只有几十个像素。遇到这种情况把输入分辨率从640提升到960可能有帮助代价是显存占用暴增。改法是在训练脚本里找到img_size参数把它从640调成960。对应地batch size需要从16降到8否则显存溢出是必然的。学习率方面默认的0.01对迁移学习是合理的但如果从头训练不用预训练权重建议降到0.001否则loss容易发散。我自己跑这类水下项目时一般会先小batch size跑5个epoch试水观察loss曲线是否稳定下降再拉长到完整训练。这一步成本很低却能提前发现数据标注问题——很多时候loss不降不是模型问题是标注里有脏数据。3.4 显存不足不是脚本问题四件事按顺序检查训练到一半报CUDA out of memory是家常便饭。第一步看GPU占用用nvidia-smi查一下是不是有其他进程占着显存第二步降batch size从16降到8再不行降到4第三步把img_size从640降到416检测精度损失不算太大但显存占用直接少一半第四步检查是否在训练脚本里同时加载了多个模型有的项目会在验证时重复加载模型导致内存堆积。如果你只有6G显存上面的组合拳打下来基本能跑通。4. 从权重到界面03pyqt.py的推理链路和PyQt可视化4.1 PyQt界面不只是花花架子它解决了「模型怎么给非技术的人用」的问题很多检测项目的交付形态就是一个命令行脚本输入图片路径、输出结果图片。但这个项目的03pyqt.py做了个带界面的推理工具点击「加载图片」按钮选择本地图片再点「检测」按钮界面上直接显示检测结果和置信度。它的价值在于你在实际项目中用户不会去看终端输出他们要的是「点一下、出结果」。整个推理链路是读取图片 → 预处理resize到640x640、归一化 → 加载训练好的权重 → YOLO前向推理 → 后处理NMS去除重复框 → 把检测框画到原图上 → 在PyQt的QGraphicsView或QLabel上显示。4.2 核心推理代码拆解界面代码一般较长核心推理部分通常长这样import torch from models.experimental import attempt_load from utils.general import non_max_suppression # 加载训练好的权重 weights_path runs/train/exp/weights/best.pt model attempt_load(weights_path, map_locationtorch.device(cpu)) # CPU推理时指定 def detect_image(img_path): img0 cv2.imread(img_path) # 原始图片 img letterbox(img0, new_shape640)[0] # 保持宽高比缩放 img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGBHWC转CHW img torch.from_numpy(img).float() / 255.0 # 归一化到0~1 img img.unsqueeze(0) # 增加batch维度 with torch.no_grad(): pred model(img)[0] # 前向推理 pred non_max_suppression(pred, conf_thres0.25, iou_thres0.45) return draw_boxes(img0, pred) # 在原始图上画框这段代码要注意两个参数conf_thres0.25是置信度阈值低于这个置信度的框会被过滤当检测结果太少时比如水下生物比较小、颜色和背景接近建议往下调到0.15多放出一些候选框。iou_thres0.45是NMS的IoU阈值两个框重叠面积超过45%时保留高置信度那个。如果场景里大量目标聚集比如鱼群这个值可以调到0.5减少误删。attempt_load加载的是best.pt而不是last.pt——训练过程中会保存两个权重文件last.pt是最后一个epoch的权重best.pt是验证集mAP最高的权重。推理时务必指定best.pt。4.3 CPU推理还是GPU推理的取舍水下目标检测项目如果在GPU上推理一张640x640的图大概几十毫秒如果是CPU可能要100~300毫秒取决于CPU型号和图片数量。03pyqt.py默认会使用CUDA如果可用否则回退到CPU。我的习惯是代码里加一行torch.cuda.is_available()的判断为了保证界面流畅如果检测图片尺寸很大比如水下相机拍的4K图先做一次降采样再推理出水下生物的框后映射回原图坐标。否则在CPU上一张大图能卡好几秒用户会怀疑程序挂了。4.4 PyQt界面资源管理的隐藏坑resources.py和界面UI文件是配套存在的。如果你改了界面布局通常用pyuic5 xxx.ui -o xxx.py重新生成Python文件但注意这个项目里的界面代码可能有一层封装不要直接改自动生成的代码应该改.ui文件再重新生成。还有一点PyQt加载的图片如果路径包含中文或者空格cv2.imread会读取失败返回None代码里优先处理这个情况。界面里显示结果图片时记得把OpenCV的BGR格式转成RGB再设置到QLabel上否则颜色会偏蓝偏暗看起来像调色错了。5. 翻车记录环境配置与目标检测复现中常见的几个坑5.1 现象python 01划分数据集.py跑完train.txt是空的原因脚本遍历图片目录时用的后缀名是.jpg但你的数据集里是.png或.jpeg遍历没抓到任何文件后面所有逻辑都空转。解决打开脚本找到后缀名匹配那行改成同时支持多种格式extensions (.jpg, .jpeg, .png, .bmp) all_images [f for f in os.listdir(image_dir) if f.lower().endswith(extensions)]这类问题本质上是数据集来源杂用爬虫或者相机导出的图片格式不统一先统一图片格式再跑脚本是最省事的办法。我一般会先跑一个find . -name *.png -o -name *.jpeg统计一下确认图片格式分布再动脚本。5.2 现象训练时loss是nan原因有三个高频来源一是学习率过大模型参数直接爆炸二是标注txt里出现了0值坐标或者大于1的坐标标签数据异常三是数据增强时如果输入图片没有归一化到0~1数值范围跨度太大。解决先把学习率降到0.0001再检查txt标注文件里是否有非数字字符最后确认代码里img / 255.0这一步有没有被改掉。实践中我发现很多新手跑飞是因为pip安装的OpenCV版本和PyTorch在图像读取上的数值范围不一致读取时是0~255的整数直接喂给模型而忘了除以255。5.3 现象检测框偏移严重框的位置和物体对不上原因数据集划分脚本里转换坐标时用的图片宽高是原始图片的尺寸但训练时YOLO会把图片resize到640x640如果原始标注坐标没有归一化到0~1之间resize之后框就偏了。解决检查生成的txt标注文件确认所有坐标值都在0~1之间。如果发现x_center大于1说明转换时除的是目标宽高而不是图片宽高回去改代码里x_center ((x1 x2) / 2) / img_width这行。这个坑我在第一次跑YOLO的时候踩过浪费了一整天在调训练参数结果问题出在前处理。5.4 现象03pyqt.py能打开但点检测就闪退原因通常是没有加载到模型权重best.pt路径写错导致attempt_load返回空模型后续推理时访问模型输出直接报错。解决在界面上检测按钮的点击事件里加一行打印输出weights_path的实际值确认它指向的文件存在。另一种情况是界面线程直接执行了模型推理模型加载耗时数秒导致界面无响应看起来像闪退。改进做法是用QThread把推理放到后台线程界面不卡死完成后再通过信号把结果传回主线程。5.5 现象同一个模型在不同图片上一类目标检测稳定、另一类几乎检不到原因数据集中类别分布严重不均衡。比如这个水下生物项目里海龟图片有500张但螃蟹只有50张mAP会被海龟拉高模型实际对螃蟹的检测能力很弱。解决回看labels.jpg和类别分布统计给少数类做数据增强翻转、亮度扰动、马赛克增强或者直接增加采集数据。如果数据实在拿不到可以考虑用class_weights参数给少数类更高的损失权重这在YOLOv5里通过在训练脚本中设置--cls 0.3间接实现让分类损失对少数类更敏感。6. 验证模型能不能用从上一节说的val图片到bad case分析训练完不要急着把模型交出去先跑一轮系统验证。我的习惯是找一批训练时没见过的水下图片不同水域、不同光照条件批量放进检测脚本里把检测结果按「正确检测」「漏检」「误检」三堆分好。漏检集中的场景多半是「目标过小」和「目标与背景纹理相近」——水下生物的保护色非常影响检测器这种情况下需要针对性补数据单纯调置信度阈值是治标不治本。误检的典型情况是「把水草、气泡当成生物」处理方式是在后处理里加过滤条件比如鱼的宽高比通常在0.3~3.0之间超出范围就可以当作非目标过滤掉。在YOLO的后处理代码里加一个长宽比过滤逻辑并不复杂# 在NMS之后对每个检测框做长宽比过滤 boxes, scores, classes pred for i, box in enumerate(boxes): w box[2] - box[0] h box[3] - box[1] if w / h 0.2 or w / h 5.0: # 极端长宽比大概率是误检 continue这类启发式过滤要根据你的具体场景调整阈值。水下海龟的长宽比接近1:1而水草碎片的长宽比往往很极端单独过滤长宽比就能减少一大部分误检。我做过一次教训深刻的验证模型在训练集上的mAP到了0.85我当时觉得稳了结果拿到真实水域测试mAP直接掉到0.5。原因不是模型问题而是训练图片大多是实验室水箱拍的背景是玻璃和灯管而现场图片是昏暗的河底泥沙背景。从那以后我每次交模型的验证流程都强制走一遍「真实场景采样 随机选图 坏case统计」这个习惯让我避开了至少三个项目的返工。这个水下生物检测项目也一样别只看压缩包里的val_batch图片效果可以强烈建议拍一段你自己的水下视频抽帧测试后再下结论。希望这套「划分数据 → 训练 → PyQt推理 → 验证」的链路和踩坑记录能帮你省下几个周末的查错时间。本文还有配套的精品资源点击获取