
简介本资源为印章检测数据集采用Pascal VOC与YOLO双格式标注面向从事目标检测算法练习、印章识别模型训练的学生与开发者可用于快速搭建单类别检测任务的数据基础。压缩包共632个文件包含210张jpg图片、210个VOC格式xml标注文件、210个YOLO格式txt标注文件及少量说明文本整体约36.47MB图片与标注一一对应便于直接投入训练流程。标注类别仅seal一类共488个矩形框使用labelImg完成画框标注标注规则统一适合作为入门级检测数据或小样本实验素材。目前已有919人学习下载数据规模适中便于快速验证模型效果与调试训练流程。需要说明的是数据集仅提供准确合理的标注不对训练所得模型或权重文件的精度作任何保证使用者需自行评估与调优。1. 印章检测数据集实测210 张 VOCYOLO 双格式到底能跑出什么手里只有两百来张图还想把印章检测跑通这事听着像玄学但我最近确实拿一份 210 张的印章数据集走了一遍完整流程。它叫「印章检测数据集VOCYOLO格式210张1类别」图片、VOC 的 xml、YOLO 的 txt 各 210 个类别只有一个seal总框数 488标注工具是 labelImg画的是矩形框。说白了这是一份小而专的单类目标检测数据适合做印章定位的原型验证、教学演示或者给已有模型做一次小样本微调。它不适合指望直接刷高精度也不承诺任何权重文件的精度只保证标注本身合理。如果你正卡在「找不到干净的单类印章数据」这一步这份资源能让你先把训练管线跑起来再谈调优。2. 拆开这份数据集VOC 与 YOLO 双格式的目录结构与字段含义2.1 210 张图、488 个框先看清数据分布拿到压缩包第一件事不是急着训练而是把数据摸清楚。这份数据集的核心数字很明确jpg 图片 210 张xml 标注 210 个txt 标注 210 个类别数 1类别名seal框数 488。平均下来每张图约 2.3 个印章框说明不少图片里存在多个印章这对单类检测来说反而是好事能逼着模型学会区分相邻目标。需要特别注意的是摘要里写得很清楚不包含分割路径的 txt 文件只有 jpg 图片加对应的 VOC xml 和 YOLO txt。也就是说没有train.txt、val.txt这类划分文件也没有classes.txt。这两样东西得你自己生成后面章节会给出脚本。很多人下载完发现 YOLO 训练报「找不到标签」八成就是缺了划分文件和类别文件。常见做法是先把目录整理成下面这种结构VOC 和 YOLO 分开放避免路径混淆dataset/ ├── images/ # 210 张 jpg ├── annotations/ # 210 个 VOC xml └── labels/ # 210 个 YOLO txt整理完先数一遍文件数三个目录都应该是 210少一个都别往下走。我一般会跑一句ls images | wc -l和ls labels | wc -l对一下数字对不上就说明解压或复制时丢了文件。2.2 VOC xml 里到底存了什么VOC 格式的 xml 是 labelImg 的标准输出结构固定。打开任意一个 xml你会看到folder、filename、size、object这几层。size里的width、height、depth是原图尺寸object里则是name、pose、truncated、difficult和bndbox。这份数据类别名统一是sealbndbox里是xmin、ymin、xmax、ymax四个坐标。这里有个容易被忽略的点VOC 的坐标是左上角和右下角的绝对像素值而 YOLO 要的是归一化后的中心点加宽高。两者不是简单换个名字转换时一旦把xmax和xmin写反框就会翻到图像外面去。下面这段解析逻辑我用了很多次能直接把 xml 读成字典import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) boxes [] for obj in root.iter(object): name obj.find(name).text bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes逻辑说明先取原图宽高再遍历每个object把类别名和四个坐标取出来。参数上w、h用于后续归一化boxes里存的是绝对坐标。注意float转换不能省有些 xml 里坐标带小数点直接int会截断导致框偏移一两个像素小目标上这点偏差会被放大。2.3 YOLO txt 的五个字段与类别索引YOLO 格式的 txt 每行五个值class_id center_x center_y width height全部归一化到 0 到 1 之间。这份数据只有一个类别seal所以class_id恒为 0。归一化公式是center_x (xmin xmax) / 2 / wcenter_y (ymin ymax) / 2 / hwidth (xmax - xmin) / wheight (ymax - ymin) / h转换脚本在下一章给。这里先记住一个边界如果xmax等于w归一化后center_x width/2会正好等于 1有些训练框架对等于 1 的边界容忍度不同稳妥做法是转换后统一clip到[0, 1]。我见过因为一个框贴边没裁剪训练时 loss 直接变 NaN 的情况排查了半天才发现是坐标越界。3. 从 VOC 到 YOLO转换脚本、划分文件与训练配置3.1 一条命令完成 VOC 转 YOLO转换是这份数据集落地的第一道坎。下面这个脚本把annotations里的 xml 批量转成labels里的 txt同时生成classes.txtimport os import xml.etree.ElementTree as ET VOC_DIR dataset/annotations OUT_DIR dataset/labels CLASSES [seal] os.makedirs(OUT_DIR, exist_okTrue) def convert(xml_path, out_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue cls_id CLASSES.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 裁剪到 [0,1]避免贴边框越界 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) bw min(max(bw, 0.0), 1.0) bh min(max(bh, 0.0), 1.0) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) for fn in os.listdir(VOC_DIR): if fn.endswith(.xml): convert(os.path.join(VOC_DIR, fn), os.path.join(OUT_DIR, fn.replace(.xml, .txt))) with open(dataset/classes.txt, w) as f: f.write(\n.join(CLASSES))逻辑说明遍历 xml按类别名取索引算出归一化中心点和宽高写入同名 txt。参数上CLASSES必须和标注里的name完全一致大小写都不能差:.6f保留六位小数足够训练用。转换完抽查几个 txt确认每行五个值、没有空行、没有负值。如果某个 txt 是空的说明那张图里没有seal框这种图要么删掉要么在划分时排除否则训练时会被当成负样本。3.2 生成 train/val 划分文件数据集没给划分文件得自己切。210 张图常见做法是 8:2即 168 张训练、42 张验证。下面脚本按文件名排序后切分保证可复现import os import random IMG_DIR dataset/images random.seed(42) imgs sorted([f for f in os.listdir(IMG_DIR) if f.endswith(.jpg)]) random.shuffle(imgs) split int(len(imgs) * 0.8) train, val imgs[:split], imgs[split:] with open(dataset/train.txt, w) as f: f.write(\n.join([os.path.join(images, i) for i in train])) with open(dataset/val.txt, w) as f: f.write(\n.join([os.path.join(images, i) for i in val])) print(ftrain: {len(train)}, val: {len(val)})逻辑说明固定随机种子42保证每次切分结果一致方便复现实验。参数上0.8是训练比例样本少的时候可以调到0.85但验证集别少于 30 张否则评估指标波动会很大。生成的两个 txt 里存的是相对路径训练时把dataset作为根目录即可。3.3 YOLOv8 训练配置与关键参数划分文件有了就可以起训练。以 YOLOv8 为例先写一个seal.yamlpath: dataset train: train.txt val: val.txt nc: 1 names: [seal]然后一条命令开跑yolo detect train dataseal.yaml modelyolov8n.pt epochs100 imgsz640 batch16参数说明modelyolov8n.pt用 nano 版预训练权重210 张图用大模型容易过拟合epochs100对小数据集够用配合早停imgsz640是常见输入尺寸如果原图分辨率远大于 640印章又小可以提到960或1280但显存要跟上batch16在 8G 显存上比较稳爆显存就降到 8。训练完看runs/detect/train下的results.csv重点盯mAP50和mAP50-95如果mAP50一直上不去先回头查标注别急着换模型。4. 小样本单类检测的避坑与排查清单4.1 训练 loss 变 NaN现象训练几个 epoch 后 loss 突然变成nan之后再也不下降。原因多半是标注坐标越界归一化后出现大于 1 或小于 0 的值或者某个框宽高为 0。解决转换脚本里加clip到[0,1]并过滤掉bw或bh为 0 的行。我一般会在转换后跑一句统计把异常 txt 挑出来单独看。4.2 模型把背景当印章现象验证时框出一堆明显不是印章的区域误检高。原因210 张图里负样本太少或者某些图里印章极小模型学不到判别特征。解决检查是否有空 txt 的图被放进训练集如果有保留少量作为负样本是好事但别太多另外可以适当提高imgsz让小目标在特征图上占更多像素。4.3 验证集 mAP 波动大现象每次跑valmAP50差好几个点。原因验证集只有 42 张样本量太小指标本身就不稳。解决把随机种子固定划分文件一次生成后不再变评估时多看几轮的平均值别拿单次结果下结论。如果条件允许做 5 折交叉验证虽然费时间但结论可靠得多。4.4 类别名大小写不一致现象训练报「class name not found」或类别索引错乱。原因xml 里写的是sealclasses.txt里写成了Seal或者 YAML 里names写错。解决统一用小写seal转换脚本里加一句断言遇到不在CLASSES里的name直接报错别静默跳过。静默跳过最坑你以为转了 488 个框实际可能只转了 400 个。4.5 图片和标签文件名对不上现象训练时提示找不到某些标签。原因jpg 是firc_yz_176.jpgtxt 却是firc_yz_176.JPG.txt或少了前缀。解决转换时用os.path.splitext取主文件名确保 txt 和 jpg 主名完全一致。整理完跑一遍集合比对把只在 images 里、不在 labels 里的文件名打印出来逐个处理。5. 进阶技巧用 488 个框反推标注质量与数据增强边界数据量小的时候增强策略不能照搬大数据的套路。210 张图、488 个框翻转和轻微缩放是安全的但大幅裁剪要小心印章被裁掉一半会引入错误标签。我的习惯是先用一个脚本把框的宽高分布画出来看看有没有极端值。import os LABEL_DIR dataset/labels ws, hs [], [] for fn in os.listdir(LABEL_DIR): with open(os.path.join(LABEL_DIR, fn)) as f: for line in f: parts line.strip().split() if len(parts) 5: ws.append(float(parts[3])) hs.append(float(parts[4])) print(f框数: {len(ws)}) print(f宽 min/max: {min(ws):.4f} / {max(ws):.4f}) print(f高 min/max: {min(hs):.4f} / {max(hs):.4f})逻辑说明统计所有归一化宽高看分布。如果min接近 0说明有极小框增强时别用会进一步缩小目标的策略如果max接近 1说明有框几乎占满整图裁剪增强容易把它切坏。参数上len(ws)应该等于 488对不上就说明转换或读取有问题。验证标注质量的另一个办法是可视化。把原图和框画在一起随机抽 20 张看一遍import cv2 import os IMG_DIR dataset/images LABEL_DIR dataset/labels sample sorted(os.listdir(IMG_DIR))[:20] for fn in sample: img cv2.imread(os.path.join(IMG_DIR, fn)) h, w img.shape[:2] txt os.path.join(LABEL_DIR, fn.replace(.jpg, .txt)) if os.path.exists(txt): with open(txt) as f: for line in f: c, cx, cy, bw, bh map(float, line.split()) 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.imwrite(fvis_{fn}, img)逻辑说明把归一化坐标还原成像素坐标画框保存。参数上sample取前 20 张实际可以随机抽。看可视化时重点检查框有没有明显偏移、有没有漏标、有没有把非印章区域框进去。这一步花十分钟能省下后面几小时的无效训练。增强策略上我一般只用水平翻转、±10% 的随机缩放和轻微色彩抖动。旋转要谨慎印章本身有方向性大角度旋转可能让模型学到错误的方向先验。Mosaic 增强在小数据集上效果不错但要注意它会把四张图拼一起如果原图里印章本来就小拼完更小反而增加难度。可以先用默认增强跑一版再关掉 Mosaic 跑一版对比mAP50决定。从那以后我每次拿到小数据集都强制先跑一遍框分布统计和可视化再动训练命令。这份 210 张的印章数据不算大但胜在干净、单类、双格式齐全把转换、划分、训练、排查这条链路走通比直接拿一个大而杂的数据集更有收获。希望帮到你。本文还有配套的精品资源点击获取