
简介面向计算机视觉中的目标检测任务特构建一套已标注的钓鱼人员检测数据集适合算法工程师、科研人员及目标检测入门者作为训练与验证素材。压缩包内共2000个文件包括1000张jpg原始图像与1000个xml标注文件标注格式为边界框完整记录目标类别与坐标信息可配合YOLO、Faster R-CNN、SSD等主流算法直接使用资源整体约41.55MB图像与标注一一对应便于数据划分与训练验证。图像多拍摄于岸边钓鱼场景覆盖不同角度、远近与光照条件标注边界框定位准确可直接用于mAP、召回率等指标评估有效减少数据预处理成本。当前已有2079人学习下载受到目标检测领域开发者的关注。该数据集适用于钓鱼检测方向的模型研究、算法对比与课程设计也可支撑智能监控中的非法捕鱼预警、钓鱼比赛自动计分等实际应用。1. 钓鱼fishing数据集与1000张已标注图的整理思路拿到“钓鱼fishing数据集21000IMG已标注.zip”这样的压缩包别急着双击解压。这个场景数据集的核心价值在于它把钓鱼活动拆成了可训练的目标检测任务1000 张图片、已经标好的类别框通常覆盖鱼、鱼竿、浮漂或人物一类对象。适合它的下游任务很明确渔获统计、鱼塘监控、钓鱼行为识别还有水面钓鱼视频里的小目标检测。适合的人群分两类一类是想在真实业务里落地检测模型的算法工程师一类是刚接触 YOLO 训练、需要一份干净数据把训练到部署整条链路走通的学习者。整理的重点不在解压而在确认标注格式、复查标注质量、调整训练参数这三步它们直接决定模型是能用还是要返工。2. 解压后先看结构和标注YOLO 格式与类别分布2.1 zip 解压与目录结构检查第一步是解压但要注意方式。常见做法是建一个独立目录再解压避免压缩包里的文件直接散落到当前工作目录后面找路径会非常痛苦。mkdir -p fishing_dataset unzip 钓鱼fishing数据集21000IMG已标注.zip -d fishing_dataset/ tree fishing_dataset -L 2解压后通常会看到images和labels两个根目录各自下面再按train和val分。如果压缩包里带有校验文件比如.sha256解压完可以用sha256sum -c对一遍确认中间没有缺包或坏块。用tree看到的典型结构如下fishing_dataset/ ├── images/ │ ├── train/ # 约 800 张 │ └── val/ # 约 200 张 └── labels/ ├── train/ # 与 images/train 一一对应的 txt └── val/ # 与 images/val 一一对应的 txt结构检查里最常踩的坑是 image 和 label 文件名对不上后来训练时就会报 “No labels found”。一条 diff 命令可以快速验证对应关系ls images/train | sed s/\.jpg$// | sort img_names.txt ls labels/train | sed s/\.txt$// | sort lbl_names.txt diff img_names.txt lbl_names.txt没有输出就说明两个目录里的文件名集合一致。这里用 sed 去掉扩展名后再比较避免 jpg 和 txt 的扩展名差异干扰结果。如果 diff 有输出按行号逐个处理不要批量删文件。2.2 YOLO 标注格式与归一化坐标这个数据集最常见的标注格式是 YOLO 纯文本格式每张图片对应一个同名.txt每行描述一个目标框0 0.482 0.371 0.203 0.118 1 0.315 0.694 0.150 0.084每行五个值依次是类别 id、中心点 x、中心点 y、框宽 w、框高 h。关键点是后四个值都除以了图片宽高归一化到 0~1 之间所以同一个标注文件在 640×640 和 1280×720 两种分辨率下都成立不需要按输入尺寸换算。cat fishing_dataset/labels/train/000042.txt直接查看文本是确认格式最直观的办法。看到全部坐标落在 0~1 区间、没有负数或大于 1 的值就可以认定格式基本正常。如果某些行为空那通常是标注时误标后被清空先记录文件名后续清洗阶段单独处理。2.2.1 class_id 与类别文件的关系类别 id 从 0 开始连续编号。钓鱼场景里最常见的是两类鱼fish和钓鱼竿fishing_rod有的版本会把浮漂或人加进来变成三类。压缩包里如果没有classes.txt就用当前注释里看到的最大 id 加 1 推断类别数再通过画框脚本人工确认类别名顺序。这一步推断错了后面画框时类别名会整体错位。2.3 用 Python 统计类别分布和实例量理解分布比理解格式更重要。1000 张图是总量真正决定训练上限的是每个类别的实例数。我一般会把每个类别框数量统计一遍顺带看下单图最多框数方便后面估算 batch 显存占用。# 统计 YOLO 格式标注的类别分布 import os from collections import Counter label_dir fishing_dataset/labels/train counter Counter() per_image_count [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: lines [line for line in f if line.strip()] per_image_count.append(len(lines)) for line in lines: cls_id int(line.split()[0]) counter[cls_id] 1 print(train 集合类别实例数:, dict(counter)) print(单图框数均值:, sum(per_image_count) / len(per_image_count)) print(单图框数最大:, max(per_image_count))脚本先过滤空行再用 Counter 统计每个类别的实例总数。输出里两类实例数如果相差超过 3 倍需要先确认 val 集合里的分布是否一致再决定训练阶段要不要调整类别权重。单图最大框数如果超过 50说明存在密集场景训练时图片缩放会把这些目标压得很小后面要优先考虑提高 imgsz。提示某张图对应的 txt 不存在时先检查文件名后缀或路径大小写不要急着删图很多问题是命名规范不一致造成的。3. 训练前的数据清洗与可视化校验3.1 用 OpenCV 批量画框检查标注质量坐标数值正确不代表框位置正确。水面的反光、远处的鱼影、鱼竿的细长形状都容易让标注人员把框画偏。最直接的校验方式是写画框脚本把图片和标注叠加后输出到 preview 目录随机抽 30 张过目。# 批量画框校验标注质量 import cv2 import os img_dir fishing_dataset/images/train label_dir fishing_dataset/labels/train class_names [fish, fishing_rod] def draw_boxes(img_path, label_path, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue # 跳过格式错误的行 cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(out_path, img) os.makedirs(preview, exist_okTrue) names sorted(os.listdir(img_dir)) for idx, fname in enumerate(names[:30]): name os.path.splitext(fname)[0] label_path os.path.join(label_dir, name .txt) if os.path.exists(label_path): draw_boxes( os.path.join(img_dir, fname), label_path, fpreview/{idx:03d}.jpg )脚本的关键点有三个一是 label 文件名要去掉扩展名再和图片配对二是画框时用int()截断坐标不要用round()靠近图片边缘的框用四舍五入可能画出画布导致预览图不完整三是循环前先判断 label 文件是否存在避免 FileNotFoundError 中断整批校验。检查时重点看三类问题框中心是否明显偏离目标、框只框住了目标的一部分、同一张图里是否出现几乎重叠的两个框。第二类问题在钓鱼竿上最常见竿身细长标注时很容易只框到中间一段。3.1.1 局部错框的修正方式如果抽查发现少量框位置偏移不必重新标注整个数据集。把对应的 label 文件单独挑出来用 LabelImg 打开原图手动微调再导出成同样的 YOLO 格式覆盖回去。修正完必须重新跑一遍画框脚本确认导出过程没有改变坐标顺序或类别 id。3.2 模糊、过曝与负样本筛选钓鱼图片大量来自手持拍摄或监控截图水面光线变化大模糊和过曝的比例比一般数据集高。训练前用拉普拉斯方差过滤模糊图是标准操作。# 用拉普拉斯方差识别模糊图片 import cv2 import os img_dir fishing_dataset/images/train threshold 80 bad_images [] for fname in os.listdir(img_dir): path os.path.join(img_dir, fname) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: bad_images.append(fname) # 无法读取按坏图处理 continue score cv2.Laplacian(img, cv2.CV_64F).var() if score threshold: bad_images.append(fname) print(f疑似模糊或不可读图片: {len(bad_images)}) for f in bad_images[:30]: print(f)拉普拉斯方差反映图像梯度的强弱数值越低越可能是模糊图。阈值 80 是通用经验值但钓鱼场景里水面占大面积的图会让整体方差偏低所以阈值可以放宽到 60画框校验时发现某张图虽然是水面但目标清晰、方差却很低就不要按模糊处理用直方图看分数分布再定阈值比拍脑袋设一个数更可靠。筛选出的坏图要从 image 和 label 两端一起删除。只删图片不删对应 txt训练时就会遇到“标签存在但图片缺失”的问题ultralytics 会直接跳过该样本但会在地图指标里造成统计偏差。3.3 负样本与难例的处理思路如果数据集只有鱼和钓鱼竿两类出现“只有水面没有目标”的图片时保留还是删除取决于业务需求。我一般会在训练集里保留 10%~15% 的负样本让模型学会在这些背景上不输出框能显著降低水面反光误检率。负样本的 label 文件保持空白训练脚本会自动跳过它。对于只包含鱼鳞、饵料碎屑等局部特征的难例说明模型容易把局部当整体这类样本保留下来对提升鲁棒性有帮助。4. 用 YOLOv8 训练自己的数据集参数与流程4.1 数据集划分与 yaml 配置训练前先确认压缩包自带的 train/val 划分是否合理。如果只有图片和标注但没有划分按 8:2 比例随机切分即可。划分时注意随机种子固定不然每次重新划分都会得到不同的验证集影响实验结果可比性。# 按固定随机种子划分训练集和验证集 import os import random from shutil import move random.seed(42) img_dir fishing_dataset/images label_dir fishing_dataset/labels for split in [train, val]: os.makedirs(f{img_dir}/{split}, exist_okTrue) os.makedirs(f{label_dir}/{split}, exist_okTrue) all_images [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(all_images) val_count int(len(all_images) * 0.2) for i, fname in enumerate(all_images): split val if i val_count else train name os.path.splitext(fname)[0] # 只在文件还未移动时执行移动避免重复运行报错 if os.path.exists(os.path.join(img_dir, fname)): move(os.path.join(img_dir, fname), os.path.join(img_dir, split, fname)) if os.path.exists(os.path.join(label_dir, name .txt)): move(os.path.join(label_dir, name .txt), os.path.join(label_dir, split, name .txt))划分后写数据集配置文件这是训练前必须做的一步。YOLOv8 用 yaml 描述数据集的路径和类别# fishing.yaml path: ./fishing_dataset # 数据集根目录 train: images/train val: images/val nc: 2 names: 0: fish 1: fishing_rodyaml 里path决定其他相对路径的基准位置train和val指向目录而不是具体图片文件。常见错误是把路径写成images/train/*.jpg这会导致数据集加载直接失败。4.2 训练命令与核心参数数据集准备完成后用下面的命令开始训练yolo detect train \ datafishing.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience15 \ projectrun/fishing \ namebaseline \ seed42这里用的是迁移学习方式modelyolov8n.pt可以加载在 COCO 上预训练过的权重。虽然 COCO 里没有“钓鱼竿”这个类别但底层特征提取器对纹理、边缘、形状的通用表达能力已经具备直接用预训练权重比从零训练收敛更快。如果压缩包自带的数据量超过 5000 张可以考虑把model换成yolov8n.yaml从头训练避免 COCO 先验干扰新类别的定位。参数说明参数推荐值作用和调整方向epochs100~1501000 张图在验证指标不再上升时由 patience 兜底不要死等训练完成imgsz640鱼类小目标多时提到 960显存占用约按面积关系增长batch166GB 显存用 1612GB 显存用 32batch 大小影响收敛稳定性patience15~20连续多少轮验证指标不上升就停止防止过拟合optimizerauto小数据集用 AdamW 效果稳定显存紧张时切 SGD 配动量cos_lrTrue余弦退火后期训练更平缓配合较大 epochs 收敛更稳训练过程中看两个东西命令行输出的指标和run/fishing/baseline/下的results.png。重点关注验证集 mAP50 和 mAP50-95 曲线。如果 loss 一直在降但 mAP 不涨大概率是标注噪声大或类别不平衡回到第 3 章重新检查标注而不是继续加训练轮数。4.2.1 数据增强参数在钓鱼场景里的取舍YOLOv8 默认开启的数据增强对钓鱼图片并非全是好处。水面反光会被hsv_h和hsv_s增强放大鱼的体色可能因此失真导致模型学到不稳定的颜色特征。我一般会把色相增强从默认 0.015 降到 0.01饱和度从默认 0.7 降到 0.5。mosaic1.0保留它让小目标学习到更多上下文对远处的小鱼和浮漂都有帮助。钓鱼竿是细长目标degrees旋转增强设置在 0~10 度就足够转得太多会让框的宽高比失真。4.3 训练结果的评估与指标解读训练结束后验证集评估结果一般长这样Class Images Instances Box(P) R mAP50 mAP50-95 all 200 412 0.892 0.867 0.912 0.611 fish 200 238 0.905 0.882 0.933 0.638 fishing_rod 200 174 0.878 0.851 0.890 0.581只盯 mAP50 会高估模型的实际表现。钓鱼竿是细长目标mAP50-95 比鱼低 0.05 是正常的因为 mAP50-95 对定位精度要求更高细长目标的框微调几个像素IoU 就会明显下降。如果钓鱼竿 mAP50 超过 0.85 但 mAP50-95 不到 0.6下一步优先把 imgsz 提到 960或者针对钓鱼竿样本做定向增强而不是盲目加数据。5. 训练完的下半场导出与推理适配5.1 导出 ONNX 模型训练完成后best.pt只是中间产物生产环境首选导出 ONNX 格式yolo export modelrun/fishing/baseline/weights/best.pt formatonnx imgsz640 opset12导出后先用一张验证集图片做端到端验证yolo predict modelrun/fishing/baseline/weights/best.onnx sourcefishing_dataset/images/val/000005.jpg如果 ONNX 版本的输出框和 .pt 版本基本一致说明计算图导出没有问题。注意对比输出的置信度数值ONNX 的精度损失正常情况下会控制在 0.01 以内。5.2 推理脚本里按类别调阈值ONNX 模型拿到后我一般会在推理脚本里单独控制每个类别的置信度阈值而不是全局一个 conf。钓鱼场景的特殊性在于水面反光和波纹会产生高置信度的误检远处的鱼又是低置信度的漏检两个问题在同一张图里都有。# 按类别拆分阈值的推理脚本 from ultralytics import YOLO import cv2 model YOLO(baseline.onnx) img cv2.imread(lake_frame_042.jpg) results model.predict(img, conf0.25, iou0.45, imgsz640) class_names model.names for box in results[0].boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) # 鱼类受水面反光干扰大提高阈值钓鱼竿形状独特压低阈值防漏检 if cls_id 0 and conf 0.35: continue if cls_id 1 and conf 0.2: continue print(f{class_names[cls_id]} conf{conf:.3f} box{box.xyxy[0].tolist()})NMS 的 IoU 阈值在这里不要拉太高。iou0.45比默认的 0.7 更能压住水面场景下的重复框因为反光区域经常产生两个高度重叠的预测框代价是部分遮挡严重的鱼会只剩一个框实测下来漏检增量小于误检减量整体收益是正的。5.2.1 视频流推理时的抽帧技巧接监控视频流做实时检测时逐帧推理会明显掉帧。常见做法是每 3 帧取 1 帧送入模型其余 2 帧沿用上一帧的结果同时记录帧编号用于对齐。钓鱼场景中鱼的移动速度不快鱼竿和浮漂更是长时间静止这种抽帧策略能把吞吐量提升近 3 倍而检出效果几乎不受影响。如果画面里同时存在快速游动的小鱼可以把抽帧间隔从 3 降到 2再配合按类别阈值过滤让近处目标优先保框。本文还有配套的精品资源点击获取