☰
水下生物目标检测实战:Python+YOLO从数据集到训练全流程
2026/10/5 2:55:55 网站建设 项目流程

简介:一份面向水下生物目标检测的Python深度学习资源,基于YOLO框架与PyTorch环境,适合有基础目标检测知识的开发者和研究者用于学习、训练及快速验证。资源共1830个文件,压缩包约112.1MB,其中包含910张jpg训练图像、448个xml原始标注和453个txt格式标签,以及3份yaml配置、3个py脚本和3个pt权重文件,可满足从数据划分、模型训练到PyQt图形界面识别的一整套流程。yaml文件定义了训练参数与类别信息,pt权重可直接用于推理或迁移学习,csv训练日志则方便对比指标并分析收敛情况。配套提供清晰的文件结构与缓存、预测可视化图片,便于查看训练效果与错误样例。目前已有112人学习下载,尤其适合需要直接体验水下生物检测完整链路并基于YOLO进行二次开发的读者。

1. 水下生物目标检测:从数据集到训练代码,为什么Python和YOLO能直接上手

做水下生物目标检测的团队,十有八九不是搞算法的,而是搞渔业的、搞生态监测的、搞潜水机器人的。你手里可能是一堆水下摄像机拍回来的视频,一个池子几百条鱼,人工看录像数数能数到怀疑人生。这就是这个标题要解决的问题:用Python搭一套深度学习目标检测管线,输入水下图像或者视频帧,输出每个生物的位置框和类别,顺带把数据集和代码都给你备齐,不用自己从零标几百张图。

适合谁?两类人。一类是刚接触目标检测的Python用户,想用现成数据集练手,跑通一个完整的检测流程;另一类是实际项目里被水下数据集搞烦了的工程师——水下数据不比陆地上,光线烂、背景花、目标小,直接用通用模型往往翻车。这个标题的落地路径很清晰:准备数据集、整理标注格式、训练检测模型、评估效果、把模型用到视频推理上。下面我就按这条链路,把能复现的部分一步步拆开。

2. 水下数据集长什么样:标注格式整理与类别筛选

2.1 先认清你要检测的“水下生物”是哪几类

水下目标检测和通用目标检测最大的差异在数据侧。常见的水下数据集包括鱼类、海参、海胆、扇贝、螃蟹等,这些类别在标注时往往存在“同一个东西不同视角完全两个样”的情况:鱼侧视是一条梭形,俯视就是一条细线;海参在泥沙背景上几乎和石头同色。所以拿到数据集后的第一步不是急着训模型,而是把类别清单拉出来,看每类的样本数和标注框质量。

这一步常见做法是用脚本统计标注文件里每个类别的出现次数和标注框面积分布。如果某个类只有几十张图,后续训练基本学不好,要么放弃该类,要么做数据增强补样本。还有一个容易被忽视的点:水下数据集的图像尺寸通常不统一,有些是1920x1080的截图,有些是ROV采集的4K单帧,直接丢给模型会导致训练时按长边缩放产生大量形变。

2.2 VOC格式和YOLO格式互转:标注文件处理脚本

多数公开水下数据集给的是VOC格式的XML标注,而YOLO系列训练需要的是TXT格式,每行一个目标,内容是“类别id x_center y_center width height”,坐标全部归一化到0到1之间。转换时最常出错的不是坐标计算,而是“格式名义上转了,但坐标除以的是原图尺寸,不是缩放后的尺寸”。下面这个脚本我一般直接放在项目根目录,跑完不清理,后续每次重新整理数据都用它。

import os import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo(xml_file, class_names, out_dir): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue # 跳过不在类别清单里的标注 box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 防止标注越界导致训练时loss爆炸 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{class_names.index(name)} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") txt_path = os.path.join(out_dir, os.path.basename(xml_file).replace('.xml', '.txt')) with open(txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) return len(lines) # 用法:把VOC XML目录下所有文件转到YOLO目录 # class_names顺序就是模型训练时的类别id,务必和最终训练配置一致

