简介:面向yolo系列算法研发的钓鱼与垂钓行为数据集,共含九百零二张带标签图像,适合研究人员、算法工程师以及计算机视觉学习者用于训练、验证与评估钓鱼检测模型。压缩包内共两千个文件,主要包括一百九十五张jpg原始图像、九百零二份yolo格式的txt标签、九百零二份voc格式的xml标签以及一份yaml配置文件;其中txt标签采用类别、归一化中心坐标与宽高的标准写法,xml标签与yolo标签分开保存于不同目录,使用者可根据不同训练框架灵活选用。数据集已经完成训练集与验证集的科学划分,可直接供yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流模型开展训练、验证与测试,省去自行标注与整理数据的麻烦。资源包整体大小约为三十七兆,已有三百零三人学习下载,特别适用于垂钓行为识别、水域安全监控、渔业管理等场景的快速算法验证与项目部署落地。
1. 为什么说“钓鱼行为检测”卡你的不是网络结构
拿到这个 yolo算法-钓鱼-垂钓行为数据集-902张图像带标签.zip,先别急着解压。这个标题看起来像个标准的“YOLO目标检测数据集”:902张图、带标签、zip打包,解压后理论上就能丢进yolov8训练自己的数据集。但“钓鱼/垂钓行为检测”和行人、车辆检测有一个本质差别:你要检的不是一类形状稳定的物体,而是人、鱼竿、鱼线、水面共同构成的行为场景。网上讲yolo算法讲解ppt能给你网络结构图和指标曲线,真落地时卡住你的反而是鱼竿太细、钓鱼人太远、902张图到底扛不扛得住训练。下面这些内容的目的是陪你把这902张图的每一张都榨干,从解压一路跑到部署前。
2. zip解压后先别急:902张图的目录结构与标签体检
压缩包拿到手,第一件事不是写训练脚本,而是搞清楚两件事:标签是什么格式,标注质量能不能用。这两个问题决定你后面是“直接开跑”还是“先写两小时转换脚本”。很多下载过数据集的人都踩过同一个坑:解压后以为images和labels一一对应,结果跑训练时一半图片找不到标签,程序也不报错,只是默默跳过。902张图如果少了100个标签,训练出来的模型对那类场景就是瞎的。
2.1 先看清labels是txt还是xml,这决定你要不要写转换脚本
解压这个zip包,我一般会在Linux或macOS上执行:
unzip -O gbk yolo算法-钓鱼-垂钓行为数据集-902张图像带标签.zip -d fishing_dataset-O gbk是给Windows压缩的中文文件名解乱码用的,如果你当前系统是Windows,直接右键解压即可。解压完先不要急着打开图片,先看目录结构:
find fishing_dataset -maxdepth 2 -type d常见的数据集布局有两种:一种是images/配labels/,另一种是JPEGImages/配Annotations/。前者大概率是YOLO格式的txt标签,后者有极大概率是Pascal VOC的xml格式。用一条命令确认标签扩展名:
from pathlib import Path root = Path("fishing_dataset") for sub in ["labels", "Annotations", "annotations"]: p = root / sub if p.exists(): exts = {f.suffix for f in p.iterdir() if f.is_file()} print(sub, exts)这段脚本会打印每个标签目录下出现的扩展名集合。看到{'.txt'}就是YOLO格式,看到{'.xml'}就是VOC格式,看到{'.json'}则可能是COCO格式。这三种格式的后续处理差别很大:YOLO txt里面存的是归一化的class_id x_center y_center width height,VOC xml存的是像素坐标xmin ymin xmax ymax,COCO json则是一个大文件加id字段关联。网上很多现成脚本能互相转,但转换前必须知道源头格式。
提示:如果
labels目录里既有txt又有xml,说明这个数据集的标签是半手工整理出来的,别直接用,先跑一遍2.2的统计脚本。
2.2 用一个小脚本统计目标数、类别与长宽比
确定标签格式后,先跑一次目标统计。902张图的数据集,标注框数量、空标签数量、小目标占比,这些数字直接决定你的训练策略。写一个简单的统计脚本:
from pathlib import Path labels_dir = Path("fishing_dataset/labels") imgs_dir = Path("fishing_dataset/images") label_files = list(labels_dir.glob("*.txt")) img_files = list(imgs_dir.glob("*")) empty_frames = 0 total_boxes = 0 tiny_boxes = 0 ratios = [] for lb in label_files: lines = lb.read_text(encoding="utf-8", errors="ignore").strip().splitlines() if not lines: empty_frames += 1 for line in lines: parts = line.split() if len(parts) != 5: continue _, _, _, w, h = float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) total_boxes += 1 ratios.append(w / h) if w * h < 0.005: # 框面积小于图面积的0.5% tiny_boxes += 1 print(f"图片文件数: {len(img_files)}") print(f"标签文件数: {len(label_files)}") print(f"空标签数: {empty_frames}") print(f"目标总数: {total_boxes}") print(f"小目标占比: {tiny_boxes / max(1, total_boxes) * 100:.1f}%")这段脚本会告诉你三个关键风险。第一,图片和标签文件数如果对不上,说明有漏标或空标签的图,训练时要决定是丢弃还是保留空图当负样本。第二,小目标占比如果超过30%,你后面必须考虑提升imgsz或者做滑窗切片,否则学习到的特征对远处的垂钓者毫无反应。第三,ratios里如果出现大量大于5:1或小于0.2的极端值,说明标注框里有大量细长目标——典型的鱼竿框,这类框在YOLO的anchor匹配阶段容易被当成背景。
2.3 把标注画回原图,900张图足够肉眼抽检
统计脚本只能发现数值问题,标注框到底包得准不准,必须肉眼抽检。写一个画框脚本,把YOLO格式的坐标还原成像素框:
import cv2 from pathlib import Path def draw_yolo_boxes(img_path, txt_path, class_color=(0, 0, 255)): img = cv2.imread(str(img_path)) if img is None: return None h, w = img.shape[:2] if not txt_path.exists(): return img for line in txt_path.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: continue _, xc, yc, bw, bh = map(float, parts) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), class_color, 2) return img img_path = Path("fishing_dataset/images/0001.jpg") # 换成实际文件名 txt_path = Path("fishing_dataset/labels/0001.txt") result = draw_yolo_boxes(img_path, txt_path) cv2.imwrite("check_0001.jpg", result)画出来的图建议重点看三类样本:目标在画面边缘的、目标尺寸小于32×32像素的、有水草或栏杆遮挡的。钓鱼行为检测里最常见的问题不是框错,而是只框了鱼竿没框人,或者把整个场景连水面一起框进去。后一种框虽然面积大,但学习到的特征是“有水的地方”,部署到无水场景立刻失效。画框这一步,我建议你每类场景至少抽30张图看一眼,不要只看前10张——数据集经常按场景排序,前10张往往是同一个监控角度的连续帧,不具有代表性。
3. 处理数据集用于yolov8训练:格式统一与细长框修正
数据集如果是干净的YOLO txt,这章你只需要看3.3的数据划分;如果带的是VOC xml,恭喜你,手动活开始了。处理数据集用于yolov8训练,核心原则只有一条:标签文件必须和图像文件同名同目录映射,且坐标必须严格落在图像边界内。违反其中任意一条,训练过程都可能“翻车”——表现为loss正常下降但验证集mAP奇低,你查不到原因,只能一张张翻标签。
3.1 XML转YOLO txt:别把class_id算错
VOC xml转YOLO txt的脚本网上到处都有,但大多数人照抄时都会漏掉两步:类别去重和size校验。先写一个脚本打印xml里所有的name去重结果:
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("fishing_dataset/Annotations") names = set() for xml_file in xml_dir.glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.findall("object"): names.add(obj.find("name").text) print(sorted(names))打印出来的类别列表决定了你的cls_map怎么写。常见情况是只有fishing一个类,有时会出现person和rod分开标。这两种做法的训练目标完全不同:单类学的是“这个画面里有人钓鱼”,双类学的是“哪里有人、哪里是鱼竿”。如果你只关心垂钓行为,我建议把person和rod合并成同一个fishing类,但前提是它们的边界框都完整包住了对应目标。接着执行转换:
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("fishing_dataset/Annotations") txt_dir = Path("fishing_dataset/labels") txt_dir.mkdir(exist_ok=True) cls_map = {"fishing": 0, "person": 0, "rod": 0} # 合并成单类 for xml_file in xml_dir.glob("*.xml"): root = ET.parse(xml_file).getroot() size = root.find("size") im_w, im_h = int(size.find("width").text), int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in cls_map: 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 / im_w y_center = (ymin + ymax) / 2 / im_h w = (xmax - xmin) / im_w h = (ymax - ymin) / im_h lines.append(f"{cls_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") txt_path = txt_dir / (xml_file.stem + ".txt") txt_path.write_text("\n".join(lines))这里最关键的参数是cls_map。如果你把rod标成1、person标成0,训练时模型会拼命区分“人”和“鱼竿”,但由于鱼竿太细,这个区分任务几乎无解。合并成单类后,模型学的是“这些像素组合起来像钓鱼场景”,反而稳定。另外注意im_w和im_h取自xml里的size,如果这个值和图片真实尺寸不一致,转换出的坐标全会偏。所以转换完要抽一张图,拿2.3的画框脚本再验证一次。
3.2 修正越界框:clip前先算中心点
VOC转YOLO时最常见的坐标问题是越界。图片是600×800,标注的xmax却到了850,这类错误通常来自标注工具的缩放bug,或者标注时图片被拉伸过。YOLO格式虽然允许归一化坐标后框的中心点超出边界,但训练时目标会被剪裁,导致学习到的特征残缺。修正逻辑不是简单地把w裁到图像宽度,而是先夹住边角再重新计算中心点:
def clip_yolo_box(xc, yc, bw, bh): x1 = max(0.0, xc - bw / 2) y1 = max(0.0, yc - bh / 2) x2 = min(1.0, xc + bw / 2) y2 = min(1.0, yc + bh / 2) if x2 <= x1 or y2 <= y1: return None new_xc = (x1 + x2) / 2 new_yc = (y1 + y2) / 2 new_w = x2 - x1 new_h = y2 - y1 return new_xc, new_yc, new_w, new_h这段代码的核心逻辑是:先把框的四个边角限制在[0,1]区间内,再反推中心点、宽和高。中心点必须从夹住后的边角计算,而不是直接夹住原始中心点,否则框会整体漂移。我见过有人写xc = min(1.0, max(0.0, xc)),这样修完后框虽然不越界了,但位置偏了半个身子,训练出来检测框总是偏向目标一侧。
另外提醒一句:钓鱼场景的鱼竿框特别容易出现一种“假越界”——鱼竿斜伸出画面,框的一个角在画面外。这种框修正后面积会缩小,但如果你用的是2.3的脚本抽查,会发现修正后的框仍然包住鱼竿的可见部分,这样就够了。不要试图让框完整包住画面外的鱼竿,那是不可能的。
3.3 按场景划分train/val,而不是纯随机
902张图的数据集,train/val划分看起来是个不起眼的步骤,但划分策略直接决定你验证集数字可信不可信。纯随机划分在大多数数据集上没问题,但垂钓监控数据有个特点:连续帧高度相似。同一监控点位、同一时段采集的图片,背景几乎一样。如果这些相似帧被同时分到train和val,模型相当于提前看到了答案,val mAP会虚高至少10个百分点。
我一般按“场景文件夹”划分,而不是按文件随机打散:
import random from pathlib import Path img_root = Path("fishing_dataset/images") scenes = {} for img in img_root.glob("*"): scene_key = img.name.split("_")[0] # 假设文件名前缀是场景编号,实际按你的命名规则改 scenes.setdefault(scene_key, []).append(img) val_ratio = 0.15 rng = random.Random(42) val_imgs = [] train_imgs = [] for scene_imgs in scenes.values(): rng.shuffle(scene_imgs) n_val = int(len(scene_imgs) * val_ratio) val_imgs.extend(scene_imgs[:n_val]) train_imgs.extend(scene_imgs[n_val:]) print(f"train: {len(train_imgs)}, val: {len(val_imgs)}")这个脚本按文件名前缀把同一场景的图聚在一起,再从每个场景里抽15%进验证集。如果你的数据集目录本身就是按场景分文件夹的,直接把scenes换成子文件夹名。这样划分的train和val在背景分布上更接近真实部署时遇到的新场景。902张图至少要保证val里有120张以上,否则mAP的置信区间太宽,一个epoch的随机波动都能让它跳3-5个点。
划分完成后,别忘把train和val的图片分别拷到images/train和images/val,标签也同步拷贝。很多人在这一步只拷图片忘了拷标签,训练时yolov8的data.yaml里配好了路径,结果图像全找到了,标签全是空的,训练出来的模型对任何目标都输出空检测框。
4. 用yolov8训练钓鱼检测模型:最小命令与4个必调参数
图片和标签都准备好了,接下来就是正式用yolov8训练自己的数据集。这一章不讨论网络结构内部怎么算的,只看落地时要做什么。yolov8训练自己的数据集这件事,本质上是三个文件的事:一个数据配置yaml、一份命令、一套参数。把这套流程固化下来,以后换任何数据集都是同一套操作。
4.1 目录结构与一份minimal的fishing.yaml
训练前把目录整理成yolov8最容易识别的方式。我常用的结构是:
fishing_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── fishing.yaml注意labels目录必须和images目录平级,yolov8会自动把images替换成labels去找标签。如果你把标签放在其他名字的目录里,必须在yaml里显式指定,否则它只会傻傻地去images的兄弟目录找。
fishing.yaml内容如下:
path: ./fishing_dataset train: images/train val: images/val nc: 1 names: ['fishing']path是相对路径,建议不要写绝对路径,尤其是这个zip包名字里带中文和特殊符号。有人把这个yaml放在项目根目录,path直接写./fishing_dataset,跑起来没问题;有人图省事把图片放在桌面中文路径下,结果训练时cuda报错路径找不到——不是yolov8不支持中文路径,而是底层依赖库对编码的处理不一致,后面避坑章会细说。nc是类别数,names是类别名列表,顺序必须和标签txt里的class_id严格对应。如果你用的是单类,names里只有fishing,所有txt里默认都是0开头,这两者一旦错位,模型训练时会把标签当成错误样本丢掉。
4.2 最小训练命令与4个必调参数
训练命令可以很简短,先跑通流程再调优:
yolo detect train data=fishing.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0这一行命令会先下载预训练权重(如果本机没有缓存),然后在runs/detect/下生成一个实验目录。第一次训练建议用yolov8n.pt起步,这个模型只有约300万参数量,902张图半小时内能跑完一个完整的epoch周期,适合快速验证数据管线和标签质量。等确认一切正常,再换yolov8s.pt或yolov8m.pt找精度上限。
训练前必须先确认四个参数,这几个参数在钓鱼检测场景里尤其关键:
| 参数 | 建议值 | 说明 |
|---|---|---|
| imgsz | 640起步 | 显存够就上1280;鱼竿细长目标在小分辨率下只有1-2像素宽 |
| batch | 16 | 显存不够时优先降imgsz而不是降batch,否则BN统计量不稳定 |
| mosaic | 0.5 | 默认1.0会把细长目标拼碎并缩小,钓鱼场景背景相似反而帮倒忙 |
| patience | 30 | 902张图很容易过拟合,早停30个epoch能防止后段震荡浪费时间 |
mosaic在yolov8里默认是1.0,也就是每张训练图都由4张图拼接。这个增强对通用目标检测很有效,但对鱼竿这种细长目标是个灾难:拼接时每张子图被缩小到一半甚至四分之一,鱼竿变成一条1像素宽的斜线,模型学到的是“斜线”而不是“鱼竿”。我建议显式设置mosaic=0.5,保留一部分干净样本。完整的调参命令长这样:
yolo detect train data=fishing.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 mosaic=0.5 patience=30 project=runs/fishing name=exp1 device=04.3 中断恢复与训练过程的三个风险点
训练中途断开是常态,尤其是用远程服务器跑数据集的场景。yolov8支持恢复训练,只要训练过程正常退出时last.pt保存过:
yolo detect train resume=True这个命令会自动找最近一次训练的实验目录。如果实验目录被手动移动过,可以指定model=runs/fishing/exp1/weights/last.pt。恢复训练时要特别注意:data.yaml的参数必须和中断时一致,否则会自动重新划分数据集,之前的epoch学习率状态全乱。
训练过程中有三个风险点,我建议每20个epoch看一次runs/fishing/exp1/下的曲线图。第一,train loss下降但val mAP长期不动,先考虑是不是mosaic太大或lr0不合适,而不是盲目加epoch。第二,val mAP在前30个epoch冲到高点然后开始锯齿轮动,这是典型的过拟合信号,902张图训练集太小,epoch超过80后模型开始记背景纹理。第三,loss曲线出现断崖式跳水,排查是不是有标签文件损坏导致某张图被跳过。这几个风险点的共同特征是:损失指标看起来正常,但可视化输出完全不能看。所以不要只盯loss,定期跑一次验证集推理看真实效果。
5. 避坑指南:钓鱼数据集训练最常见的5个坑
把训练流程跑通之后,真正的麻烦才开始显形。下面这五个坑在垂钓行为这种小目标、细长目标、小数据集组合场景里几乎必现,按出现频次从高到低排列。
5.1 类别ID错位:names顺序和txt里的0/1对不上
现象:训练loss正常下降,验证时mAP@0.5也能到0.7以上,但打开验证集的PR曲线,发现精确率在置信度0.2附近有一个断崖式下跌,大量检测框被标记为错误的类别。
原因:data.yaml里的names列表顺序和txt标签里的class_id对不上。比如标签txt里0代表fishing,但yaml里写的是names: ['rod', 'fishing'],0对应rod。yolov8的损失函数不会报错,它只会把“渔竿”的标签按“钓鱼”去学,两个类特征混淆在一起。
解决:训练前写一个五行脚本,把标签里出现的所有class_id打印出来,和names列表做映射核对。902张图的数据集,class_id只应该出现你定义的几个整数,一旦出现越界值,说明xml转txt时映射表写错或者某个标签文件损坏。我的习惯是在训练脚本里加一段断言,凡是出现未知class_id直接抛异常,而不是让训练带病跑完100个epoch。
5.2 细长鱼竿漏检:置信度阈值和imgsz怎么配合
现象:模型在验证集上mAP不错,但用摄像头实拍时,画面里的鱼竿完全没有框;把同一帧截图调低置信度阈值后,又能看到微弱的检测框。
原因:鱼竿在640分辨率下通常只有1-2像素宽,yolov8n的特征图最深层下采样到20×20左右,这个级别的感受野对细长目标不敏感,模型输出的置信度普遍偏低。默认推断阈值conf=0.25会把这些低置信度框全部过滤掉。
解决:推理时把置信度阈值降到0.05,并配合NMS参数调整:
yolo predict model=runs/fishing/exp1/weights/best.pt source=test_video.mp4 conf=0.05 iou=0.5 imgsz=1280这行命令会把输出置信度阈值降到0.05,同时把输入分辨率提到1280。如果换成1280后漏检明显减少,说明模型本身学到了鱼竿特征,只是分辨率不够。如果1280下依然漏检,就要考虑把训练时的imgsz也提到1280重新训练一轮。注意推理分辨率可以和训练分辨率不一致,一般推理设为训练的1.5倍以内效果最好,过大会引入大量插值噪声。
5.3 902张图过拟合:增强策略与backbone冻结
现象:训练集loss降到0.03,验证集mAP却在50个epoch后开始下滑,F1曲线峰值越来越低。可视化验证集预测结果,模型把地面纹理、水面波纹都检测成目标。
原因:902张图对“人+鱼竿+水面”这个组合来说太小。模型在训练后期开始记忆背景纹理,而不是学习目标结构。尤其鱼竿框内有大片水面背景,模型会误认为“蓝色区域”是正样本特征。
解决:先冻结backbone训练10个epoch,再解冻全部微调。yolov8的freeze参数可以指定冻结前几层:
yolo detect train data=fishing.yaml model=yolov8n.pt epochs=100 freeze=10 mosaic=0.5freeze=10表示冻结backbone的前10层,让模型先训练检测头,把已经预训练过的特征提取器保留下来。同时开启适当的颜色增强:hsv_h=0.015 hsv_s=0.5,让模型对水面颜色变化更鲁棒。不要开mixup,这个增强方式对细长目标的边缘特征干扰极大。
5.4 zip伪加密与损坏包:不是每层压缩包都能直接解
现象:解压时提示End-of-central-directory signature not found,或者解压到一半报CRC校验错误。文件名带中文的zip包还可能解出来全是乱码目录。
原因:数据集zip包的压缩层数通常不止一层,有时外层的zip是伪加密——它只是设置了加密标志但没有实际加密内容,普通解压工具会要求输入密码。另外下载中断会导致zip的中央目录损坏,标准的unzip会直接罢工。
解决:先判断是不是伪加密,用7-Zip打开zip包,如果能直接看到文件列表且能拖出来,说明只是zip伪加密,直接用7-Zip解压到目标目录。中央目录损坏的zip包别急着扔,用zip -F尝试修复:
zip -F damaged.zip --out repaired.zip修复后解压出来优先校验图片文件数是不是902张。我遇到过修复后图片文件都在但标签目录少了一个子文件夹的情况,所以解压后务必跑一遍2.2的统计脚本,确认标签文件和图片文件数量一致再进入训练流程。
5.5 中文文件与路径:Windows上好好的,Linux上找不到图
现象:在Windows本地解压后训练正常,把整个fishing_dataset目录传到Linux服务器后再跑训练,报Image not found,但图片明明就在目录里。
原因:zip包里的中文文件名在Windows解压时用的是GBK编码,Linux服务器默认UTF-8读取,文件名里的中文变成了一串乱码。yolov8的图片加载器找不到路径,就会静默跳过,最终导致训练时有效图片数远小于902。
解决:在任何平台解压后,立即把文件名统一改成纯英文和数字:
import re from pathlib import Path for sub_dir in ["images", "labels"]: for p in Path(f"fishing_dataset/{sub_dir}").glob("*"): if p.is_dir(): continue new_name = re.sub(r"[^\w.]", "_", p.name) if new_name != p.name: p.rename(p.with_name(new_name))这段脚本把所有非字母数字下划线的字符替换成下划线,把中文、空格、括号全部清理干净。我当时在这个问题上浪费了一个晚上,最后发现罪魁祸首是数据集里有一张图片名称里带着全角括号。训练脚本查不到文件时会跳过而不是报错,所以检查日志里image count是否等于902,是最快的定位方式。
6. 模型出来先别信mAP:漏检回放与两阶段验证
训练完拿到best.pt,先不要看mAP数字,直接跑一次验证集推理,把漏检图回放出来看。
yolo val model=runs/fishing/exp1/weights/best.pt data=fishing.yaml运行结束后打开runs/detect/val/下的PR_curve.png和F1_curve.png。重点看F1曲线的峰值位置:如果峰值在conf=0.3附近且F1值高于0.75,说明模型学到的是有效特征,阈值选在峰值即可;如果F1峰值低于0.6,提高置信度阈值只会让漏检更严重,这时候要回数据而不是调阈值。PR曲线如果在小recall区间精确率就开始下跌,说明模型把大量水面背景误判为目标,需要补充负样本或重新检查标签框是否包进了太多背景。
6.1 用PR曲线和F1曲线判断该调数据还是调阈值
曲线图呈现在你面前的时候,判断逻辑要清晰:调阈值只能解决“检测到了但置信度低”的问题,解决不了“压根没检测到”的问题。如果F1曲线的峰值出现在低置信度区域,比如conf=0.1,说明所有框的置信度都被压低,这是特征学习问题,要回去调训练参数;如果峰值出现在0.5以上,说明模型区分得很自信,实际部署时可以直接把阈值设到0.5,减少误报。我一般会在验证集上多测几个阈值点:0.1、0.25、0.5,把每个阈值下的精确率和召回率记下来,选精确率大于0.8且召回率损失最小的那个点。
6.2 单帧漏检不是死局:用5帧状态窗口做行为判定
垂钓是一个持续时间较长的行为,单帧偶尔漏检并不致命,致命的是漏掉整个行为过程。在监控场景里,我习惯把YOLO的单帧检测结果喂给一个极简的状态窗口:
from collections import deque class FishingTemporalJudge: def __init__(self, window=5, threshold=3): self.window = window self.threshold = threshold self.state = deque(maxlen=window) def update(self, person_box, rod_box, img_shape): if person_box is None or rod_box is None: self.state.append(0) return False ih, iw = img_shape rx = (rod_box[0] + rod_box[2]) / 2 / iw ry = (rod_box[1] + rod_box[3]) / 2 / ih hit = int(person_box[0] / iw < rx < person_box[2] / iw and person_box[1] / ih < ry < person_box[3] / ih) self.state.append(hit) return sum(self.state) >= self.threshold这个类维护一个长度为5的滑动窗口,每次推理传入人和鱼竿的边界框,只有鱼竿中心落在人框内才算一次有效观测。连续5帧里如果有3帧满足条件,就判定为“垂钓行为进行中”。这个策略能容忍短暂遮挡和单帧漏检,同时过滤掉路人手持长棍经过造成的瞬时误报。我现在的习惯是:任何新数据集都不直接上线单帧模型,至少套一层这样的时序逻辑再交给业务方。这套方法不复杂,但能让误报率降一半以上。希望帮到你。
本文还有配套的精品资源,点击获取