简介:这份文物目标检测数据集面向文化遗产保护、智慧博物馆建设及遥感监测等方向的算法开发者与研究人员,提供可直接投入YOLO系列模型训练的标注数据,帮助解决文物自动识别、考古现场清点与遗址巡检等实际问题。资源包共1596个文件,以797张jpg图像与797个同名txt标注文件为主体,另附1个yaml数据配置和1份docx说明文档,压缩包约29.29MB,训练集619张、验证集126张、测试集52张,标注统一为YOLO格式边界框,兼容YOLOv5/v7/v8及yolov12等主流框架。目前已有110人学习下载。数据集单图平均含3至8个标注实例,覆盖不同角度与遮挡状态,兼顾小目标与多尺度文物部件,并同时适配地面拍摄与航拍视角,读者可据此快速搭建文物检测基线、验证复杂场景下的模型泛化能力,或将其迁移至工业、农业、医药等跨领域检测任务中。
1. 文物目标检测数据集.zip:从解压到跑通第一条训练命令
拿到一个名为「文物目标检测数据集.zip」的压缩包,多数人的第一反应是解压看目录,然后卡在标注格式上——这是我在做博物馆数字化项目时反复见到的场景。文物目标检测和常规 COCO、VOC 场景最大的区别在于:类别极度不均衡,一件青铜器和一枚钱币的像素占比可能差两个数量级;背景干扰强,展柜玻璃反光、标签牌、参观者剪影都会混进正样本;标注粒度细,鼎的耳、足、纹饰区域经常被不同标注者切成不同框。这个数据集要解决的就是「让模型在展厅实拍图里稳定框出文物主体」这件事,适合做文博数字化、智慧博物馆、藏品盘点方向的工程师,也适合想拿一个非通用场景练手 YOLO 系列微调的人。下面按「先看清数据长什么样、再决定怎么转、最后跑通训练」的顺序讲,每一步都给可复现的命令和参数。
2. 解压后先做三件事:目录结构、类别分布、标注格式
2.1 用一条命令摸清目录骨架
不要急着写 dataloader,先把压缩包解开,用tree或find把结构打出来。文物数据集常见的组织方式有两种:一种是images/和labels/平行存放,另一种是按train/val/test再分子目录。先确认属于哪种,后面所有路径都依赖这个判断。
unzip 文物目标检测数据集.zip -d wenwu_dataset cd wenwu_dataset find . -maxdepth 3 -type d | sort find . -type f -name "*.jpg" | head -5 find . -type f -name "*.xml" | head -5 find . -type f -name "*.txt" | head -5逻辑说明:find -maxdepth 3 -type d只列三层目录,避免文物数据集动辄上万张图导致输出刷屏;后面三条分别探测 jpg、xml、txt 三种文件是否存在,用来判断标注是 VOC XML、YOLO txt 还是 COCO json。参数上-maxdepth按实际层级调整,如果目录嵌套深就加到 4。
如果输出里同时有annotations.json和images/,那大概率是 COCO 格式;如果每个 jpg 旁边有个同名 xml,就是 VOC;如果只有labels/下的 txt,且每行是class x_center y_center w h,那就是已经转好的 YOLO 格式,可以直接跳到第 4 章。
2.2 统计类别分布,先看长尾有多长
文物数据集的类别数通常不大,十几到几十类,但分布极偏。先统计每个类别的框数量,决定后面要不要做重采样或类别加权。
import os, glob from collections import Counter import xml.etree.ElementTree as ET label_dir = "wenwu_dataset/annotations" # 按实际路径改 counter = Counter() for xml_path in glob.glob(os.path.join(label_dir, "*.xml")): tree = ET.parse(xml_path) for obj in tree.findall("object"): name = obj.find("name").text.strip() counter[name] += 1 for name, cnt in counter.most_common(): print(f"{name}\t{cnt}") print("总类别数:", len(counter)) print("总框数:", sum(counter.values()))逻辑说明:遍历所有 XML,用findall("object")取每个标注框的name字段累加。参数上label_dir换成你的实际标注目录。输出后重点看两件事:最多的类是不是超过最少的类 50 倍以上,以及有没有只出现个位数的类别。前者决定训练时要不要用copy_paste或mosaic增强,后者决定要不要直接合并到「其他文物」大类,否则模型根本学不动。
提示:文物类别名里经常有空格、括号、中文,转 YOLO 前必须做一次名称规范化,否则后面
data.yaml的names列表会对不上。
2.3 判断标注质量:三个快速抽检动作
文物标注最容易出问题的地方是框贴边、框套框、漏标。抽 20 张图做三件事:一是看有没有宽或高小于 5 像素的框,这种框训练时是噪声;二是看有没有两个框 IoU 超过 0.8,多半是重复标注;三是随机挑几张展柜反光强的图,确认文物主体有没有被漏掉。
import glob, os import xml.etree.ElementTree as ET def box_stats(xml_path): tree = ET.parse(xml_path) boxes = [] for obj in tree.findall("object"): b = obj.find("bndbox") x1, y1 = int(b.find("xmin").text), int(b.find("ymin").text) x2, y2 = int(b.find("xmax").text), int(b.find("ymax").text) boxes.append((x1, y1, x2, y2)) return boxes tiny, overlap = 0, 0 for p in glob.glob("wenwu_dataset/annotations/*.xml")[:200]: boxes = box_stats(p) for (x1, y1, x2, y2) in boxes: if (x2 - x1) < 5 or (y2 - y1) < 5: tiny += 1 for i in range(len(boxes)): for j in range(i + 1, len(boxes)): ax1, ay1, ax2, ay2 = boxes[i] bx1, by1, bx2, by2 = boxes[j] ix1, iy1 = max(ax1, bx1), max(ay1, by1) ix2, iy2 = min(ax2, bx2), min(ay2, by2) if ix1 < ix2 and iy1 < iy2: inter = (ix2 - ix1) * (iy2 - iy1) area_a = (ax2 - ax1) * (ay2 - ay1) area_b = (bx2 - bx1) * (by2 - by1) if inter / min(area_a, area_b) > 0.8: overlap += 1 print("过小框:", tiny, "高重叠框对:", overlap)逻辑说明:只抽前 200 个文件做快速体检,避免全量遍历太慢。tiny统计宽高小于 5 像素的框,overlap用交集除以较小面积判断重复标注。参数上 5 像素和 0.8 是经验阈值,文物小件多的话可以把 5 调到 3。如果过小框占比超过 5%,建议在转换阶段直接过滤掉。
3. 把 VOC 标注转成 YOLO 格式:转换脚本与四个边界坑
3.1 转换脚本:坐标归一化与类别映射
YOLO 格式要求每行class_id x_center y_center width height,且四个值都是相对图像宽高的 0 到 1 浮点数。文物数据集如果给的是 VOC XML,必须做这一步。
import os, glob import xml.etree.ElementTree as ET from PIL import Image classes = ["青铜器", "陶瓷", "书画", "玉器", "钱币", "其他文物"] # 按实际类别改 class_to_id = {c: i for i, c in enumerate(classes)} src_img = "wenwu_dataset/images" src_xml = "wenwu_dataset/annotations" dst_img = "wenwu_yolo/images/train" dst_lbl = "wenwu_yolo/labels/train" os.makedirs(dst_img, exist_ok=True) os.makedirs(dst_lbl, exist_ok=True) for xml_path in glob.glob(os.path.join(src_xml, "*.xml")): stem = os.path.splitext(os.path.basename(xml_path))[0] img_path = os.path.join(src_img, stem + ".jpg") if not os.path.exists(img_path): continue w, h = Image.open(img_path).size tree = ET.parse(xml_path) lines = [] for obj in tree.findall("object"): name = obj.find("name").text.strip() if name not in class_to_id: continue b = obj.find("bndbox") x1 = max(0, int(b.find("xmin").text)) y1 = max(0, int(b.find("ymin").text)) x2 = min(w, int(b.find("xmax").text)) y2 = min(h, int(b.find("ymax").text)) if x2 - x1 < 3 or y2 - y1 < 3: continue xc = (x1 + x2) / 2 / w yc = (y1 + y2) / 2 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{class_to_id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") if not lines: continue with open(os.path.join(dst_lbl, stem + ".txt"), "w") as f: f.write("\n".join(lines)) Image.open(img_path).save(os.path.join(dst_img, stem + ".jpg"))逻辑说明:先读图像真实宽高,再把 VOC 的绝对坐标转成归一化中心点加宽高。max(0, ...)和min(w, ...)是防止标注越界,文物数据集里展柜边缘的框经常超出图像边界。if x2 - x1 < 3过滤掉过小框,和第 2 章体检结果呼应。参数上classes列表顺序必须和后面data.yaml的names完全一致,否则类别全错。
3.2 坑一:图像和标注文件名大小写不一致
现象是转换后labels/里文件数远少于images/。原因是 XML 里写的是IMG_001.JPG,实际文件是img_001.jpg。解决方式是在匹配前统一转小写,或者用glob时加case_sensitive=False。我一般会在转换脚本开头加一句stem = stem.lower(),两边都按小写匹配。
3.3 坑二:中文类别名导致 data.yaml 读取乱码
现象是训练时报KeyError或类别名显示成问号。原因是 YOLO 训练脚本读data.yaml时默认按 UTF-8,但有些 Windows 下生成的 yaml 是 GBK。解决方式是统一用 UTF-8 保存,并在 yaml 里把中文类别名用引号包起来。如果框架对中文支持不稳,直接映射成class_0到class_n,在文档里另附对照表。
3.4 坑三:train/val 划分按图像随机,导致同展柜泄漏
现象是验证集指标虚高,实际部署掉点。原因是同一件文物的多角度照片被随机分到了训练和验证两边。正确做法是按「文物编号」或「展柜编号」分组划分,同一件文物的所有照片只能进同一侧。常见做法是先按文件名前缀分组,再用GroupShuffleSplit。
from sklearn.model_selection import GroupShuffleSplit import glob, os, shutil files = glob.glob("wenwu_yolo/images/train/*.jpg") groups = [os.path.basename(f).split("_")[0] for f in files] # 按前缀分组 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(files, groups=groups))逻辑说明:groups取文件名前缀作为文物编号,GroupShuffleSplit保证同组不跨集。参数test_size=0.2按数据量调,文物数据集通常几千张,0.2 够用。
3.5 坑四:转换后没做可视化抽检
现象是训练 loss 正常下降但框全偏。原因是归一化时用错了宽高,比如把 PIL 的size返回的(w, h)当成(h, w)。解决方式是转换后随机抽 10 张,把 YOLO 坐标反算回像素画框,肉眼确认。
import cv2, glob, random for p in random.sample(glob.glob("wenwu_yolo/images/train/*.jpg"), 10): img = cv2.imread(p) h, w = img.shape[:2] lbl = p.replace("images", "labels").replace(".jpg", ".txt") if not os.path.exists(lbl): continue for line in open(lbl): c, xc, yc, bw, bh = map(float, line.split()) 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), (0, 255, 0), 2) cv2.imwrite(f"check_{os.path.basename(p)}", img)逻辑说明:反算公式是(xc - bw/2) * w,和转换时的正算严格对应。抽检图存到当前目录,肉眼过一遍再开始训练,这一步能省掉后面几小时的无效训练。
4. 用 YOLOv8 跑通第一条训练命令:data.yaml 与关键参数
4.1 data.yaml 的最小写法
YOLOv8 训练只认一个 yaml,里面写清路径、类别数和类别名。文物数据集路径建议用绝对路径,避免相对路径在不同工作目录下失效。
path: /data/wenwu_yolo train: images/train val: images/val nc: 6 names: 0: 青铜器 1: 陶瓷 2: 书画 3: 玉器 4: 钱币 5: 其他文物逻辑说明:path是数据集根目录,train和val相对path。nc必须等于names的长度,且顺序和转换脚本里的classes一致。参数上如果类别名有中文,确保文件保存为 UTF-8。
4.2 第一条训练命令与参数含义
yolo detect train \ data=/data/wenwu_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=wenwu_runs \ name=exp1逻辑说明:model=yolov8n.pt用 nano 版先跑通流程,文物数据集通常几千张,nano 足够验证 pipeline。imgsz=640是默认值,如果文物小件多可以提到 1024,但显存要够。batch=16按显存调,8G 显存跑 640 大概能到 16。lr0=0.01是初始学习率,微调场景可以降到 0.001。patience=20表示 20 轮没提升就早停,省时间。project和name决定权重存到wenwu_runs/exp1/weights/。
注意:第一次跑先用 10 个 epoch 验证流程通不通,别直接上 100。文物数据集如果类别极偏,前几轮 loss 可能不降,看
cls_loss而不是总 loss。
4.3 训练中该盯哪几个指标
box_loss反映框回归,cls_loss反映分类,dfl_loss是分布焦点损失。文物场景里cls_loss通常降得比box_loss慢,因为类别难分。如果cls_loss震荡不降,先检查类别映射有没有错,再考虑加类别权重。验证集看mAP50和mAP50-95,文物小件多的话mAP50-95会明显低于mAP50,这是正常的,因为小框对 IoU 阈值敏感。
4.4 从训练日志判断要不要调参
如果训练集 loss 降但验证集 loss 升,是过拟合,加增强或减模型容量。如果两者都不降,是欠拟合或学习率不对,先调lr0。如果mAP50高但mAP50-95低,说明框位置不够准,可以试imgsz提到 1024 或换更大的模型。文物数据集常见的情况是验证集mAP50到 0.7 左右就上不去,这时候回头看第 2 章的类别分布,多半是长尾类别拖后腿。
5. 避坑与排查:文物数据集训练翻车的五个真实场景
5.1 现象:训练正常但推理时框全堆在图像中心
原因:转换时把x_center和y_center写反,或者归一化时用了错误的宽高顺序。模型学到的是错误分布,推理时自然偏。解决:用第 3.5 节的可视化脚本反算,确认框位置和原图一致再训练。这个坑我踩过两次,血泪经验是转换后必做可视化。
5.2 现象:验证集 mAP 很高,部署到新展柜照片上全漏
原因:训练集和验证集来自同一批展柜,背景、光照、角度高度相似,模型记住了背景而不是文物。解决:按展柜或拍摄批次分组划分,验证集要包含训练集没见过的展柜。如果数据不够,至少留一个展柜完全不出现在训练集。
5.3 现象:小件文物(钱币、印章)几乎检测不到
原因:小目标在 640 分辨率下像素太少,且长尾类别样本不足。解决:把imgsz提到 1024 或 1280,对小件类别做过采样,或者用copy_paste增强把小件贴到更多背景上。参数上 YOLOv8 的mosaic对小目标有帮助,但文物场景里mosaic可能把不同展柜的图拼在一起造成语义混乱,建议mosaic=0.5而不是默认的 1.0。
5.4 现象:训练到一半 loss 突然变 NaN
原因:学习率太高,或者标注里有宽高为 0 的框导致除零。解决:先检查转换后的 txt 有没有0.000000的宽高,有就过滤掉;再把lr0降到 0.001 重跑。文物数据集里展柜边缘的框容易被标成零宽,转换时的if x2 - x1 < 3能挡掉大部分。
5.5 现象:data.yaml 路径对但报「No labels found」
原因:YOLO 要求images和labels目录结构严格对应,即images/train/xxx.jpg对应labels/train/xxx.txt。如果 labels 放在了images同级但目录名不同,或者 txt 文件名和 jpg 不一致,就会报这个。解决:用find确认两边文件名一一对应,目录名必须是images和labels,不能改成imgs和lbls。
6. 进阶:用类别加权和分层采样把长尾类别拉回来
跑通基线后,真正决定文物检测能不能落地的是长尾类别。我一般会做两件事:一是在损失里给稀有类别加权,二是用分层采样保证每个 batch 里稀有类别都有出现。YOLOv8 本身不直接暴露类别权重参数,但可以通过自定义 dataset 或在train时用class_weights的思路改cls_loss。更简单的做法是过采样:把稀有类别的图像在文件列表里重复若干次,重复次数和类别频率成反比。
import glob, os, random from collections import Counter lbl_files = glob.glob("wenwu_yolo/labels/train/*.txt") cls_count = Counter() file_cls = {} for lf in lbl_files: cls_in_file = set() for line in open(lf): c = int(line.split()[0]) cls_count[c] += 1 cls_in_file.add(c) file_cls[lf] = cls_in_file max_cnt = max(cls_count.values()) weights = {c: max_cnt / cnt for c, cnt in cls_count.items()} sampled = [] for lf, cs in file_cls.items(): w = max(weights[c] for c in cs) sampled.extend([lf] * int(w)) random.shuffle(sampled) print("原始文件数:", len(lbl_files), "过采样后:", len(sampled))逻辑说明:先统计每个类别的框数,再算权重max_cnt / cnt,每个文件按它包含的最稀有类别取权重,重复加入列表。参数上int(w)会截断,稀有类别可能重复几十次,建议加个上限比如min(int(w), 10)防止过采样过头导致过拟合。过采样后重新生成 train 列表,再跑一次训练对比mAP50-95。
验证方法上,我习惯留一个「困难集」:专门挑反光强、遮挡多、小件密集的图,不参与训练,只用来做最终验收。基线模型和加权模型都在这个集上跑一遍,看稀有类别的召回率有没有提升。如果提升不明显,说明问题不在类别不平衡,而在分辨率或标注质量,回头查第 2 章的体检结果。
最后说个习惯:每次改完数据或参数,先跑 10 个 epoch 看趋势,别直接上 100。文物数据集不大,10 个 epoch 几分钟就能看出方向对不对,省下的时间够你多试三组参数。希望帮到你。
本文还有配套的精品资源,点击获取