ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv13俯视舰船检测实战:从数据集构建到训练部署全指南

YOLOv13俯视舰船检测实战:从数据集构建到训练部署全指南 简介目标检测是计算机视觉中的基础任务从通用场景到垂直领域模型的精度与泛化能力往往受限于特征表达和数据分布。在俯视视角下舰船目标因尺寸小、方向任意、背景灰度接近水面成为小目标检测中的典型难题。YOLO系列作为主流检测框架其YOLOv13版本通过跨尺度特征融合增强了对小目标的找回能力配合专门的舰船目标检测数据集进行训练可显著提升港口监控、无人机海事巡查等场景的识别效果。本文围绕俯视舰船检测梳理了数据集标注规范、Labelme/VOC转YOLO格式、按场景划分训练集到YOLOv13/YOLOv10模型选型、训练参数调整以及TensorRT部署避坑的完整链路为工程落地提供可复现参考。1. 俯视舰船检测为什么说一套模型加数据集的打包方案最省事无人机、监控塔和低空巡查这类俯视场景里船目标检测和常规道路目标检测完全不是一个难度等级船头可以朝向任意角度船体和水面的灰度高度接近数百米高空下一条二十米的渔船在画面里只有几十个像素。用COCO预训练的通用模型直接跑结果多半是把船漏成水面噪点。标题这套方案——YOLOv13俯视视角下舰船目标检测搭配训练好的舰船目标检测模型和一份舰船目标检测数据集——解决的就是这个落地断层先直接用现成权重跑通验证确认场景可用再基于配套数据集做增量训练补上自己监控点位的小船和复杂背景。适合刚接触目标检测的工程新人也适合急着交差的港口巡检、河道监控、无人机海事巡查项目组。这篇笔记把数据集格式、两个版本的选型、yolov13训练参数和部署坑点一次讲完每一步都可复现。2. 舰船目标检测数据集准备从俯视影像到YOLO标签的四步处理2.1 俯视视角下舰船标注的三个边界原则俯视影像里标注船和给行人、车辆打框完全是两套逻辑。第一个问题是船体要不要包含船头浪花和尾流。高速快艇在俯视图里船头和尾流连成一条白线如果标注框把整条尾流包进去模型会把水面痕迹学成船。我一般以甲板边缘为基准船体外轮廓为准浪花、尾流、溅水一律不标宁可少包一点也不能让模型把“尾流”当成船的强特征。第二个问题是并排停靠的船怎么切分。港口里多船靠在一起矩形框必然互相重叠这时候不要为了追求“一个框只含一艘船”而把目标裁掉一半。俯视场景的标注优先级是“不遗漏 不严重粘连 严格贴合”相邻船即使框有重叠也要全部标出来漏标一艘会让模型在loss层面直接学到“该处不是目标”比框大一点更伤。第三个问题是你到底要不要上旋转框。长条货船并排靠岸时旋转框比水平框能少包进去30%的背景干扰遥感社区里mmrotate这类旋转框检测器配合DOTA数据集格式处理这种场景确实更好用。但旋转框的标注成本高出一大截部署时转TensorRT也更麻烦。如果手头这份数据是普通水平矩形框先老老实实用YOLO跑不要强行转旋转检测。提示一份能用的舰船目标检测数据集最低标准是500张以上俯视图、2000个以上船目标且其中必须包含一部分“画面里有水、有浮标、但没有船”的负样本。没有负样本的船模型部署时会把所有亮色水纹当船。2.2 标签转换脚本Labelme JSON与VOC XML统一转YOLO txt拿到带标注的数据集第一件事不是训练而是检查标签格式。常见交付格式有两种Labelme导出的JSON和标注工具导出的VOC XML。YOLO训练需要每张图对应一个同名txt文件每行是class_id x_center y_center width height四个值全部用图像宽高归一化。我习惯写一个转换脚本两种格式一次处理import os import json import xml.etree.ElementTree as ET import cv2 def convert_labelme(json_path, out_dir, class_map): 将 Labelme 标注转成 YOLO txt。 取标注多边形所有点的外接矩形作为 bbox。 with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] img_name os.path.splitext(os.path.basename(json_path))[0] with open(os.path.join(out_dir, img_name .txt), w, encodingutf-8) as out: for shape in data[shapes]: label shape[label] if label not in class_map: continue pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h w, h max(w, 1e-5), max(h, 1e-5) out.write(f{class_map[label]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) def convert_voc(xml_path, out_dir, class_map, img_dir): 将 VOC XML 转成 YOLO txt。 bndbox 本身就是矩形直接做归一化即可。 tree ET.parse(xml_path) root tree.getroot() img_name os.path.splitext(os.path.basename(xml_path))[0] img_file os.path.join(img_dir, img_name .jpg) h, w cv2.imread(img_file).shape[:2] with open(os.path.join(out_dir, img_name .txt), w, encodingutf-8) as out: for obj in root.findall(object): label obj.find(name).text if label not in class_map: continue box obj.find(bndbox) x_min float(box.find(xmin).text) y_min float(box.find(ymin).text) x_max float(box.find(xmax).text) y_max float(box.find(ymax).text) x_center ((x_min x_max) / 2) / w y_center ((y_min y_max) / 2) / h bw (x_max - x_min) / w bh (y_max - y_min) / h out.write(f{class_map[label]} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}\n)这段脚本有两个关键点。第一是归一化坐标必须落在0到1之间出现大于1的值要么是标注越界要么是读图尺寸和标注尺寸不是同一张图第二是class_map里没有的杂物标签会被跳过比如“person”“lifebuoy”。这里有一个常见误用把杂物标签全部跳过数据集里就彻底没有“干扰物”的概念了模型自然会把救生圈、浮标当成船。我现在的做法是给常见水面杂物单独建一个background或buoy类别参与训练而不是删掉。转换完必须做一次可视化抽检随机读十个txt把坐标反算回像素在图上画框。这一步能暴露大量问题图片是PNG但XML里写成了jpg后缀、多边形的点顺序错乱导致外接矩形变成长条、Labelme里的imageWidth字段和实际图片宽高不一致这些错误都能让训练阶段数据加载器静默跳过对应样本最后表现为train样本数量比预期少了一截。2.3 按场景划分训练验证集杜绝目标泄漏舰船数据集最典型的问题是目标泄漏。无人机视频抽帧得到的数据连续帧之间高度相关同一艘船会出现在几十帧里。如果只是按单帧随机切分训练验证集同一艘船的相似画面会同时出现在train和val里验证mAP虚高部署后立刻打回原形。划分必须按场景或视频片段做而不是按单张图做。import os import random import shutil random.seed(20240618) img_dir ship_imgs train_dir, val_dir train, val os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) # 假设文件名形如 clip_001_frame_00012.jpg取前两段作为场景id imgs sorted(os.listdir(img_dir)) groups {} for img in imgs: cid _.join(img.split(_)[:2]) groups.setdefault(cid, []).append(img) group_ids list(groups.keys()) random.shuffle(group_ids) val_count max(1, int(len(group_ids) * 0.2)) val_ids set(group_ids[:val_count]) for cid, files in groups.items(): target_dir val_dir if cid in val_ids else train_dir for fname in files: shutil.copy(os.path.join(img_dir, fname), os.path.join(target_dir, fname)) label_src os.path.join(img_dir, os.path.splitext(fname)[0] .txt) if os.path.exists(label_src): shutil.copy(label_src, os.path.join(target_dir, os.path.basename(label_src)))这个脚本按文件名前缀里的场景编号分组同一段视频的所有帧只会进入train或val中的一边。random.seed(20240618)固定随机种子每次划分结果一致评测对比才有意义不会今天能复现明天就变了。划分之后打印两侧的图片数、标注文件数、类别分布三个数对不上就不要开始训练。划分完成数据集的yaml文件这样写path: ./ship_dataset train: train val: val names: 0: ship第一版建议只做ship单类别。多分类比如货轮、渔船、快艇先不要上单类mAP0.5稳定过0.85之后再按船型细分才有价值否则多分类只会同时拖累每一类的精度。3. YOLOv13和YOLOv10怎么选俯视船检测的模型差异与部署场景3.1 两个版本在舰船场景的本质差异俯视船检测选模型不要只看版本号新旧要看两个东西小目标特征的融合强度和预测头对密集目标的处理方式。YOLOv13属于新一代迭代主干和颈部做了更多跨尺度特征复用网络下采样到P3层时几十像素的小船特征已经比较弱跨层融合能把浅层细节和深层语义拼到一起对小目标找回有帮助YOLOv10的优势在检测头端它省掉了传统NMS直接端到端回归最终框部署链路短、推理延迟低但代价是密集并排船场景里相邻目标要靠训练时的匹配策略硬分对标注质量和增强策略要求更高。实际勘测场景里我的经验是两种模型差别在前景和背景高度混淆的近水面。YOLOv13因为特征复用更多对“船体灰度和水面灰度接近”的情况更稳YOLOv10在检测头更轻量适合硬件有限但需要高帧率的视频流。另外一个容易忽略的差异是模型对旋转的建模能力。俯视船方向任意但两个模型用的都是水平锚框角度问题要靠数据增强里的水平翻转和上下翻转来覆盖。如果拍摄是严格的垂直俯拍fliplr和flipud可以同时开如果是倾斜俯视镜像翻转会改变船只受光方向反而教坏模型这类数据就只保留原始角度。3.2 显存、速度与小目标召回对比表模型版本输入尺寸显存占用小目标召回部署便利度典型用途YOLOv10n640最低一般免NMS最方便边缘盒子、低功耗设备YOLOv10m640中中等免NMS监控服务器、视频流服务YOLOv13s640 / 1280中较v10好标准NMS流程无人机航拍单机处理YOLOv13m/x960 / 1280高好标准NMS流程港口全景、高分影像检测上面这张是相对对比具体数字随硬件和batch变化很大不用背。选型时只需要记住一条主线显存带宽紧、要跑实时视频流选YOLOv10n或YOLOv10m精度优先、要找回小目标选YOLOv13s或更大模型。多数舰船项目最后卡在小目标找回率上所以我一般推荐先试YOLOv13s显存不够再降级到YOLOv10m而不是反着来。3.3 三种硬件配置下的选型参考配置一Jetson Orin Nano这类边缘设备8G内存同时还要跑推流和存储任务。选YOLOv10n输入大小固定640INT8量化优先试图数低延时直接对接RTSP流。配置二普通x86服务器带一张12G以上显存的消费级卡。选YOLOv13s训练时imgsz用1280推理时imgsz保持1280这样船的回归精度才够。这个配置是性价比最高的落地点。配置三多路港口全景拼接画面一幅图里有几十条大小不一的船。选YOLOv13m以上且推理阶段配合切块检测不要整图直接丢给模型。全景图的一艘小船在原尺寸下只有十几个像素任何模型都救不回来。3.4 用一个基准脚本快速验证模型差异选型不要凭感觉同一份数据跑个对比基准。下面这段脚本加载两个权重对同一批验证集图片做推理统计检测数量、平均置信度和单张耗时。from ultralytics import YOLO import time test_images val/*.jpg models { v10m: yolov10m.pt, v13s: yolov13s.pt, } for name, weight in models.items(): model YOLO(weight) start time.time() # pred 结果带 boxes 对象可直接读取 conf results model.predict( test_images, conf0.15, imgsz1280, verboseFalse ) elapsed time.time() - start total sum(len(r.boxes) for r in results) confs [b.conf.item() for r in results for b in r.boxes] avg_conf sum(confs) / len(confs) if confs else 0 print(f{name}: 检测框总数{total}, 平均置信度{avg_conf:.3f}, 推理耗时{elapsed:.2f}s)conf0.15是为了把低置信度的小船也捞出来基准测试阶段看的是模型的召回上限不是最终阈值。特别注意average conf这个数如果YOLOv13s的检测框总数比YOLOv10m多但平均置信度低一截说明v13把小船找回来了只是置信度偏低这种模型更值得花时间调阈值如果总数和置信度都低那就是特征融合也没救回来问题出在训练数据而不是模型。4. yolov13训练舰船检测模型最小训练命令与6个必调参数4.1 最小训练命令与参数含义数据集和yaml准备好之后yolov13训练的第一条命令可以这样起yolo detect train \ modelyolov13s.pt \ dataship_dataset/ship.yaml \ epochs120 \ imgsz640 \ batch16 \ device0 \ workers4 \ projectship_run \ namev13_shipsmodelyolov13s.pt用的是预训练的COCO权重作为起点舰船检测新手可以先跑通这条链路再谈优化。imgsz640适合第一轮验证流程batch16在12G显存上通常没问题显存不够就往下减半。project和name定义训练日志和权重输出目录建议每次都换新name不要覆盖上一个实验结果。4.2 舰船场景的增强参数调整第一轮跑通之后把训练命令升级成适合俯视船场景的增强参数yolo detect train \ modelyolov13s.pt \ dataship.yaml \ epochs160 \ imgsz1280 \ batch8 \ device0 \ mosaic0.8 \ close_mosaic10 \ fliplr0.5 \ flipud0.3 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3 \ scale0.4 \ translate0.1逐个说为什么这样调。imgsz1280是针对小目标最有效的手段船的像素面积直接翻四倍代价是训练速度变慢、显存占用升高mosaic0.8保留大部分拼接增强但不要到1.0纯俯视数据里船往往聚集在同一水面拼接太狠会生成“船上岸”的假样本。close_mosaic10表示最后10个epoch关闭mosaic让模型在正常分布上收尾这一项对小船检测尤其重要。fliplr0.5开水平翻转flipud0.3开垂直翻转前提是拍摄接近垂直俯拍。如果是斜俯视监控塔画面这两个值都改成0。hsv_h0.015色相扰动压得很低水面颜色变化本来就大但船体颜色是重要线索色相扰动过猛会让灰船变成红船模型学乱。scale0.4缩放宽放大可以模拟不同飞行高度但不要超过0.5否则船会被缩成几个像素loss被这些极端样本带偏。translate0.1平移扰动模拟目标出现在画面不同位置。4.3 训练日志怎么读Loss下降但mAP不动训练到一半最容易遇到的状况box_loss和cls_loss都在降但mAP0.5:0.95连续20个epoch不动。对小目标舰船场景这是正常现象分类分支学完了回归分支卡在精度边缘。这时候先画曲线别急着停训练import pandas as pd import matplotlib.pyplot as plt res pd.read_csv(ship_run/v13_ships/results.csv) res.columns [c.strip() for c in res.columns] plt.plot(res[epoch], res[val/box_loss], labelval/box_loss) plt.plot(res[epoch], res[metrics/mAP50(B)], labelmAP50) plt.plot(res[epoch], res[metrics/mAP50-95(B)], labelmAP50-95) plt.legend() plt.savefig(train_curve.png)results.csv是训练自动生成的每行一个epoch。看的时候两个指标结合如果val/box_loss仍在下降而mAP不动说明模型还在继续适应目标形状继续训练如果三条曲线都走平mAP还是不涨问题不在训练时长回到数据集里挑出错得最离谱的样本看标注。长条船最常见错误是标注框的y_center偏了半个船身模型回归损失学不进去画出来永远是歪的。4.4 训练收尾与best.pt、last.pt的选择训练结束输出目录里有best.pt和last.pt。默认常识是拿best但对船检测我要多说一句best按验证集mAP挑验证集如果目标较少best往往是过拟合那几十条船的产物。我先看best和last在同一批固定hard test集上的表现用下面脚本判定yolo detect val \ modelship_run/v13_ships/weights/best.pt \ dataship.yaml \ imgsz1280 yolo detect val \ modelship_run/v13_ships/weights/last.pt \ dataship.yaml \ imgsz1280比较两次val输出的prec和recall。best的precision高但recall掉很多说明验证集分布和训练集太接近last往往鲁棒性好一点。实际项目里如果这两版差异巨大说明数据划分有问题回第2章重新按场景划一次而不是在这两个权重之间纠结。5. 舰船目标检测避坑记录从数据集到部署的五个翻车点5.1 训练loss正常下降验证集mAP一直是0现象训练了20个epochbox_loss和cls_loss都正常下降验证集mAP0.5始终是0。原因标签文件与图片没有同步最常见是labelme转出来的txt文件名是图片原名但训练时yaml里train目录用的图片是复制后的新名字还有一类原因是自定义类别ID从1开始写但YOLO的类别索引从0开始所有标签都偏移了一位。解决可视化抽检必须做满三遍。转换后抽10张训练前抽10张训练中第一个epoch结束时再抽一次。检查txt里第一列数字是否都在names范围内、是否落在0到len(names)-1。顺手统计一下训练集图片数和标签文件数是否一致不一致就把多出来的孤立文件清掉。5.2 验证集mAP很高实拍视频漏掉所有小船现象验证集mAP0.5有0.87看起来模型很能打部署到实拍无人机画面后20米以下的小船全部漏检大船能检测到。原因标注和验证集里小船占比高是因为抽帧时小船镜头多但模型在1280输入下能找回的船有像素下限另一个原因是验证集与训练集同源模型记住的是训练帧的纹理不是船的本质特征。解决单独整理一份hard test集专门挑不同水域、不同光线的视频抽帧不参与训练。训练过程中每个epoch都在hard test上跑一遍小鱼小虾捞不出来就是捞不出来。如果hard test上与验证集差距超过15%数据集划分重做按视频场景分组而不是按帧。5.3 并排靠岸的船被合并成一个框现象停靠在一起的三条船模型只出一个框框住整排船。原因相邻船的真实框IoU超过0.5推理阶段NMS把它们合并了。训练时增强数据里也大量存在这种重叠框模型回归出来的预测框天然靠近中心合并趋势被强化。解决推理时把iou0.3调低让NMS合并条件更严格同时把conf0.1适当调低给并排船留出被分开检出的空间。训练端不要做任何改变因为这是推理策略问题不是训练问题。如果调完还是合并说明训练数据里并排停靠的样本不足专门补充这组场景或者考虑后续往旋转框方向走。5.4 转TensorRT后浮标误报暴增现象ONNX在GPU上用float32推理一切正常转了TensorRT INT8之后浮标、白色浪花、救生圈全部变成船小目标召回也下降。原因INT8量化把小目标的高频纹理信息压掉了而水面杂波的灰度分布和船低灰度分布本来就接近量化后更分不开。解决别一上来就INT8先用FP16跑TensorRT精度损失小很多。边缘设备实在要INT8做量化校准数据集时专门挑水面杂物多的帧不要选船多清晰的帧INT8在校准时学到的分布会偏。把校准集背景换杂量化损失能压住一半以上。5.5 多类别训练后货轮和渔船互相混淆现象数据集里有货轮、渔船两类训练结束后两类mAP都不低但实拍时货轮频繁报成渔船。原因类别ID在yaml里定义顺序和标注脚本里的class_map映射不一致。比如yaml里0: cargo, 1: fishing转换脚本里class_map写成cargo: 1, fishing: 0模型学到的类别语义和推理时输出索引对不上整个混淆矩阵都是错的。解决训练结束后先打印验证集预测结果的混淆矩阵而不是只看mAP数字。发现两类混淆异常高回查class_map和yaml的names顺序。这种问题不会报错但会悄悄毁掉多类别舰船检测项目所以我一直建议第一版只做单类ship类别多了以后用一个小脚本统一管理ID映射不靠人工记忆。6. 验证训练好的舰船检测模型用固定测试集跑通推理闭环训练完的模型不能只看验证mAP就交付。我固定用两个手段验证一个固定hard test集跑批处理另一个是单帧阈值扫描。批处理脚本如下from ultralytics import YOLO import glob, os model YOLO(ship_run/v13_ships/weights/best.pt) os.makedirs(result_imgs, exist_okTrue) hard_test sorted(glob.glob(hard_test/*.jpg)) for img in hard_test: r model.predict( img, conf0.15, iou0.4, imgsz1280, saveTrue, save_txtTrue, projectresult_imgs, namerun1, ) print(os.path.basename(img), det:, len(r[0].boxes))参数按部署场景反过来推iou0.4比验证时严格模拟靠岸并排船conf0.15偏低目的是观察模型召回上限部署时再根据误报情况往0.3到0.5提。如果hard test上全部正常才把模型导出成onnx或tensorrt进入部署流程。第二个验证技巧是阈值扫描。对同一段测试视频用conf从0.5到0.05每隔0.05跑一遍统计每档的检测框数量和平均置信度。框数量随阈值下降而陡增的区间就是模型在捞小船的甜点区甜点区附近多噪声就选高阈值部署场景偏看大船就选低阈值。这个表打出来交付时能直接告诉现场人员阈值该设多少而不是让他们自己猜。我的习惯是每训练一版都把hard test跑批、阈值扫描、模型导出三步做成固定脚本换数据、换参数都先跑这套再谈效果。没有稳定评估闭环就调参纯属自欺欺人这种翻车我吃过太多次。评估、记录、再训练把验证沉淀成流程舰船检测项目的上线速度会明显快过“每次从头来一遍”。希望帮到你。本文还有配套的精品资源点击获取
返回列表