这段代码的关键有两处:一是class_names列表的顺序直接决定每个类别在模型输出里的id,一旦训练中途改了顺序,之前转好的标注全部作废,这是很多人训练到一半发现loss不降的隐藏原因。二是坐标归一化前做了上下界截断,水下标注经常出现框画到图像边缘外的情况,UDP、YOLO系列训练时遇到负数坐标会直接报错,提前防御比后面排查省事得多。

2.3 数据集划分:不是随机切,而是按“来源”切

很多人在这个环节翻车。水下数据集往往是按视频片段组织的,同一段视频的连续帧高度相似,如果随机划分train/val,那些高度相似的帧会同时出现在训练集和验证集里,验证集mAP虚高。等模型部署到新环境时,面对从没见过的水流、光照、泥沙背景,精度断崖式下跌。正确做法是按视频来源或者采集环境划分:同一个视频片段的所有帧必须进同一个集合。

import os import random from glob import glob def split_by_source(image_dir, label_dir, train_ratio=0.8): # 假设文件名格式: source_123_frame_0345.jpg,按source前缀划分 image_paths = glob(os.path.join(image_dir, '*.jpg')) source_map = {} for img_path in image_paths: filename = os.path.basename(img_path) source_id = filename.split('_')[0] # 按来源前缀分组 source_map.setdefault(source_id, []).append(img_path) train_list, val_list = [], [] for source_id, paths in source_map.items(): random.shuffle(paths) split_idx = int(len(paths) * train_ratio) train_list.extend(paths[:split_idx]) val_list.extend(paths[split_idx:]) return train_list, val_list

我在实际项目中吃过这个亏:第一次训练时mAP到0.92,自己以为稳了,结果放到实拍视频里几乎找不到鱼,后来定位到问题就是数据划分没按来源分,验证集本质上是在“开卷考试”。如果你手头数据来自多个不同的水下摄像头,最好训练前直接打印一下每一帧的来源分布,别急着开训。

3. 模型选型与训练启动:YOLO系列做水下检测的适配经验

3.1 为什么首选YOLO系列而不是Faster R-CNN或DETR

水下生物检测的最大特点是目标多、目标小、遮挡严重。Faster R-CNN在精度上有优势,但检测速度在嵌入式设备上根本跑不起来;DETR这类Transformer检测器对GPU显存要求高,且小目标检测仍处于弱势。YOLO系列(尤其是v8和v11这一代)在速度和精度的平衡上最适合这个场景,而且ultralytics库把数据加载、训练、验证、导出都封装好了,适合0基础纯小白启动。

如果你的机器只有CPU,我也建议先装ultralytics跑通最小流程再考虑要不要换模型;YOLO的CPU训练慢,但能跑,验证数据集准备有没有问题。另一个在选型时要考虑的点是模型体量:水下场景建议优先尝试yolov8s和yolov8m,yolov8n对水下小目标过于勉强,x和l体量在GTX 3060级别GPU上训练时间偏长,日常迭代不方便。

3.2 ultralytics环境配置:Python版本和依赖装完先跑一个最小验证

安装ultralytics需要Python 3.8到3.11之间,太新的Python版本(比如3.12、3.13)会导致依赖的PyTorch轮子不全,很多新手在这一步就卡住了。装完后别急着训练,先跑一个最小推理脚本验证环境没问题:

# 创建环境并安装依赖,国内用户用清华源加速 pip install ultralytics torch torchvision --index-url https://pypi.tuna.tsinghua.edu.cn/simple

装完后跑一段最小推理:

from ultralytics import YOLO # 加载官方预训练权重,第一次运行会自动下载到当前目录 model = YOLO("yolov8n.pt") # 用一张普通图片验证推理流程正常 results = model.predict("test.jpg") results[0].save("output.jpg") print("环境正常,检测到", len(results[0].boxes), "个目标")

