
简介这是一套面向YOLO系列目标检测任务的捕鱼识别数据集适合需要训练、验证或测试渔业检测模型的算法工程师与研究者可直接用于智慧渔场、捕捞监控等场景。压缩包内标注文件共2000个含1584个XML与416个TXT分别对应VOC格式与YOLO格式并单独保存在不同文件夹中配合1813张已标注捕鱼场景图像整体约96.7MB。数据已经预先划分好训练与验证范围可无缝接入yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流框架。YOLO格式的TXT标注记录类别索引、中心点坐标及宽高比例方便直接进行模型训练和结果验证XML标注则可配合标注工具完成二次整理或格式转换。对使用者而言这套数据能显著压缩从原始图像到可训练数据集的准备耗时也便于不同项目间切换目前已有75人学习下载适合具备一定深度学习基础、希望快速验证捕鱼识别效果的研究者与工程人员。1. 捕鱼识别数据集与 YOLO为什么 1813 张带标签图能快速起步渔港监控、渔业资源调查、钓鱼行为识别这些场景里最缺的不是算法而是带标签的数据。标题里的 yolo算法-捕鱼识别数据集-1813张图像带标签-钓鱼.zip 是一个典型的目标检测数据包1813 张图像每张图配一个或多个文本标签框住鱼、渔网或捕捞动作。对于刚想跑通 yolov8 训练自己的数据集的人来说它意味着你不需要从零画框解压后就能进入训练流程。适合三类人第一次用 YOLO 做水产检测的新手、想快速验证方案可行性的算法工程师、以及需要扩充渔获识别样本的团队。但拿到 zip 先别急着解压就训练目录结构、标签格式和标注质量这三关不过后面会浪费你一整天。2. 拿到 zip 先别急着训练目录结构、标签格式与图片完整性检查2.1 解压命令与目录核对先看 images 和 labels 是否配对在 Linux 服务器上处理这类资源我一般先把中文文件名换成简单名字避免 Python 和 OpenCV 在读取路径时因为编码问题报错。然后用 unzip 解压看目录结构。很多开源数据集打包时习惯把 train、val 分好也可能只是 images/ 和 labels/ 两个平级目录。先创建干净的工作目录再解压避免文件散落当前目录。# 先给中文名换一个简单名避免后续路径编码问题 mv yolo算法-捕鱼识别数据集-1813张图像带标签-钓鱼.zip fishing.zip mkdir -p fishing unzip -q fishing.zip -d fishing cd fishing find . -maxdepth 2 -type d | sortunzip 的 -d 指定解压目标目录-q 关闭逐文件打印。find 只看两层目录能快速确认根目录下面是不是干脆套了一层文件夹。如果出现嵌套建议先规整成 images/ 和 labels/ 两个平级目录后面所有脚本都按这个假设写。接下来统计图片和标签数量判断是否一一对应。echo images: $(ls images/*.jpg 2/dev/null | wc -l) echo labels: $(ls labels/*.txt 2/dev/null | wc -l)wc -l 在这里统计的是 ls 输出行数也就是文件数量。如果 images 有 1813 张但 labels 只有 1800 个说明有图未标注如果数量相等也要小心某些 txt 是 0 字节空文件。注意*.jpg只匹配小写 jpg遇到 PNG 或大写 JPG 会漏统计更稳妥的写法是find images -type f | wc -l。这里只是先做个快速感知真正严格的检查放在下一节。2.2 标签检查TXT 坐标是否规范、类别 id 是否越界YOLO 标签的标准格式一行是class x_center y_center width height坐标全部归一化到 0~1。用 labelimg 打标完 YOLO 格式的标导出的文件本来没问题但经过拷贝、二次编辑、人工改后缀之后容易出现逗号分隔、坐标写成像素值、行末带多余空格等情况。这些脏字会在训练时被静默忽略或直接报错。最好在训练前写脚本把所有 txt 过一遍。import os import glob labels_dir labels for txt in glob.glob(os.path.join(labels_dir, *.txt)): with open(txt) as f: lines [l.strip() for l in f if l.strip()] for line in lines: parts line.split() if len(parts) ! 5: print(f{txt}: 非5列数据: {line}) continue _, x, y, w, h parts x, y, w, h map(float, [x, y, w, h]) if not (0 x 1 and 0 y 1 and w 0 and h 0): print(f{txt}: 坐标越界: {line}) if x - w / 2 0 or x w / 2 1 or y - h / 2 0 or y h / 2 1: print(f{txt}: 边框出图: {line})脚本逐行检查 5 列数量、坐标范围和归一化边界。如果某一行坐标写成720 300这种像素值数字会大于 1这里直接抓出来。边框出图指的是框的中心在合理范围但宽高延伸到图外YOLO 能容忍轻微越界但训练时会被裁剪严格检查总是对的。还要单独扫一遍类别 id 的最大值看是否超过 data.yaml 里的类别数-1特别是当你只有一个类别时标签里混入 id1 会被 YOLO 悄悄过滤训练日志里只有一行 warning不仔细看根本发现不了。2.3 可视化验证用 OpenCV 把标注框画回原图格式检查只能说明文件能读不能证明标得对。我会抽 10 张图把标签画回去用眼睛看一遍。水面反光、渔网边缘模糊、鱼体被波浪遮挡这些场景最容易出现框贴不紧的问题。可视化这步也能帮你确认类别定义有的数据集把整条鱼框住有的把鱼头单独框有的把钓鱼动作整体框在一个大框里模型学出来的能力完全不一样。import cv2 import glob for img_path in glob.glob(images/*.jpg)[:10]: txt_path img_path.replace(images, labels).replace(.jpg, .txt) img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.split() if len(parts) ! 5: continue cls, x, y, bw, bh parts cls, x, y, bw, bh int(cls), float(x) * w, float(y) * h, float(bw) * w, float(bh) * h x1, y1 int(x - bw / 2), int(y - bh / 2) x2, y2 int(x bw / 2), int(y bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_ img_path.split(/)[-1], img)代码把归一化坐标乘回图片宽高转成像素坐标画框。注意 OpenCV 读到的是 BGR 通道画绿色框没问题但如果图片本身带 EXIF 旋转信息这里画出来的框会错位这个坑留到第 5 章专门说。可视化结果不用一张张细看扫一遍绿色框是不是贴近目标、有没有把水花框进去、有没有重叠框。如果大量“框不贴边”建议用 labelimg 把问题图重新精修一下1813 张图重标成本可控比带着坏标签训练一个月再返工划算得多。3. 把 1813 张图整理成 YOLOv8 训练集数据拆分与 YAML 配置3.1 目录规整与 train/val/test 拆分脚本YOLOv8 的标准数据目录长这样dataset/images/train、dataset/images/val、dataset/images/test以及对应的dataset/labels/train等。images 和 labels 必须平级图片文件名和标签文件名保持一致只是后缀不同。拆分比例我常用 8:1:11813 张图大概能分出 1450 张训练、180 张验证、180 张测试。少样本任务验证集可以缩小到 100 张给训练多留一点。import os import random import shutil from glob import glob random.seed(42) os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/images/test, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) os.makedirs(dataset/labels/test, exist_okTrue) images glob(images/*) random.shuffle(images) n_train int(len(images) * 0.8) n_val int(len(images) * 0.1) for i, img in enumerate(images): name os.path.basename(img).replace(.jpg, .txt) lab os.path.join(labels, name) if not os.path.exists(lab): print(f跳过无标签图片: {img}) continue if i n_train: split train elif i n_train n_val: split val else: split test shutil.copy(img, fdataset/images/{split}/) shutil.copy(lab, fdataset/labels/{split}/) print(train:, len(glob(dataset/images/train/*))) print(val:, len(glob(dataset/images/val/*))) print(test:, len(glob(dataset/images/test/*)))拆分脚本先用random.seed(42)固定随机数保证每次运行结果一致。shutil.copy 是复制不是移动原始数据保留着后面出了任何问题都有后悔药。标签文件缺失时直接跳过并打印避免把无标签图片塞进训练集。如果你打算做时序场景验证随机拆分其实不合适因为连续帧很可能分到 train 和 val 两边导致验证指标虚高。这个泄漏问题在第 5 章还会展开这里先用随机拆分跑通流程。3.2 编写 data.yaml类别名、路径与训练参数训练 YOLOv8 的所有数据集信息都写在一个 data.yaml 里。这个文件不复杂但最容易被粗心的人写错。最常见的翻车点是类别顺序和标签里的 id 对不上。labelimg 打标完成 YOLO 格式时txt 第一列的 0、1、2 就是类别 id对应 names 列表的下标。如果你把 names 写成[fishing_net, fish, boat]而标签里 id0 代表的是 fish模型学到的就是把鱼识别成渔网。# dataset/data.yaml path: ./dataset train: images/train val: images/val test: images/test names: 0: fish 1: net 2: boatnames 可以用列表也可以用这种字典形式字典里的 key 必须和标签 id 严格一致。如果数据集只有一个类别names 写成[fish]或{0: fish}都可以但一定不要只写一个字符串。path 建议用绝对路径例如/home/user/fishing_dataset/data.yaml。相对路径在训练时经常因为工作目录变化而找不到文件这个坑不致命但很浪费排查时间。提示类名最好用英文。中文类名虽然能训练但在结果绘图和日志打印时容易乱码可视化不好用没必要给自己添麻烦。3.3 首次训练命令选 imgsz、batch、epochs 的经验值数据集检查完、data.yaml 写好后第一件事不是换大模型而是用小模型把流程跑通。YOLOv8 的 n/s/m/l/x 五档规格1813 张图不算多先跑 n 最稳妥。如果一上来就用 l 或 x显存先不说训练日志里看到的 loss 波动可能掩盖数据本身的问题。我一般用yolov8n.pt预训练权重做迁移学习收敛速度比随机初始化快很多。yolo train datadataset/data.yaml modelyolov8n.pt imgsz640 batch16 epochs100 device0data 指向刚才写的 yamlmodel 指定预训练权重imgsz 是训练输入尺寸。渔港监控图很多是 1920x1080 甚至更高缩到 640 会让小鱼只有十几个像素对检测很不利。8G 显存跑 n 模型 batch16 没有压力如果显存不够先降 batch 到 8再考虑 imgsz 降到 416。imgsz 低于 480 对渔业小目标不友好所以能不动就别动。epochs 第一次设 100 就够不用 300。训练到 50 轮时看 val 指标还在涨后面再延长也不迟。训练中断了也可以用yolo train resumeTrue从 last.pt 恢复不必从头开始。4. 训练与调参从损失曲线到置信度门限的完整链路4.1 训练命令解析与显存不足时的应对训练开始后控制台会刷出一堆信息很多人直接忽略只看最后的 mAP。这样训练完模型就是一个黑匣子出了问题也不知道从哪调。我习惯在训练前先看一眼显卡剩余显存确认之前选定的 batch 能不能放下。如果 OOM优先降 batch其次降 imgsz最后才换更小的模型。降 batch 会让每个 epoch 的迭代次数变多训练时间略微增加但对稳定性影响最小。# 查看显存 nvidia-smi --query-gpuname,memory.used,memory.free --formatcsv # 显存不足时把 batch 降到 8 yolo train datadataset/data.yaml modelyolov8n.pt imgsz640 batch8 epochs100 device0 # 如果还 OOM再把 imgsz 降到 416 yolo train datadataset/data.yaml modelyolov8n.pt imgsz416 batch16 epochs100 device0device0 指定第一张 GPU单卡机器可以不写但写上更明确。CPU 也能训练只是慢到让人怀疑人生1813 张图跑 100 轮可能要十几个小时不建议尝试。OOM 报错一般会出现在日志中段特征是CUDA out of memory。如果你加了很多数据增强OOM 往往不是模型本身大而是 mosaic 把 4 张图拼在一起内存峰值翻倍。这时候不要只盯 batch先把 mosaic 临时关闭或降小等方法验证完再开回来。4.2 看日志判断模型是否收敛box_loss、cls_loss、dfl_loss训练日志里最需要关注的是三块 lossbox_loss、cls_loss、dfl_loss。box_loss 衡量预测框和真实框的回归误差cls_loss 是分类误差dfl_loss 是分布焦点损失用来细化边框的坐标分布。渔类识别场景里鱼体小、数量多box_loss 往往比常规目标检测高因为漏检和重叠目标多。把三个 loss 分开看能定位到底是数据问题还是参数问题。# 训练日志里截取对比实际输出会更多 Epoch 1/100: box_loss 0.92, cls_loss 1.65, dfl_loss 1.20 Epoch 50/100: box_loss 0.48, cls_loss 0.66, dfl_loss 0.89 Epoch 100/100: box_loss 0.31, cls_loss 0.38, dfl_loss 0.72box_loss 不降说明框学不准大概率是标注边界贴得不紧。cls_loss 不降可能是类别不平衡比如“鱼”的样本远多于“渔网”模型倾向于把网也预测成鱼。dfl_loss 降得慢一般和输入尺寸有关原图 1920x1080 缩到 640鱼尾边缘信息丢失严重dfl 自然学不动。这时候可以试 imgsz960但训练时间几乎翻倍要在精度和预算之间权衡。不要只看最后一个 epoch中间趋势更重要。如果 50 轮后三个 loss 还在大幅下降说明 100 轮不够继续加。4.3 推理时调整置信度门限别让水花和渔网框满天飞训练完成后用 best.pt 推理默认 conf0.25这个阈值对渔业场景偏低。水面反光、渔网、漂浮物都容易被模型高置信度预测成鱼导致输出图上一堆框。YOLO 检测调整置信度门限是最快的改善手段不用重新训练只需要在 predict 命令里指定 conf 和 iou 参数。# 先看默认阈值下的结果 yolo predict modelruns/detect/train/weights/best.pt sourcedataset/images/test conf0.25 iou0.5 # 提高到 0.45过滤水花误检 yolo predict modelruns/detect/train/weights/best.pt sourcedataset/images/test conf0.45 iou0.6conf 是置信度阈值低于这个值的框会被丢掉iou 是 NMS 合并阈值鱼群相互重叠时调高到 0.6可以把同一鱼群的重复框合并成一个。阈值不是越高越好conf 调到 0.7 以上会漏掉小目标。最好的做法是在验证集上跑几个 conf 梯度算一遍 mAP0.5找曲线拐点。单张图上看效果容易被干扰因为某张图可能恰好没有水花看起来什么都好。5. 捕鱼识别数据集训练避坑四个高频翻车点与排查方法5.1 标签空文件、类别 id 越界与 class 顺序问题现象训练刚开始就出现WARNING no labels found in dataset/train.cache但检查 images 目录图片明明都在labels 目录也不是空的。原因txt 文件存在但是 0 字节或者内容格式错误导致 YOLO 缓存过滤掉。另一个隐蔽原因是类别 id 越界YOLOv8 会把 id 大于等于类别数的标签静默忽略。训练日志里只有一行 warning不仔细盯控制台根本发现不了尤其批量训练时很容易滑过去。解决先用 find 找空文件再写脚本扫最大类别 id。find labels -name *.txt -size 0 -printimport glob ids set() for txt in glob.glob(labels/*.txt): for line in open(txt): parts line.split() if parts: ids.add(int(parts[0])) print(max class id:, max(ids))空文件直接删掉下次训练会重建缓存。max id 如果比 data.yaml 里 names 长度减 1 还大说明有几类标签缺失。最常见的原因是 labelimg 打标时 class 顺序和 yaml 里不一致导致 id 映射错误。这时候最稳妥的做法是按第 2 章的可视化脚本把每个 id 对应的图打印出来对照文件名统一重写 txt 第一列。不要相信“只改 names 不用动标签”的玄学标签和 names 是硬对应关系错一个全错。5.2 图片尺寸不一致与 EXIF 旋转带来的读图事故现象训练到一半突然报错类似error: (-215:Assertion failed) size.width0 size.height0 in function imread或者某些图片虽然训练过了但可视化时标注框整体偏移 90 度。原因OpenCV 读取不了部分编码异常或损坏的图片另一个高频原因是图片包含 EXIF 旋转信息。手机和部分监控相机写入的 EXIF 会让图片在预览软件里显示正常但像素矩阵本身没有旋转YOLO 读图时也不会主动处理 EXIF。于是图片是横的标签框还是按竖的来标全部错位。解决先统一图片格式和 EXIF 方向再进训练。下面这个脚本会把所有图片转成 RGB JPG并按 EXIF 方向旋转。from PIL import Image, ExifTags import glob, os os.makedirs(images_clean, exist_okTrue) for img_path in glob.glob(images/*): try: img Image.open(img_path) exif img.getexif() for tag, value in exif.items(): if ExifTags.TAGS.get(tag) Orientation: if value 3: img img.rotate(180, expandTrue) elif value 6: img img.rotate(270, expandTrue) elif value 8: img img.rotate(90, expandTrue) img.convert(RGB).save(images_clean/ os.path.basename(img_path).rsplit(., 1)[0] .jpg, quality95) except Exception as e: print(img_path, error:, e)注意如果图片旋转了标注框坐标也要跟着做同样的旋转变换因为 YOLO 坐标是相对图片宽高的。上面脚本只处理图片适合图片本身没有 EXIF 偏移的场景。如果你确认数据集里有 EXIF 问题建议回到 labelimg 重新导出标签或者写一个同时变换图片和标签坐标的脚本。这个坑最耗时所以第 2 章的可视化检查一定要做早发现早处理。5.3 漏检背后是标注边界和遮挡问题不是模型问题现象模型对单条鱼检测效果尚可一到鱼群就只检出一两条或者渔网覆盖下的鱼完全漏掉调高 conf 更严重。原因YOLO 是矩形框模型目标互相遮挡时矩形框把多条鱼框在一起模型不知道该学哪一条。另一个原因是数据集标注时只标了可见部分没有标完整鱼身模型学到的“鱼”只是鱼肚子或鱼头。水面反光又加深了这种误差。解决先做可视化确认是框太大还是框太小。如果框边界粗糙可以用脚本把框收缩 10%让模型更聚焦目标本体。import glob for txt in glob.glob(labels/*.txt): lines [] for line in open(txt): parts line.split() if len(parts) ! 5: continue # 收缩因子 0.9只在确认边界偏松时使用 cls, xc, yc, w, h parts[0], float(parts[1]), float(parts[2]), float(parts[3]) * 0.9, float(parts[4]) * 0.9 lines.append(f{cls} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}\n) open(txt, w).writelines(lines)收缩因子 0.9 表示宽高各缩小 10%中心点不变。如果原始标注已经很紧这步会裁掉鱼头鱼尾所以改完后再可视化一遍。渔网遮挡漏检靠调参解决不了更好的方向是收集更多部分遮挡的样本或者用实例分割模型做二次过滤。这里不展开核心思路是漏检优先查数据不要急着堆模型复杂度换更大的 YOLO 模型只是在延缓问题。5.4 重复图片与数据泄漏训练集里混进同一场景的不同帧现象训练时 mAP0.5 高得吓人接近 0.98但放到新场景的测试视频里指标立刻掉到 0.6 以下像背题一样。原因监控视频抽帧得到的数据集如果按随机方式拆分 train/val/test同一段视频的相邻帧会同时出现在训练集和验证集。相邻两帧画面几乎一样模型实际上是在记忆画面没有学到泛化的鱼体特征。渔港场景里船不动、鱼群缓缓游这种时间相关性特别强。解决先用 md5 找完全相同的重复文件再考虑场景聚类。import glob, hashlib seen, dups set(), [] for img in sorted(glob.glob(images/*.jpg)): h hashlib.md5(open(img, rb).read()).hexdigest() if h in seen: dups.append(img) print(dup:, img) else: seen.add(h) print(dups:, len(dups))md5 只能找出完全一致的重复帧不能找出画面高度相似的相邻帧。更实用的做法是先按文件名里的时间戳或序号排序每 30 帧当做一个片段一个片段整体分到 train 或 val。如果文件名完全没有规律就只能用感知哈希按汉明距离聚类先把相似帧归组再按组拆分。对于 1813 张图这个工作量不大但很多人会忽略。验证指标虚高时先怀疑数据泄漏再怀疑模型和参数。6. 最后一公里用小模型预标注、增强策略与验证技巧6.1 用训练好的模型给未标注视频帧生成伪标签当你跑通了基础训练下一步通常是扩大样本量。完全手标一万张渔港监控图不现实我一般会用已经训好的 best.pt 做半自动标注模型先预测一批未标注帧生成 YOLO 格式的伪标签人再抽查修正。yolo predict modelruns/detect/train/weights/best.pt sourceunlabeled_frames conf0.5 save_txtTrue projectpseudo_labelssave_txtTrue 会把每个预测框写成同名 txt输出在 pseudo_labels 目录的 labels 文件夹里。conf 建议设 0.35 到 0.5阈值太高会导致伪标签太少太低会产生大量假阳框。生成后用 labelimg 抽查 20% 样本把边界偏的框删掉剩下的并入训练集。这样循环两轮数据集就能从 1813 张扩到几千张且正样本分布比手动标更贴近真实场景。6.2 增强验证多测试几个时段、角度和水面反光场景渔港和钓鱼场景里光照变化对模型影响很大。同一个模型在晴天正午和阴天黄昏的表现可能完全不同这不是模型玄学而是训练集和推理分布不一致。我习惯训练后准备 4 到 6 个典型场景验证白天顺光、白天逆光、黄昏、夜间灯光、雨天、渔网遮挡。拿这些图跑一遍推理统计每张图的平均检测数和平均置信度。如果某个场景平均置信度低于 0.35说明训练集覆盖不够。这时候不要盲目加训练轮数更好的办法是收集该场景的样本或者对现有训练集做亮度、对比度、色相随机增强模拟不同时段的水面反光。我现在的习惯是每次拿到 yolo算法-捕鱼识别数据集这类资源先花一小时做完整性和泄漏检查再开始训练。这套流程帮我在渔港监控和水产平台的多个项目里省下了大量返工时间希望帮到你。本文还有配套的精品资源点击获取