简介:面向海洋工程与基础设施巡检场景,这份ultralytics-yolov8水下管道检测识别项目包,集成了基于YOLOv8的检测方案,涵盖标注数据集、训练好的模型与配套使用教程,可帮助巡检人员或CV研究者快速构建水下管道识别流程。压缩包包含2000个文件,以1985个xml标签文件为主,辅以md说明文档、data.yaml配置与必要txt文件,整体大小878.42MB。数据集内置7971张已标注图像,同时提供YOLO格式(txt)与VOC格式(xml)标签,并预先划分好train、val、test子集,附带data.yaml可直接用于YOLOv5/v8/v9/v10/v11/v12系列算法训练;类别仅针对underwater-pipe单类目标,专一性较强。目前已有118人学习浏览,适合水下管道巡检、海洋机器人视觉识别等方向的入门与进阶参考,附带的教程和可视化参考链接也有助于对照验证模型效果。
1. 水下管道检测资源:一份可以直接开工的YOLOv8工程包
水下管道巡检是海洋工程里高频但难落地的场景,难点不在模型结构,而在标注数据。这份资源包的价值恰好落在这里:7971张真实场景图,同时给了YOLO格式(txt)和VOC格式(xml)双份标签,train/val/test已经划分好,data.yaml写好了类别和路径,训练好的权重也附在包里。拿到手不用重新标注,改一下data.yaml里的绝对路径就能跑。适合三类人:海洋工程做视觉检测的工程师、需要真实数据集做课题的学生、想找一个干净YOLO项目完整走通训练到部署流程的初学者。检测类别只有一类,underwater-pipe,针对性极强,海洋工程基础设施巡检是它最直接的应用面。
2. 数据集结构与双格式标签:7971张图的组织方式和转换方法
2.1 目录划分与data.yaml拆解
解压后建议先看整个目录树。标准的项目结构应该是这样的:
project/ ├── data.yaml ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels_yolo/ # YOLO格式txt ├── labels_voc/ # VOC格式xml ├── best.pt # 训练好的模型权重 └── README.mdimages和labels的对应关系要特别关注:如果你看到images/train下面有6000张图,labels_yolo/train下面也必须有6000个txt,一一对应。数量不一致说明标注文件缺失或图片重复,训练时会出现警告。
接下来是data.yaml,这个文件是整个训练流程的入口,Ultralytics框架加载数据集时全靠它。里面通常写着:
# 路径配置 train: /your_abs_path/train/images val: /your_abs_path/val/images test: /your_abs_path/test/images # 类别定义 nc: 1 names: 0: underwater-pipe重点是路径部分。我用过很多次YOLO训练,最常见的问题就在这一段:train和val的路径如果没写全,或者用的是相对路径,训练脚本报出的错误信息又不直观,经常是“train set is empty”之类的提示,排查起来很费劲。我的经验是拿到资源后第一步不是训练,而是先把data.yaml里的路径全部改成当前机器上的绝对路径,然后写一个三行脚本验证一下:
import os import yaml with open("data.yaml", "r") as f: cfg = yaml.safe_load(f) for key in ["train", "val", "test"]: p = cfg[key] print(key, p, os.path.exists(p))每个字段都输出True,说明路径没问题。很多第一次用这个资源包的人卡住的地方都在这里,先从路径下手排查可以节省大量时间。
2.2 YOLO/TXT与VOC/XML的转换脚本
对于很多下游任务来说,标签格式往往决定能否直接复用现有代码。比如有些检测代码读xml、有些读txt,甚至有的工程只接受单个文件。这个资源包给了双份标签,是件好事,但使用的时候还是要注意:双份标签的生成时间可能不完全同步,用之前最好抽查几个文件对比。
我自己做转换时比较喜欢用一段独立的Python脚本。下面是把YOLO的txt转成VOC的xml的代码,假设图像宽度和高度已知:
import os from lxml import etree def yolo_txt_to_voc_xml(img_path, txt_path, xml_out, class_names, img_w, img_h): """ 把YOLO格式txt转成VOC格式xml。 txt每行: class_id cx cy w h,cx/cy/w/h都是归一化到[0,1]的浮点数。 """ root = etree.Element("annotation") folder = etree.SubElement(root, "folder") folder.text = os.path.basename(os.path.dirname(img_path)) or "images" filename = etree.SubElement(root, "filename") filename.text = os.path.basename(img_path) size = etree.SubElement(root, "size") width = etree.SubElement(size, "width") width.text = str(img_w) height = etree.SubElement(size, "height") height.text = str(img_h) depth = etree.SubElement(size, "depth") depth.text = "3" if not os.path.exists(txt_path): return with open(txt_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cx = float(parts[1]) cy = float(parts[2]) w = float(parts[3]) h = float(parts[4]) # 反归一化得到绝对像素坐标 xmin = int((cx - w / 2) * img_w) ymin = int((cy - h / 2) * img_h) xmax = int((cx + w / 2) * img_w) ymax = int((cy + h / 2) * img_h) # 坐标越界保护,VOC对越界框处理不友好 xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) obj = etree.SubElement(root, "object") name = etree.SubElement(obj, "name") name.text = class_names[cls_id] pose = etree.SubElement(obj, "pose") pose.text = "Unspecified" truncated = etree.SubElement(obj, "truncated") truncated.text = "0" difficult = etree.SubElement(obj, "difficult") difficult.text = "0" bndbox = etree.SubElement(obj, "bndbox") xmin_el = etree.SubElement(bndbox, "xmin") xmin_el.text = str(xmin) ymin_el = etree.SubElement(bndbox, "ymin") ymin_el.text = str(ymin) xmax_el = etree.SubElement(bndbox, "xmax") xmax_el.text = str(xmax) ymax_el = etree.SubElement(bndbox, "ymax") ymax_el.text = str(ymax) tree = etree.ElementTree(root) tree.write(xml_out, encoding="utf-8", xml_declaration=True) if __name__ == "__main__": class_names = ["underwater-pipe"] yolo_txt_to_voc_xml( img_path="sample.jpg", txt_path="sample.txt", xml_out="sample.xml", class_names=class_names, img_w=640, img_h=480, )这段代码有几个需要留意的边界:首先是class_names列表,顺序必须和训练时data.yaml里的names一致,否则转换出来的tag名字就是错的;其次是img_w和img_h,如果是从图片读取的,要先用PIL或OpenCV确认尺寸,拿错尺寸转换出来的坐标会整体偏移。
2.3 标注质量的快速体检
接手别人标注的数据集最怕的是标签噪声,水下场景尤其明显:管道被泥雾遮挡、目标太小、漏标、误标。在开始训练前,可以先做一次快速统计:
import os label_dir = "labels_yolo/train" total_boxes = 0 empty_files = 0 for filename in os.listdir(label_dir): if not filename.endswith(".txt"): continue path = os.path.join(label_dir, filename) with open(path, "r") as f: lines = [line for line in f.read().strip().split("\n") if line.strip()] total_boxes += len(lines) if len(lines) == 0: empty_files += 1 print(f"total boxes: {total_boxes}, empty files: {empty_files}")如果空文件比例超过3%,说明标注阶段有过漏标,建议先补标或把空文件对应的图片挑出来检查。另外,统计框的宽高分布会很有帮助:大部分框如果宽度小于图像宽度的1/20,说明目标是典型的小目标,训练时imgsz可以考虑加大到768甚至896。
3. 用YOLOv8训练水下管道模型:环境搭建、参数设定与训练闭环
3.1 环境安装与依赖版本选择
Ultralytics YOLOv8训练依赖的核心库是ultralytics和PyTorch。很多人直接pip install ultralytics报错,最常见的错误是could not find a version that satisfies the requirement ultralytics,这通常是pip源问题或Python版本过低。我一般建议用conda新建一个干净环境:
conda create -n yolo python=3.10 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralyticstorch版本必须和CUDA驱动匹配。cu121对应CUDA 12.1,如果你的显卡驱动只支持CUDA 11.x,装上去会报驱动不匹配。新驱动基本都支持12.x,但如果用的是GTX 1660 Ti这类老卡,建议改用cu118或干脆装CPU版本,否则训练速度极慢且不稳定。
装完ultralytics之后跑一条命令验证:
python -c "from ultralytics import YOLO; print(YOLO.__name__)"如果能输出YOLO说明环境没问题。常见的一个坑是OpenCV的头文件版本和torch冲突,直观表现是import ultralytics时直接段错误,这时需要重新装opencv-python-headless版:
pip uninstall opencv-python opencv-contrib-python -y pip install opencv-python-headless3.2 训练参数与启动命令
环境没问题后,把data.yaml修改好,直接开始训练:
yolo detect train data=/path/to/data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 device=0参数解析:
- data:指向上一步修改好的data.yaml,建议使用绝对路径。
- model:如果想从头训练,用yolov8n.yaml;如果想用预训练权重做迁移学习,用yolov8n.pt。通常用.pt比.yaml效果更好,因为COCO预训练权重提供了底层特征。
- epochs:150是单类任务的合理值,mAP曲线一般第60-90轮开始收敛,但数据量很大时可能100轮还不够。
- imgsz:输入分辨率。水下管道检测时目标可能很小,把imgsz从640提高到768能提升小目标召回率,代价是训练时间和显存上升约50%。
- batch:取决于显存。一张8G显存的卡跑yolov8n+imgsz=640,batch最多16;换成yolov8m只能跑到4。建议打开amp混合精度训练,默认是开启的,不需要额外配置。
GTX 1660 Ti跑yolov8的常见问题是显存只有6G,我尝试过batch=16直接OOM。把batch降到8、加上amp,可以把150个epoch跑完,单epoch时间大约2分钟,总共5小时左右。
训练过程中如果想实时看损失函数曲线,Ultralytics默认会在runs/detect/train/expXXX目录下生成results.png,包含loss、precision、recall、mAP的曲线。嫌内置图不够细的话,可以开启tensorboard回调:
yolo detect train data=data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 project=runs name=exp1 plots=Trueplots=True会额外输出混淆矩阵、F1曲线等,分析类别不平衡和漏检原因时非常有用。
3.3 训练日志阅读与权重选择
训练结束时,看几个关键文件:results.png和weights/best.pt、weights/last.pt。怎么读懂results.png里的六条曲线?三点经验:
- train/loss不断下降是必须的,如果train/loss在早期就不降,多半是学习率或数据问题。
- 对比val/loss和train/loss:val/loss如果出现持续上升而train/loss还在下降,就是过拟合信号;单类检测任务数据量足够大,过拟合风险相对低,但也不是没有。
- precision和recall两条曲线是此消彼长的,不要单看一路。mAP50-95比mAP50更能反映模型对边界框质量的把控能力。
选权重时我的习惯:先看mAP50-95的最高点,定位是哪个epoch,然后去weights目录里找对应epoch的权重文件。Ultralytics只保留best.pt和last.pt,best.pt就是验证集上mAP50-95最高时保存的权重。如果只是快速验证,用best.pt就行;如果要继续训练,best.pt和last.pt都可以做起点,我一般用last.pt继续训练,因为它在训练轨迹的最末端,继承性更顺。
4. 用训练好的模型做推理:单图、批量、视频与性能调优
4.1 单图与批量推理代码
推理是所有用户最关心的环节:模型训练好了,怎么拿它去检测。Ultralytics封装得很简单,几行代码就能跑:
from ultralytics import YOLO model = YOLO("best.pt") # 先确认类别映射正确,避免names索引错位 print(model.names) # 单张图片推理 results = model.predict("test_images/sample.jpg", conf=0.35, iou=0.45) # 批量推理,source可以是目录 results = model.predict("test_images/", conf=0.35, iou=0.45, save=True, project="output", name="run1") for r in results: # 每个r对应一张图的结果 for box in r.boxes: xyxy = box.xyxy.cpu().numpy()[0] conf = box.conf.cpu().numpy()[0] cls = int(box.cls.cpu().numpy()[0]) print(f"class={model.names[cls]}, conf={conf:.2f}, bbox={xyxy}")参数解释:
- conf是置信度阈值,水下图像环境复杂,建议先用0.3试跑一次效果,漏检多就降到0.25,误检多就升到0.5。
- iou是NMS的IoU阈值,默认0.45比较好用,目标密集时可以降到0.35。
- save=True保存标注框的图片,save_txt=True保存每个目标的坐标。
- project/name控制输出目录,默认是runs/detect/predict。
使用这个模型时要记住:模型只检测underwater-pipe这一类目标,如果图片里出现其他类型的管道或结构物,它不会给出任何预测框。遇到这种情况不用担心,说明模型精度集中且不会误检其他类别。
4.2 检测结果结构化输出
如果你需要把检测结果做成json或csv方便后续处理,可以用下面的方式提取:
import json results = model.predict("test_images/", conf=0.35, iou=0.45) output = [] for idx, r in enumerate(results): img_id = f"image_{idx}" detections = [] for box in r.boxes: x1, y1, x2, y2 = box.xyxy.cpu().numpy()[0] conf = float(box.conf.cpu().numpy()[0]) detections.append({ "bbox": [float(x1), float(y1), float(x2), float(y2)], "confidence": conf, }) output.append({"image_id": img_id, "detections": detections}) with open("detections.json", "w") as f: json.dump(output, f, indent=2)这样输出的json可以直接接入后续的业务逻辑。很多做海洋工程系统的人会把检测结果接进数据库或监控大屏,json格式比yolo默认的txt更容易解析。
4.3 视频与实时检测
如果要检测水下视频或实时摄像头画面,YOLOv8也做了封装:
yolo detect predict model=best.pt source=video.mp4 conf=0.3 iou=0.45 save=True视频推理能直接输出带框的视频,但有两个需要提前处理的问题。一个是帧率和时长保持一致:源视频fps如果是30,输出也应该是30,否则视频播放速度看起来不对。另一个是处理性能:如果用CPU推理,高清视频大概率跑不到实时帧率。我的处理方式是限制推理分辨率,把视频帧缩放成640后再送进模型,保存时再映射回原始坐标。
4.4 推理速度优化
推理速度决定了这个模型能不能用于实时巡检。三个优化手段按性价比排序:
第一,半精度推理。默认模型加载是fp32,改成fp16能省一半显存和不少时间:
model = YOLO("best.pt").half()第二,批量推理。如果是离线分析一批图片,把batch_size设成8或16,吞吐量能提升两倍以上,注意batch值不要超过显存容量。
第三,如果项目部署在边缘设备上,比如RK3588这类ARM平台,PyTorch模型跑不动实时,必须导出ONNX再转成RKNN格式,转换之后推理速度能快一个数量级。这部分内容放到第6章细说。
5. 水下管道检测避坑:五个最容易翻车的地方
5.1 data.yaml路径写错导致训练集为空
现象:训练启动后,terminal打印的train/val图片数量均为0,或者显示“WARNING no train images found”,训练无法正常进行。
原因:data.yaml里的train/val/test路径是写死的绝对路径,但把项目复制到了另一台机器上后路径失效了;也有可能是相对路径在当前工作目录下解析不到正确位置。
解决:用编辑器打开data.yaml,确认train、val两个字段指向实际存在的图片目录。最好用Python验证一下:
import os import yaml cfg = yaml.safe_load(open("data.yaml")) for key in ["train", "val", "test"]: p = cfg[key] print(key, p, os.path.exists(p))如果返回False,就把路径改成当前机器的绝对路径。这个操作看起来基础,但训练启动报错时第一个检查的就是它。
5.2 显存不足直接OOM
现象:启动训练不到10秒就报CUDA out of memory,程序终止。
原因:batch值超过显卡显存能容纳的范围,尤其是GTX 1660 Ti这类6G显存的卡,batch=16必爆。
解决:先把batch降到8,同时确认amp=True(默认开启)。如果还报OOM,把imgsz降到480,或者换用yolov8n这种最小体积的模型。amp混合精度是最省事的手段,训练速度和显存占用优化非常明显。
5.3 class id与names顺序错乱
现象:推理时打印出的类别名是数字0而不是underwater-pipe,或者标签名和实际目标对不上。
原因:推理代码里用model.names去索引类别名,但模型文件里的names定义可能保留着COCO预训练的默认类别,加载后没有覆盖。
解决:在推理前强制把model.names改成正确的映射:
model = YOLO("best.pt") model.names = {0: "underwater-pipe"}如果想彻底解决,在训练脚本里把data.yaml中的names设置为与基础数据集一致,训练出的模型就会自带正确的names。
5.4 训练epoch过多导致过拟合
现象:train/loss持续下降,但val/loss在第90轮后开始反弹,precision不再上升,mAP50-95震荡。
原因:训练轮数太多,模型开始记住训练集中的细节噪声,对验证集泛化能力反而下降。
解决:把epochs降到120,或者观察results.png,如果val曲线连续5个epoch没有下降就手动停止,用best.pt作为最终产物。单类检测场景,数据量接近8000张时150epoch基本在第80-100轮就已经最优,后面的震荡不会带来收益。
5.5 水下偏色导致的推理误检
现象:在外场水槽或真实海域拍摄的照片上,模型把水草、缆绳、石头检测成underwater-pipe,而且置信度不低。
原因:训练集里图片光照条件、水色、浑浊度过于一致,模型学到的特征偏向色彩而不是纹理结构。水下单通道图像天然偏蓝绿色,模型容易把色彩通道当成强特征。
解决:在训练时做数据增强,尤其是色调、饱和度、亮度的随机调整,让模型学会不完全依赖颜色。推理端,可以先把输入图片做白平衡预处理再送进模型:
import cv2 import numpy as np def white_balance(img): result = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) avg_a = np.mean(result[:, :, 1]) avg_b = np.mean(result[:, :, 2]) result[:, :, 1] = result[:, :, 1] - ((avg_a - 128) * (result[:, :, 0] / 255.0) * 1.1) result[:, :, 2] = result[:, :, 2] - ((avg_b - 128) * (result[:, :, 0] / 255.0) * 1.1) result = cv2.cvtColor(result, cv2.COLOR_LAB2BGR) return result预处理后再送入模型,误检率会明显下降。
6. 效果验证与部署选型:mAP指标、ONNX导出与硬样本验收
6.1 用独立test集打分
经过训练和推理调参后,模型是否达到可用水平,需要用独立的test集来评估。不要用val集代替test集,因为val集在训练过程中参与过权重选择,有过拟合嫌疑。这个资源包已经把test目录单独划分好,直接用:
yolo detect val data=data.yaml model=best.pt imgsz=640 split=test输出结果是一个包含Precision、Recall、mAP50、mAP50-95的表格。对水下管道检测来说,mAP50-95达到0.7以上,Precision和Recall都在0.8以上,基本上就能满足大多数巡检需求。
6.2 导出ONNX并确认精度
如果模型要部署到边缘设备或服务器端,把PyTorch权重导出成ONNX是一个标准步骤:
yolo export model=best.pt format=onnx imgsz=640 dynamic=False导出后用onnxruntime加载ONNX模型,和PyTorch结果做一次逐框对比,确认坐标和置信度没有明显差异。输出文件会生成best.onnx,可以直接被ONNXRuntime、TensorRT或者RK3588的RKNN工具链消费。需要注意:ONNX推理时输入图像预处理(BGR转RGB、归一化)必须和训练时保持一致,否则精度会打折。
6.3 硬样本人工验收
指标之外,我最后一定做一次硬样本人工抽检。从test集里挑出三类图片:水下浑浊度高的、管道与背景纹理相似的、目标非常小的,各抽10张,用模型检测并保存结果。检查重点不是精确的mAP分数,而是边界框是否贴合目标:框是否偏上、偏下、框了一半。凡是模型在这种极端样本上明显翻车的,我不会直接部署,会回退到数据增强或在训练集中补充这类样本重训。
从那以后我每次做水下目标检测项目都强制走一遍这个流程:先跑test集拿指标,再导出onnx做部署验证,最后人工看30张挑出来的硬样本。指标达标但硬样本翻车的模型不上线。希望帮到你。
本文还有配套的精品资源,点击获取