这里的逻辑是:先验证模型能跑通,再替换成自己的数据集。如果你的机器下载预训练权重很慢,可以手动下载yolov8n.pt放进当前目录,代码会直接读取本地文件,跳过下载。如果连首张图片推理都报错,多半是CUDA版本和PyTorch不匹配,而不是你的数据有问题。

3.3 数据配置YAML:水下场景最容易配错的三个字段

使用ultralytics训练时,需要一个data YAML文件,里面声明训练集、验证集路径和类别名。水下项目里最常见的坑有三处。第一个是路径写成相对路径,训练时工作目录一动就找不到数据,建议直接用绝对路径。第二个是类别名顺序和转换标注格式时的class_names顺序不一致,模型训练完类别名错位,比如“fish”变成了“sea_cucumber”。第三个是忘记设置val字段,有些版本会默认用训练集做验证,最终打印的mAP完全不可信。

下面是一个标准的水下检测数据配置文件:

# underwater.yaml path: /home/user/underwater_dataset # 数据集根目录,绝对路径 train: images/train # 相对path的路径 val: images/val nc: 5 names: 0: fish 1: sea_cucumber 2: sea_urchin 3: scallop 4: crab

注意事项:nc是类别总数,要和names里的条目数完全一致;names是dict格式不是list,0开头的编号写全,训练时日志会输出每个类别的AP,检查这个输出和你的预期类别是否能对上,这是发现问题最快的方式。

3.4 启动训练的推荐参数组合

YOLO训练可以直接用命令行,也可以用Python脚本。命令行方式适合快速迭代,Python脚本适合你要在训练前后插入自定义复杂逻辑时用。先给命令行版本:

# 用yolov8s预训练权重迁移学习,训练100轮 yolo train model=yolov8s.pt data=underwater.yaml epochs=100 batch=16 imgsz=640 patience=15

如果显存不够,把batch降到8或者4;如果图像里目标较小,把imgsz提到960或1280能带来明显的小目标recall提升,但训练时间相应增长。patience是早停参数,验证集mAP连续15轮不涨就自动停止,能省不少时间。

用Python脚本训练时逻辑会更可控:

from ultralytics import YOLO model = YOLO("yolov8s.pt") # 加载预训练权重 model.train( data="underwater.yaml", epochs=100, batch=16, imgsz=640, patience=15, workers=4, # 数据加载线程数,Windows下建议设为0避免报错 device=0, # 使用第一张GPU;CPU训练填"cpu" pretrained=True, # 迁移学习,保持True,这能大幅提升收敛速度 )

迁移学习对水下这种“类通用但不完全通用”的场景很关键:预训练权重是在ImageNet或COCO上学过的,保留了基础纹理和边缘特征,水下生物虽然外观特殊,但鱼的轮廓、海参的纹理边缘这些低层特征是可以复用的。我自己测试过,从零训练和用COCO预训练权重起步,同样100轮,mAP能差出10个点以上。

4. 训练结果评估:用mAP和PR曲线判断模型好坏,而不是只看loss

4.1 先看PR曲线,再看mAP,最后才看loss

很多人训练完只看最后的loss值,觉得loss低于1就是好模型。这个习惯在目标检测里容易带偏方向。训练完成后,ultralytics会在runs/train/目录下生成results.png、confusion_matrix.png和PR_curve.png。判断模型好坏,我一般按这个顺序来:先看PR曲线是不是整体往右上角顶起来了;然后看验证集上的mAP@0.5和mAP@0.5:0.95,前者反映“检测到大概位置”的能力,后者反映“框得准不准”;最后才看loss曲线有没有持续下降。

水下场景有一个普遍现象值得单独说:mAP@0.5不低,但mAP@0.5:0.95很低,这说明你的框大概位置找到了,但边界框回归不准。原因大多是水下生物的轮廓模糊,标注框本身就因人而异,另一个原因是imgsz=640对小鱼来说像素太少了,框不准很正常。

4.2 三个必看指标的计算和解读

