news 2026/10/2 2:38:07

印章检测数据集VOC+YOLO格式210张:小样本单类检测实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
印章检测数据集VOC+YOLO格式210张:小样本单类检测实战指南

简介:本资源为印章检测数据集,采用Pascal VOC与YOLO双格式标注,面向从事目标检测算法练习、印章识别模型训练的学生与开发者,可用于快速搭建单类别检测任务的数据基础。压缩包共632个文件,包含210张jpg图片、210个VOC格式xml标注文件、210个YOLO格式txt标注文件及少量说明文本,整体约36.47MB,图片与标注一一对应,便于直接投入训练流程。标注类别仅seal一类,共488个矩形框,使用labelImg完成画框标注,标注规则统一,适合作为入门级检测数据或小样本实验素材。目前已有919人学习下载,数据规模适中,便于快速验证模型效果与调试训练流程。需要说明的是,数据集仅提供准确合理的标注,不对训练所得模型或权重文件的精度作任何保证,使用者需自行评估与调优。

1. 印章检测数据集实测:210 张 VOC+YOLO 双格式到底能跑出什么

手里只有两百来张图,还想把印章检测跑通,这事听着像玄学,但我最近确实拿一份 210 张的印章数据集走了一遍完整流程。它叫「印章检测数据集VOC+YOLO格式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。这份数据类别名统一是seal,bndbox里是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 / w
  • center_y = (ymin + ymax) / 2 / h
  • width = (xmax - xmin) / w
  • height = (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.txt:

import os import xml.etree.ElementTree as ET VOC_DIR = 'dataset/annotations' OUT_DIR = 'dataset/labels' CLASSES = ['seal'] os.makedirs(OUT_DIR, exist_ok=True) 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(f'train: {len(train)}, val: {len(val)}')

逻辑说明:固定随机种子42,保证每次切分结果一致,方便复现实验。参数上,0.8是训练比例,样本少的时候可以调到0.85,但验证集别少于 30 张,否则评估指标波动会很大。生成的两个 txt 里存的是相对路径,训练时把dataset作为根目录即可。

3.3 YOLOv8 训练配置与关键参数

划分文件有了,就可以起训练。以 YOLOv8 为例,先写一个seal.yaml:

path: dataset train: train.txt val: val.txt nc: 1 names: ['seal']

然后一条命令开跑:

yolo detect train data=seal.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16

参数说明:model=yolov8n.pt用 nano 版预训练权重,210 张图用大模型容易过拟合;epochs=100对小数据集够用,配合早停;imgsz=640是常见输入尺寸,如果原图分辨率远大于 640,印章又小,可以提到960或1280,但显存要跟上;batch=16在 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 波动大

现象:每次跑val,mAP50差好几个点。原因:验证集只有 42 张,样本量太小,指标本身就不稳。解决:把随机种子固定,划分文件一次生成后不再变;评估时多看几轮的平均值,别拿单次结果下结论。如果条件允许,做 5 折交叉验证,虽然费时间,但结论可靠得多。

4.4 类别名大小写不一致

现象:训练报「class name not found」或类别索引错乱。原因:xml 里写的是seal,classes.txt里写成了Seal,或者 YAML 里names写错。解决:统一用小写seal,转换脚本里加一句断言,遇到不在CLASSES里的name直接报错,别静默跳过。静默跳过最坑,你以为转了 488 个框,实际可能只转了 400 个。

4.5 图片和标签文件名对不上

现象:训练时提示找不到某些标签。原因:jpg 是firc_yz_176.jpg,txt 却是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(f'vis_{fn}', img)

逻辑说明:把归一化坐标还原成像素坐标,画框保存。参数上,sample取前 20 张,实际可以随机抽。看可视化时重点检查框有没有明显偏移、有没有漏标、有没有把非印章区域框进去。这一步花十分钟,能省下后面几小时的无效训练。

增强策略上,我一般只用水平翻转、±10% 的随机缩放和轻微色彩抖动。旋转要谨慎,印章本身有方向性,大角度旋转可能让模型学到错误的方向先验。Mosaic 增强在小数据集上效果不错,但要注意它会把四张图拼一起,如果原图里印章本来就小,拼完更小,反而增加难度。可以先用默认增强跑一版,再关掉 Mosaic 跑一版,对比mAP50决定。

从那以后我每次拿到小数据集,都强制先跑一遍框分布统计和可视化,再动训练命令。这份 210 张的印章数据不算大,但胜在干净、单类、双格式齐全,把转换、划分、训练、排查这条链路走通,比直接拿一个大而杂的数据集更有收获。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:36:32

C++/Qt飞机大战源码拆解:从工程结构到二次开发实战

简介:一套用C编写的飞机大战游戏完整源码,适合初学C或对游戏开发感兴趣的读者学习项目结构、面向对象设计与简单游戏循环。整个压缩包共55个文件,其中13个头文件与13个源码文件构成游戏主体,覆盖飞机控制、子弹发射、敌机生成、得…

作者头像 李华
网站建设 2026/10/2 2:36:08

广州工具包生产企业怎么选好?正规源头厂家用户力荐

广州工具包生产企业怎么选好?正规源头厂家用户力荐广州康杞媛工贸有限公司是广州南沙本土专注功能性箱包研发设计与生产制造的源头工厂,为国内外客户提供工具收纳箱包、应急急救包、通用礼品箱包的定制、打样、批量生产与现货批发全链条服务,以全链路自…

作者头像 李华
网站建设 2026/10/2 2:34:58

利用电容的储能特性实现上电延时

一、前置结论电容两端的相对电压不能突变,但两端电压可以同时突变(整体被抬高/拉低)。电容两端电压为 0 时,可以把电容当成一根导线。上电瞬间电容来不及充电,电压仍是 0 —— 这正是延时效果的来源。二、为什么电容能…

作者头像 李华
网站建设 2026/10/2 2:34:41

2026年厦门靠谱的小程序开发公司推荐:四类机构对比测评

一句话答案:先按需求复杂度定档,再比价选型——标准需求选SaaS,灵活定制选本地团队,大型项目选总包,行业专属选垂直厂商。 这篇文章写给正在厦门找小程序开发团队、但被报价和机构类型绕晕的人。 看完你能分清四类机构…

作者头像 李华
网站建设 2026/10/2 2:33:12

C++ 第 14 课:引用

上一课标准答案:最后输出:5因为 change(x) 只是把 x 的值复制给参数 a,函数里改的是副本。依次输出:13 3原因是函数内部:number 10;把副本 number 从 3 改成 13,所以先输出 13。但外面的:x没有…

作者头像 李华