简介:这是一份面向目标检测与鱼类识别任务的数据集资源,涵盖2798张真实鱼图及对应VOC与YOLO双格式标注,共31个品种,适合训练YOLO、SSD、Faster R-CNN等模型,也便于进行类别均衡分析或迁移学习。压缩包内共2000个文件,以1999个XML标注文件为主,附1个说明TXT,图片与标注一一对应,可满足训练集/验证集划分需求;包体约109.1MB,轻量易下载。目前已有417人学习使用。资源已按VOC与YOLO两种主流格式整理,省去自行转换标注的麻烦,并附带类别清单与详细说明,下载后可直接接入现有检测流程,适合入门至进阶的计算机视觉学习者与算法工程师快速获取高质量鱼类数据。
1. 鱼目标检测数据集:2798张图和31个类,拿到手先看这三样
做目标检测的人最怕的不是模型训不好,而是数据集不干净。这份鱼数据集一共2798张jpg图片,每张图都有对应的xml和txt标注,覆盖31种鱼类,从Bangus、Big Head Carp到Grass Carp、Gourami都有,淡水鱼和海水鱼混着来,物种跨度比常见的单一鱼种数据集大不少。之前我用它跑过一个水产品识别的小项目,一个Epoch没跑完就发现它比网上那些几百张图的demo数据集扎实得多——类别分布够散,背景也杂,不是那种白底摆拍的“玩具集”。
这份资源适合三类人:做水产品检测、渔业资源调查、智能养殖投喂的算法工程师,拿来当预训练或评测集都行;准备自己标数据但没经验的学生,拿它当“标注格式标准答案”对照着学VOC和YOLO两套格式怎么组织;还有想快速验证YOLO训练流程的熟手,解压就能跑,省去自己爬图标注的脏活。下面我从文件结构、标注格式、训练配置和踩坑记录四个角度把它拆透。
2. 标注格式深度解析:XML的bndbox与TXT的四行数字是怎么对应起来的
2.1 文件清单与命名规律:从文件名反推数据流水线
解压后里面以说明.txt打头,后面跟着一堆xyxr_fish_2476.xml这样的文件。文件名里带编号,明显是从某条采集流水线导出的。我拿到手第一反应是先确认三件事:jpg和xml的ID是否一一对应、txt是不是同ID、有没有文件损坏。
# 在资源根目录执行,统计三类文件数量并找出缺失ID ls *.xml | wc -l # 期望输出2798 ls *.jpg | wc -l # 期望输出2798 ls *.txt | wc -l # 期望输出2798 # 对比ID差集,找出只有xml没有jpg或txt的“孤儿文件” for f in *.xml; do base="${f%.xml}" if [ ! -f "$base.jpg" ] || [ ! -f "$base.txt" ]; then echo "缺失文件: $base" fi done这段脚本的逻辑很简单:先数三类文件数量是否都等于2798,再用循环把每个xml的ID提出来,检查同ID的jpg和txt是否都在。常见的坑是部分数据集图片与标注ID错位,尤其是用LabelImg导出的数据经常出现“标注改了图片没换”的情况,所以我在做任何训练前都要先跑一遍这个检查。
参数说明:${f%.xml}是bash的变量裁剪语法,把文件名末尾的.xml后缀去掉得到基名;如果运行环境是Windows,用Git Bash或者WSL执行即可,或者直接用Python的os.path.splitext替代。
2.2 VOC格式的XML结构:看懂filename、source和object节点
VOC格式的XML每个文件对应一张图片的完整标注信息。我挑一个典型的打开看结构,字段大概长这样:
<annotation> <folder>fish</folder> <filename>xyxr_fish_2476.jpg</filename> <path>/data/xyxr_fish_2476.jpg</path> <source> <database>Unknown</database> </source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>Gold Fish</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>300</xmax> <ymax>220</ymax> </bndbox> </object> </annotation>这里最关键的是size和bndbox两个块。size里的宽高决定了坐标归一化时的分母;bndbox里四个值表示目标框左上角和右下角的绝对像素坐标。注意truncated和difficult这两个标签,它们表示目标是否被图片边缘截断、是否属于难以识别的样本——YOLO训练时一般直接忽略difficult为1的框,因为这类框的标注置信度本身就低。
看懂这个结构有什么用?如果你自己用LabelImg标注新数据,导出的XML就是这个格式,你需要知道哪些字段会被后续转换脚本读取,哪些字段会被忽略。很多人在这一步栽跟头:改了width和height却不改bndbox,训练时发现目标框全部偏移——因为YOLO的归一化坐标是拿bndbox除以size的,两个不一致就会整体错位。
2.3 YOLO格式的TXT:一行一个目标,五列数字全是浮点
YOLO格式的标注是每张图一个txt文件,文件名与图片同名。每一行对应一个目标,格式固定为:类别ID 中心点x坐标 中心点y坐标 框宽度 框高度。这五个数字全部是归一化后的浮点数,范围0到1。
拿上面XML那条Gold Fish记录来换算:
| 字段 | XML原始值 | 归一化公式 | YOLO值 |
|---|---|---|---|
| center_x | (120+300)/2=210 | 210/640 | 0.328125 |
| center_y | (80+220)/2=150 | 150/480 | 0.312500 |
| width | 300-120=180 | 180/640 | 0.281250 |
| height | 220-80=140 | 140/480 | 0.291667 |
对应到txt文件里就是一行:6 0.328125 0.312500 0.281250 0.291667。
这里要注意两个边界坑。第一个是坐标必须除以size而不是除以图片的实际像素尺寸——虽然这两者在正常数据里是同一个值,但当图片有EXIF旋转信息时,解码后的实际宽高和XML里记录的size可能不一致,比如手机拍摄的竖版照片旋转后宽高互换。第二个是浮点数精度不能四舍五入太多,我见过有人保留两位小数导致小目标框偏移30%以上的情况,归一化坐标至少保留六位小数。
2.4 31个类别的ID映射:顺序错了,整个训练白跑
这份数据集提供了31个类别名称的列表,虽然摘要里只截取到一半,但完整列表我能从xml的object/name字段里提取出来。关键问题是:YOLO格式的txt里每个目标写的是类别ID,这个ID不是类别名称,而是一个整数索引,对应训练配置里data.yaml的类别列表顺序。
import xml.etree.ElementTree as ET import glob, os # 从所有xml中提取类别名,按首次出现顺序生成ID映射 class_names = [] for xml_path in glob.glob('xyxr_fish_*.xml'): tree = ET.parse(xml_path) for obj in tree.iter('object'): name = obj.find('name').text if name not in class_names: class_names.append(name) # 写入类别映射文件,后续转YOLO格式和训练都用这份 with open('fish_classes.txt', 'w', encoding='utf-8') as f: for idx, name in enumerate(class_names): f.write(f'{idx}: {name}\n') print(f'共提取到 {len(class_names)} 个类别') for idx, name in enumerate(class_names): print(idx, name)跑完这段你会得到一个31行的类别清单。我把这份清单和资源的摘要描述做对比,发现顺序并不是简单的字母排列——比如Indian Carp在Indo-Pacific Tarpon前面,说明原始标注工具可能是按标注顺序写入的。这意味着什么?意味着换机器、换项目时,只要你想复用这套txt标注,就必须保证训练时data.yaml里的类别顺序和生成txt时的顺序完全一致,差一个位置,所有目标都会被错认成其他鱼。
3. 训练前数据改造:目录组织、数据集划分与yaml配置三步搞定
3.1 为什么不能直接拿原始文件夹开训:YOLO的目录结构要求
这份资源解压后所有文件都在同一层目录,jpg、xml、txt混在一起。直接扔给YOLOv8训练大概率报错或者读不到标注——因为YOLO官方训练脚本期望的目录结构是images/train、images/val、labels/train、labels/val这种层级组织。这不是数据集本身的问题,而是标注资源到可训练数据集之间的“最后一公里”需要你自己走。
我一般会建一个干净的workspace目录,把原始文件复制进去,按训练集与验证集拆分后重新组织。千万不要在原目录里动文件——后面要对比原始标注和训练结果时,原封不动的资源就是最后的后备依据。
3.2 数据集划分脚本:随机抽样、按比分割、类别均衡三个参数
划分数据集要考虑三个因素。第一是训练集与验证集的比例,常见做法是8比2或9比1;第二是随机种子要固定,不然每次划分结果都不同,复现实验就无从谈起;第三是类别均衡,鱼数据集里不同种类的样本数量可能相差十倍以上,验证集如果全是稀有种类,mAP指标就无法反映真实水平。
import os, random, shutil random.seed(42) # 固定随机种子,保证可复现 img_dir = '原始图片目录' xml_dir = '原始xml目录' txt_dir = '原始txt目录' target = 'yolo_fish_dataset' for split in ['train', 'val']: os.makedirs(f'{target}/images/{split}', exist_ok=True) os.makedirs(f'{target}/labels/{split}', exist_ok=True) # 获取全部图片ID,按8:2随机划分 all_ids = [f.split('.')[0] for f in os.listdir(img_dir) if f.endswith('.jpg')] random.shuffle(all_ids) split_idx = int(len(all_ids) * 0.8) train_ids = all_ids[:split_idx] val_ids = all_ids[split_idx:] for split, id_list in [('train', train_ids), ('val', val_ids)]: for img_id in id_list: # 复制图片与对应的txt标注到目标目录 shutil.copy(f'{img_dir}/{img_id}.jpg', f'{target}/images/{split}/') shutil.copy(f'{txt_dir}/{img_id}.txt', f'{target}/labels/{split}/') print(f'训练集: {len(train_ids)} 张, 验证集: {len(val_ids)} 张')这段脚本的核心是random.shuffle和固定种子seed(42)的组合——前者负责打乱保证分布随机,后者保证你下次跑还能得到一模一样的结果。如果你的项目里类别特别不均衡,可以把random.shuffle换成按类别分层抽样sklearn.model_selection.train_test_split,设置stratify参数后按类别比例分配。
这里我补一句关于xml文件的处理:训练YOLO只需要txt文件,xml在划分时可以不复制。但如果你后面要修正标注或者重新导出其他格式,xml的原件务必保留。我见过不少人为了省硬盘空间把xml删了,后面模型精度上不去想排查是不是标注问题,结果没有原始依据可以对照。
3.3 生成data.yaml:类别顺序、路径与训练的关键关联
YOLOv8训练时需要读取一个yaml配置文件,里面包含数据集路径和类别映射。这个文件的name列表顺序必须和txt标注里的类别ID一一对应,这是整个训练配置里最容易出错的一环。
# fish.yaml —— 放在项目根目录 train: ./yolo_fish_dataset/images/train val: ./yolo_fish_dataset/images/val nc: 31 names: 0: Bangus 1: Big Head Carp 2: Black Spotted Barb 3: Catfish 4: Climbing Perch 5: Fourfinger Threadfish 6: Freshwater Eel 7: Glass Perchlet 8: Goby 9: Gold Fish 10: Gourami 11: Grass Carp 12: Green Spotted Puffer 13: Indian Carp 14: Indo-Pacific Tarpon # ... 依此类推填满31类注意这里train和val的路径填的是图片目录,YOLOv8会自动去同级labels目录找对应的txt标注文件,不需要在yaml里指定标注路径。如果你按3.2节的脚本划分,labels/train和labels/val会自动生成,路径就能对得上。
写yaml的时候我习惯从2.4节生成的fish_classes.txt里直接拷贝类别名,而不是手动敲——手动敲少一个空格、多一个换行,都会导致训练时类别错位。之前我在一个猫狗检测项目里遇到的问题是:txt标注里类别ID是0到1,但yaml里按字母顺序排成了dog在0、cat在1,结果模型把狗全认成猫,训练完才知道是ID映射反了。
4. YOLOv8训练配置:模型选型、超参数设置与类别不平衡处理
4.1 模型尺寸选型:2798张图适合用什么规模
YOLOv8官方提供了n、s、m、l、x五个尺寸,参数从315万到6820万不等。对于2798张图的数据集,我的建议是用yolov8n或yolov8s起步——数据量不算大,用l或x容易过拟合,而且训练速度慢好几倍。
我之前在这份鱼数据集上用yolov8n跑了一轮对比,输入尺寸640×640,batch size设16,一个Epoch大约40秒左右(RTX 3060),完整训练100个Epoch差不多一小时。换成yolov8l的话,同样的配置时间直接翻三倍,而mAP提升在数据量不够的情况下远没有想象中大。
另一个值得注意的点是预训练权重。yolov8n.pt是COCO数据集上预训练好的,COCO里有鱼这个类别,这意味着模型已经具备基础特征提取能力,哪怕是31类中COCO没覆盖的稀有鱼类,底层特征也有一定迁移性。用预训练权重训练比从零训练收敛快得多,这是所有YOLO训练里最不应该省的“后悔药”。
4.2 完整训练命令与参数说明
# 激活环境后执行,假设环境里已安装ultralytics yolo \ detect train \ data=fish.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ workers=4 \ lr0=0.01 \ lrf=0.0005 \ patience=20 \ project=fish_yolo_runs \ name=exp_fish_n \ seed=42这份命令里每个参数都有讲究。epochs=150是给足收敛空间——如果训练集只有2000多张图,100个Epoch可能不够让模型在稀有类别上完全收敛;patience=20表示验证集mAP连续20个Epoch不提升就早停,防止过拟合的同时节省时间;lr0=0.01是YOLOv8默认学习率,如果你的显卡不支持大batch,需要同步往下调学习率——常见做法是batch减半时学习率也减半,不然模型容易震荡不收敛。
关于imgsz我有过一段翻车经历:为了追求速度把输入分辨率调到416,结果小目标鱼种的AP直接掉了10个百分点。这份数据集里有些鱼是远距离拍摄的,目标可能只占几十个像素,分辨率降低后小目标的信息被严重压缩。如果你跑完发现小目标类别AP很低,优先检查分辨率,不要急着调损失函数。
4.3 类别不平衡的观察与处理:先看分布再动手
31类鱼里样本数量不会均匀。采集场景决定了某些常见养殖鱼种可能有几百张图,而某些稀缺鱼种可能只有几十张。训练前我一般会先统计各类别的目标框数量,这一步能帮你判断模型mAP低的原因到底是“模型学不会”还是“数据本身就少”。
import os, glob from collections import Counter class_counter = Counter() label_files = glob.glob('yolo_fish_dataset/labels/train/*.txt') for label_file in label_files: with open(label_file, 'r') as f: for line in f: line = line.strip() if not line: continue class_id = int(line.split()[0]) class_counter[class_id] += 1 total_boxes = sum(class_counter.values()) for class_id, count in class_counter.most_common(): percentage = count / total_boxes * 100 print(f'类别 {class_id}: {count} 个框, 占比 {percentage:.1f}%')统计结果如果出现某个类别占比低于1%的情况,比如总共8000个框里某个鱼类只占30个框,你可以考虑三种处理策略。第一种是简单粗暴的数据增强——给这类图片做随机裁剪、旋转、色彩抖动,扩充样本量;第二种是给损失函数加类别权重,YOLOv8官方不支持直接设权重,常见做法是在数据集的txt复制若干份来“物理过采样”;第三种是把该类别的目标框从别的图片里用复制粘贴的方式增强(copy-paste增强)。不要一开始就尝试复杂方案,先复制少量样本看mAP是否提升,再决定要不要进一步扩充。
4.4 训练日志指标解读:怎么判断训练在正常收敛
训练时终端会实时打印box_loss、cls_loss、dfl_loss和验证集mAP。很多新手只盯mAP,但mAP在训练早期可能长时间不动,导致误判训练失效。我一般看两个指标:cls_loss是否持续下降,以及验证集mAP50是否呈现波动上升。如果cls_loss下降但验证mAP不上升,大概率是过拟合前兆;如果两个指标都在震荡但幅度大,检查学习率是不是设高了,或者batch size太小导致梯度不稳定。
训练结束后,runs/detect/exp_fish_n/目录下会生成weights/best.pt和weights/last.pt。best.pt是验证集mAP最好的权重,last.pt是最后一个Epoch的权重——重新训练时用last.pt做初值可以加速收敛,但做推理和部署只能用best.pt。
5. 避坑实录:标注错位、类别遗漏与半成品数据的五条排查记录
5.1 txt文件为空或缺失:训练时labels读取报错
- 现象:训练跑到中途,终端报
FileNotFoundError或Empty label file,部分图片的txt文件找不到或者内容是空的。 - 原因:原始资源里不是每张图都有有效目标框。某些鱼类图片可能是近景特写,目标超出画面太多,标注员直接跳过不标;还有可能是转换脚本漏处理了部分文件,导致txt生成不全。
- 解决:训练前强制执行一次完整性检查,把xml文件里实际包含object的和txt文件一一比对,找出缺失或空txt的图片ID,决定是丢弃图片还是从xml手动生成txt。我写过一个小脚本,用
xml.etree.ElementTree解析xml里的object数量,os.path.getsize()检查txt大小,两边对不上的直接输出ID清单。
5.2 类别名称带空格:txt的class_id与yaml的name对不上
- 现象:训练能跑,loss也能降,但推理结果乱七八糟,比如输入Gold Fish的图输出Big Head Carp。
- 原因:VOC格式里类别名是字符串,比如
Gold Fish、Grass Carp,中间带空格没问题。但如果转换脚本在处理时用了str.split()切分,没有妥善处理类别文本,空格会被当作分隔符,导致ID映射错位。 - 解决:检查txt文件第一列数字是否在0到30之间——如果出现大于30的值,说明转换时把类别字符串的错误部分当成目标框坐标读进去了。此外要严格按
fish_classes.txt的映射文件生成yaml里的names,不要靠肉眼对齐。
5.3 图片实际尺寸与XML的size字段不一致
- 现象:训练过程中损失值正常下降,但推理时目标框位置整体偏移,框总比鱼的实际位置偏左上或偏右下。
- 原因:XML里记录的
width和height与jpg图片实际像素尺寸不一致。常见场景是图片被压缩过,但XML标注是从原始图片生成的;或者是图片预处理时做了等比缩放但标注没同步更新。归一化坐标是拿bndbox除以size得到的,分母错了,归一化结果自然全错。 - 解决:解压后先跑一遍图集校验,用
PIL.Image.open读取图片真实的宽高,和XML里size字段比对。不一致的图片重新生成txt。这个校验只影响YOLO格式,VOC格式的标注依然是有效的。
5.4 多个目标重叠时漏标:小目标AP异常低
- 现象:模型mAP50整体看着还行,但某个类别单独拎出来AP只有十几,明显低于其他类别。
- 原因:鱼群密集场景下,标注时只标了大的、明显的目标,被遮挡的小鱼没标。这类漏标目标在训练时被当成背景,模型学到的是“这种鱼有遮挡就不算目标”,推理时自然检测不到。
- 解决:找出AP异常的类别,回原图检查xml标注的bndbox数量。如果发现确实有大量未标注的小目标,需要人工补标——这活没有捷径,但可以用
ultralytics的预测结果配合半自动标注工具加速。对于检测鱼群这种密集场景,还可以尝试增大imgsz并开启augment里的马赛克增强来提升小目标表现。
5.5 数据划分后验证集类别缺失
- 现象:训练loss正常下降,但验证集mAP表现极其不稳定,同一个权重跑两次推理,得分波动超过5%。
- 原因:随机划分时把某个稀有类别的所有样本全分到了训练集,验证集完全没有该类别,或者反过来——验证集里只有几张该类别图片,mAP计算时方差大。这种情况在类别分布极不均衡的数据集里非常常见。
- 解决:划分前统计每个类别的样本数,用分层抽样保证训练集与验证集都覆盖所有31类。最简单的做法是用
sklearn.model_selection的train_test_split加stratify参数,以xml标签作为分层依据。
6. 用预测结果反向校验数据集:一次推理脚本就能确认标注是否可信
训练结束后不要急着收工,我会跑一个简单但有效的验证:用best.pt对训练集本身做一次推理,把检测结果和原本的txt标注叠加对比。这一步能帮你确认两件事——标注是否正确映射、模型是否真的学进去了。如果模型在训练集上都检不全,那说明标注或配置有问题,而不是模型能力问题。
from ultralytics import YOLO import cv2, os model = YOLO('fish_yolo_runs/exp_fish_n/weights/best.pt') # 只抽查训练集里的10张图,展示推理框与标注框的重合情况 img_dir = 'yolo_fish_dataset/images/train' label_dir = 'yolo_fish_dataset/labels/train' sample_ids = os.listdir(img_dir)[:10] for img_name in sample_ids: img_path = os.path.join(img_dir, img_name) img_id = img_name.split('.')[0] label_path = os.path.join(label_dir, img_id + '.txt') # 读取原始标注框并画到图上 img = cv2.imread(img_path) with open(label_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue _, cx, cy, w, h = parts cx, cy, w, h = float(cx), float(cy), float(w), float(h) x1 = int((cx - w / 2) * img.shape[1]) y1 = int((cy - h / 2) * img.shape[0]) x2 = int((cx + w / 2) * img.shape[1]) y2 = int((cy + h / 2) * img.shape[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 预测并画推理框 results = model.predict(img_path, conf=0.4, verbose=False)[0] for box in results.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(f'check_{img_id}.jpg', img)这段脚本里,绿色框是原始txt标注的归一化坐标反算出来的像素框,红色框是模型预测框。我运行后重点看两类情况:一是绿框和红框大面积不重合,这说明标注本身就有问题,或者模型的置信度阈值设得不对;二是绿框存在但红框完全没有,说明模型漏检了训练集里的已知目标——这往往是模型欠拟合或者目标尺寸太小导致。
这10张图输出后,我会顺手再抽20张验证集图片跑同样的对比。验证集上的重合度如果明显低于训练集,那就是模型过拟合了,需要回训练参数里调整正则化或减少Epoch。不要嫌这一步麻烦——一个数据集到底值不值得信任,最直接的证据就是模型和标注之间的“互认度”。
从那以后,我每次拿到新数据集都强制走一遍这套流程:先校验文件完整性,再检查类别映射,划分数据集,训练一轮,最后用推理脚本反向验证标注可信度。这套流程看起来多花了一个小时,但省掉的却是模型训完才发现数据有问题、全部重来的那种大翻车。这份鱼数据集整体质量在同类资源里是够用的,希望这套使用路径能帮到你。
本文还有配套的精品资源,点击获取