验证输出会在终端打出每个类别的AP,这比总mAP更有排查价值。比如某个类别的AP@0.5只有0.2,别急着调模型,先去看这个类别的训练样本数和标注质量。水下数据最容易出现的问题是某类样本数极少且集中在相似的几张图上,模型学到的只是“记住图片”而不是“学会检测”。

# 用ultralytics的验证接口评估训练好的模型 from ultralytics import YOLO model = YOLO("runs/train/underwater/weights/best.pt") metrics = model.val(data="underwater.yaml", split="val") # 打印每个类别的AP for i, class_name in enumerate(model.names.values()): ap50 = metrics.box.ap50[i] # 每个类别的AP@0.5 ap = metrics.box.ap[i] # 每个类别的AP@0.5:0.95 print(f"{class_name}: AP@0.5={ap50:.4f}, AP@0.5:0.95={ap:.4f}")

这里有个实用技巧:metrics.box.ap50是按类别顺序输出的,顺序和你的data YAML里的names顺序一致。如果输出的类别顺序和你的预期对不上,那不是评估代码问题,是训练时的类别id映射已经乱了,回头去检查数据转换脚本的class_names顺序。

4.3 模型欠拟合还是数据问题:用训练集评估来分辨

有时验证mAP上不去,你不确定是模型没有学会,还是验证集太难。一个简单的分辨方法是:用训练好的模型去评估训练集本身。如果训练集上mAP很高(比如0.9),但验证集很低,这是过拟合,优先加数据增强或降低模型复杂度;如果训练集本身mAP就低,说明模型没学会,优先检查数据标注质量,再考虑加大epochs或调整学习率。

model.val(data="underwater.yaml", split="train")

不需要额外写代码,直接用上面的评估接口。跑一次对比训练集和验证集的mAP差距,这个操作能帮你省掉大量盲目调参时间。我在水下项目里遇到最多的情况是训练集mAP也就0.6左右,说明标注本身问题很大——框和生物边缘对不齐、类别标错、漏标,这些都会把模型上限锁死。

5. 水下检测避坑指南:五个高频问题的现象、原因和解决

5.1 现象:训练时loss正常下降,验证mAP却始终徘徊在0.3

原因:最常见的是数据划分不当。前面提过,水下数据集按视频帧组织,如果随机切分,验证集等于在背题,loss看着正常,但验证集mAP上不去。其次是数据标注框和生物实际边缘偏差过大,模型学的是“大概在这里”而不是“精确在这里”。

解决:先按视频来源重新划分数据,确保划分粒度是source级别;然后随机抽100张训练图片,人和标注框叠加可视化,肉眼检查标注质量。如果发现很多框只框住鱼的一半身体,需要重新标注或用半自动标注工具修正,强行训练只是浪费时间。

5.2 现象:训练到一半报错“CUDA out of memory”,调小batch后仍然报错

原因:大多不是batch太大,而是显存碎片化,尤其是你用多个GPU或者训练中途打开了其他程序。另外workers设置太高也会让显存看起来吃紧,因为数据加载进程会预取图像到显存。

解决:先确保workers=0(Windows下必设),再设batch=4或batch=2测试。如果连batch=2都崩,检查imgsz是不是被设置得过大,水下图像普遍分辨率高,imgsz=1280在消费级显卡上很容易爆显存,降到1024或960通常能解决,同时用yolov8s而不是yolov81。

5.3 现象:模型检测出来的鱼,边框比真实鱼大一圈或者偏移到尾巴上

原因:水下图像中的鱼头和鱼尾特征差异大,标注框如果在不同帧里位置漂移(有的标注从鱼头开始,有的从鱼身中间开始),模型学到的就是一个平均位置,最终输出框会偏大或偏移。另一个原因是对图像做了不合适的Mosaic数据增强,鱼被切到不同图块后,标注信息被扭曲。

解决:检查训练配置里mosaic参数,如果数据集目标较大且你的验证结果偏移严重,把mosaic设为0.0或0.5试试。同时用标注可视化工具检查同一类别在不同帧里的标注一致性,漂移明显的标注帧直接删除,不要带病训练。

5.4 现象:训练正常,单张图片推理也正常,但跑视频时每隔几十帧就漏掉一群鱼

原因:水下视频单帧之间运动模糊严重,鱼游动速度快时,拖影导致目标外形在某一帧和正常形态差异巨大。模型在静态图片测试集上没怎么见过运动模糊样本,一到视频推理就崩。

解决:准备约200-300帧含明显运动模糊的视频帧,用训练好的模型做伪标注(自动标注后人工修正),把这些帧加入训练集重新微调模型。这个技巧我反复用,比调整任何超参数都管用。

5.5 现象:训练数据只有3类鱼,模型却把海藻、石头也框出来

原因:这不是模型的错,是背景中某些纹理和鱼的纹理相似,模型学到了错误的判别特征。水下场景中,海藻的摆动和鱼游动的视频帧,在单帧静态图上有时真的很难区分。这种问题最容易出现在夜间或浑浊水域,因为纹理特征被噪声掩盖。

解决:最直接的办法是收集“负样本”——海藻、岩石、空荡荡的水面、潜水员等,标注为背景类,强制模型学习“这些东西不是目标”。YOLO系列部分版本支持纯背景图片(不含标注的TXT文件)作为额外训练数据,把这些图片放到训练集目录里,对应label目录放空TXT文件即可。

6. 把模型用于视频推理:批量跑水下视频并输出可视化结果

训练完模型后,最常见的落地场景是把模型跑在一个视频上,检测每一帧的水下生物并保存结果视频。这一环节的坑也不少:视频解码速度、目标框抖动、输出视频体积过大。

常见做法是用cv2.VideoCapture逐帧读取视频,对每一帧调用model.predict或model(frame),然后把结果可视化后写入VideoWriter。需要注意一点:model(frame)的返回值是Results对象,真正拿到可视化结果要调用results[0].plot(),而不是直接把它当图像用。

import cv2 from ultralytics import YOLO # 加载训练好的最佳权重 model = YOLO("runs/train/underwater/weights/best.pt") # 打开输入视频 cap = cv2.VideoCapture("underwater_video.mp4") fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 输出视频编码,用mp4v保证兼容性 writer = cv2.VideoWriter( "output_video.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height), ) frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break # 对每一帧做检测,conf指定置信度阈值 results = model(frame, conf=0.35, verbose=False) # plot()返回画好框的图像 annotated_frame = results[0].plot() writer.write(annotated_frame) frame_count += 1 if frame_count % 100 == 0: print(f"已处理 {frame_count} 帧") cap.release() writer.release() print("视频处理完成,输出文件:output_video.mp4")

这段代码的执行逻辑是逐帧推理并写回输出视频,关键的参数是conf=0.35,这个值决定了模型输出的框的最低置信度。水下场景置信度阈值值得反复调:设高了漏检(鱼的颜色和背景融合时置信度天然低),设低了误检(把石头当鱼框出来)。我一般会先用0.25跑一遍看误检情况,再逐渐调高到0.4左右,找到临界点。verbose=False用来关掉控制台的逐帧打印,否则视频跑下来控制台日志刷到怀疑人生。

还有一个细节:写视频时cv2.VideoWriter的fps参数要尽量和源视频一致,否则输出视频的播放速度不对。如果视频分辨率过大,比如4K,逐帧推理时可以先缩小再推理,最后把框映射回原分辨率画出来,这一步对速度提升非常明显,代价是精度略微降低。

找模型阈值时我习惯做一次快速扫描:取视频前300帧,分别用0.2、0.3、0.35、0.4四个阈值各跑一遍,然后对比输出的视频片段,选一个既能抓住目标又不至于背景乱跳的值。这个办法看起来原始,但比纸上谈兵调参快得多,也是我踩过无数坑之后沉淀下来的习惯。如果你刚拿到这个水下检测项目,建议也先跑一段10秒的短视频看看输出效果,等画质和检测结果都满意了,再部署到长视频和实时流上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询