资讯详情

资讯详情

EGEUNet印章分割实战:构建高质量数据集全指南

做文档图像处理的朋友应该都有同感印章提取这件事看着不起眼真做起来破事一堆。合同扫描件上章子盖得歪歪扭扭发票上的红章又总跟背景叠在一起人工抠章一小时只能弄几张。我这次想把这件事彻底自动化选了一套自己打磨过的组合——EGEUNet轻量分割网络加上自整理的印章分割数据集打算从数据准备、模型训练、效果评估到最终部署完整地写一个系列。这篇是系列的第一篇先把数据集这部分聊透。老规矩先说结论这一篇解决的是“用什么数据、怎么标数据、怎么把标注变成模型能吃的格式、以及数据准备好了之后怎么传给EGEUNet”这几个问题。适合正在做OCR全流程、票据信息抽取、合同比对系统的同学也适合刚入门语义分割、想找一个完整项目来练手的人。1. 项目整体思路与数据准备规划1.1 印章分割在文档处理场景中的位置印章分割就是把一张文档图像里的印章区域逐像素地分离出来。通常输入是一张合同扫描件或发票照片输出是一张和输入大小一致的单通道掩码图mask印章所在位置是白色其余位置是黑色。为什么要单独做这个任务因为后续的处理链条非常依赖这一步。比如印章文字识别如果先把红色印章从复杂背景中剥离再去跑OCR识别准确率会明显提升又比如合同真伪比对需要把两处印章的轮廓、文字位置、阴阳文细节做像素级对齐没有干净的分割结果根本无从谈起。在这个系列里我把整套系统拆成了四步数据集构建、EGEUNet训练、模型评估、推理部署。现在写的“系统1”就是这个项目的基石。后面所有模型的性能上限都在这份数据里决定。1.2 为什么选择EGEUNet作为分割模型EGEUNet是近两年在轻量分割领域比较常用的一个U型网络变体。它没有走“堆参数量换精度”的老路而是在UNet的编解码骨架上做了两个关键改动。第一卷积模块替换成了Ghost模块。普通卷积直接生成全部特征图Ghost模块只生成一小部分真实特征图其余特征图靠线性变换低成本补全。这样做的直观效果是同等通道数下参数量和计算量大幅下降推理一张512x512的扫描图在普通显卡上能做到毫秒级。第二模型加入了边缘引导分支。印章分割对边缘精度要求很高尤其红色圆章边缘有磨损、有油墨晕染普通分割网络很容易把边缘预测糊成一片。EGEUNet把边缘信息单独提炼出来与主分割分支融合正好对圆形、椭圆形的印章边界非常友好。我实际用下来EGEUNet在印章这种“前景目标小、边缘强、背景杂乱”的任务上收敛速度比同规模的其他轻量分割模型要快。1.3 数据准备为什么是“系统1”最重要的一步很多初学语义分割的同学会犯一个错拿到模型先跑通用公开数据集随便训一下觉得很顺利等到自己业务场景一上手就翻车。原因几乎都出在数据上。印章分割尤其吃数据。合同扫描件的复杂程度远超想象背景有表格线、水印、手写批注、页码、装订孔印章可能盖在文字上也可能盖在照片上印泥颜色有深红、浅红、紫红还有蓝色章和黑色章。如果数据里只放了十几张“白纸上一枚鲜红圆章”的干净样本训练出来的模型拿到真实场景基本是废的。所以我在这套数据集上花了大量精力做多样性和负样本控制。只有把数据这一关守住后面EGEUNet的训练才谈得上效果。2. 印章分割数据集详解结构与标注格式2.1 数据集的目录组织方式本系列使用的数据集V1整理后的目录结构如下stamp_seg_dataset_v1/ ├── images/ │ ├── train/ │ │ ├── 00001.jpg │ │ ├── 00002.jpg │ │ └── ... │ ├── val/ │ │ ├── 00001.jpg │ │ └── ... │ └── test/ │ ├── 00001.jpg │ └── ... ├── masks/ │ ├── train/ │ │ ├── 00001.png │ │ ├── 00002.png │ │ └── ... │ ├── val/ │ │ ├── 00001.png │ │ └── ... │ └── test/ │ ├── 00001.png │ └── ... ├── labels.json ├── README.txt └── dataset_stats.txt原图统一使用JPG格式质量参数设为95单张大小控制在500KB以内掩码统一使用PNG格式单通道8bit深度。原图和掩码的文件名一一对应没有任何扩展名差异造成的匹配问题。这里有个细节必须强调mask必须是单通道PNG而不是三通道彩色PNG。很多标注工具默认导出的是三通道RGB图如果直接拿来训练在Torch的CrossEntropyLoss里会报通道数不匹配在Dice Loss里也会把通道当batch算最后指标全是乱的。2.2 标注格式与标签语义语义分割的标签不是分类标签而是像素级标签。在这份数据集里标签只有两类印章区域为前景像素值255其他所有区域为背景像素值0。为什么用0和255而不用0和1主要是为了可视化方便。直接用图片查看器打开mask白色区域一眼就能看出标注效果。训练时只要在Dataset里把mask像素值除以255就变成了0/1的二值标签不影响任何训练逻辑。除了原始图像和掩码我还额外保存了一份labels.json里面记录了每张图的标注信息。字段设计如下{ 00001.jpg: { stamp_count: 1, stamp_type: red_circle, stamp_bbox: [120, 340, 460, 680], annotator: A同学, status: verified } }stamp_count记录印章数量stamp_type记录印章类型red_circle、red_ellipse、blue_rectangle、black_round等stamp_bbox是整个图像中所有印章的最小外接框status分为pending和verified两种。保存这份元数据有两个好处一是后续做错误分析时可以按印章类型拆分指标看看EGEUNet到底在哪类印章上表现差二是方便多人协作标注时追踪进度和标注质量。2.3 数据集统计与划分策略V1数据集的整体规模是1768张图像具体构成如下表所示类别原图数掩码数说明单印章场景12541254一张图像中仅出现一枚印章多印章场景348348同一图像中出现2枚及以上印章无印章负样本166166纯合同、票据、表格背景无任何印章无印章负样本是很多自建数据集容易漏掉的部分。模型如果只见过“有印章”的图像推理时遇到没有印章的页面可能也会在某个位置幻觉出一块印章区域。加入这些负样本之后模型学会了输出一张全黑的mask这个能力在真实业务里非常有用。数据划分上我按8:1:1随机分成训练集、验证集、测试集每类图像都按相同比例抽取避免出现验证集里全是单印章、测试集里全是多印章这种尴尬情况。同时我按图像来源做了去重校验确保同一张合同的不同扫描版本不会同时出现在训练集和验证集中防止数据泄漏造成的虚高指标。3. 从原始图像到掩码数据集构建全流程实操3.1 原始材料收集与预处理收集原始材料是数据构建的第一关。我这套数据集的图像来源比较杂一部分是公开渠道下载的脱敏合同样张一部分是自己用红色、蓝色印泥在白纸、牛皮纸、带表格的打印纸上盖章后扫描的样本还有一部分是模拟业务环境拍摄的手机照片。材料收集阶段要注意版权和隐私问题合同样张里的公司信息、身份证号码、手机号等敏感字段我在整理时已经统一做了马赛克处理。这一步不能省否则后续数据就算训练效果再好也不敢公开发布。原始图像的分辨率差异很大扫描件一般在300dpi单边长度2000到4000像素手机照片则可能只有1200万像素但视角有倾斜。我做了两步统一预处理第一步是统一转成RGB三通道剔除灰度图和CMYK图避免训练时通道数报错第二步是等比缩放到长边不超过2048像素控制显存占用同时保留足够分辨率供后续裁切。缩放的时机要留意最好在标注之前完成因为标注的坐标是基于图像像素的先缩放后标注坐标就不会出现越界问题。3.2 用标注工具描摹印章轮廓我用的标注工具是Labelme这是一个开源的多边形标注工具。为什么不用矩形框标注工具因为印章通常带有轻微椭圆形变形、边缘磨损、印泥飞白矩形框会把大量背景包进去直接导致模型学不到精细边缘。具体标注流程分三步。第一步新建标签统一命名为stamp不要同一个数据集里出现“stamp”“seal”“红章”等多个标签名后面转mask时很容易漏。第二步沿着印章外围轮廓点多边形点对于圆形章建议每隔20到30度取一个锚点这样边缘拟合比较圆润对于圆形文字环绕的印章锚点可以稍微密一些真实边缘转折处务必加点。第三步添加完所有印章的多边形之后切换到编辑模式检查一遍重点看有没有把背景中的红色logo、红色表格线误圈进来。标注过程里最容易犯的错是“偷懒画椭圆”直接用一个近似圆形套上去。我第一批数据就用过这个办法结果模型严重依赖红色先验看到红色元素就圈反而把真正的印章漏掉了。后来全部改成逐点精确描边虽然速度慢了两三倍但训练效果完全不一样。3.3 批量生成mask的转换脚本Labelme导出的是JSON格式的多边形坐标需要转成训练用的PNG掩码。我写了一个批量转换脚本核心逻辑如下import json import numpy as np import cv2 from pathlib import Path def labelme_json_to_mask(json_path, image_path): img cv2.imread(image_path) h, w img.shape[:2] mask np.zeros((h, w), dtypenp.uint8) with open(json_path, r, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: if shape[label] ! stamp: continue points np.array(shape[points], dtypenp.float32) points points.astype(np.int32) cv2.fillPoly(mask, [points], 255) return mask json_dir Path(./labelme_json) img_dir Path(./images) mask_dir Path(./masks) mask_dir.mkdir(exist_okTrue) for json_p in json_dir.glob(*.json): img_p img_dir / (json_p.stem .jpg) mask labelme_json_to_mask(str(json_p), str(img_p)) cv2.imwrite(str(mask_dir / (json_p.stem .png)), mask)脚本本身不复杂但有三个细节值得注意。第一是fillPoly的输入坐标要用int32用float会报错第二是mask的初始值必须是0背景区域不要用其他值填充否则会在损失函数里造成额外干扰第三是转换完成后要写一个人机检查环节随机抽20%的图用红色半透明叠加到原图上肉眼看是否和印章边缘对齐。def overlay_visualize(img_path, mask_path, save_path): img cv2.imread(img_path) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) colored_mask np.zeros_like(img) colored_mask[:, :, 2] mask overlay cv2.addWeighted(img, 0.7, colored_mask, 0.3, 0) cv2.imwrite(save_path, overlay)这一步能直观发现“漏标”“错标”“多边形没闭合”等脚本检查不到的问题。3.4 数据增强策略与样本平衡数据扩充不能等训练时才临时决定而应该在数据集构建阶段就做好预案。印章分割场景里旋转增强是最重要的一类。印章不像行人检测那样有固定的上下方向合同里的章可能任意角度旋转所以训练时做随机旋转非常有必要我一般限制在30度以内角度太大会把印章形状扭曲得太不真实。另一个常用增强是光照和颜色抖动。扫描件的亮度和对比度差异很大有的偏黄有的偏灰所以亮度、对比度的随机调整能提升模型在不同扫描仪下的泛化能力。但色相抖动要小心红色印章的色相一旦大幅偏移模型可能会学到“红色偏紫的图像里没有印章”这种错误规律所以hue的偏移量不要超过0.02。关于样本平衡我额外做了两类采样调整。一类是过采样多印章样本因为大部分标注精力都在单印章上多印章样本少训练时每轮多抽几批可以防止模型对“页面里只有一个章”过于适应另一类是负样本控制的增强给无印章图像加一个随机的白噪声裁切模拟扫描时出现的条纹干扰提升模型对背景干扰的免疫力。4. 训练环境的准备与关键参数配置4.1 数据集加载器的编写要点数据准备好了接下来说EGEUNet训练要用的Dataset加载器。这里我直接给出一份在项目里实际能跑的代码框架import torch from torch.utils.data import Dataset import cv2 import os import albumentations as A from albumentations.pytorch import ToTensorV2 class StampDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.filenames [f for f in os.listdir(img_dir) if f.endswith((.jpg, .png))] self.transform transform def __len__(self): return len(self.filenames) def __getitem__(self, idx): fname self.filenames[idx] img cv2.imread(os.path.join(self.img_dir, fname)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, fname.replace(.jpg, .png)), cv2.IMREAD_GRAYSCALE) mask mask.astype(np.float32) / 255.0 if self.transform: augmented self.transform(imageimg, maskmask) img augmented[image] mask augmented[mask] return img, mask.float()这里有两个容易踩坑的细节。第一cv2.imread默认读进来的是BGR通道顺序如果不转换成RGB训练时图像会变成“蓝红互换”的奇怪颜色红色印章直接被识别成蓝色损失函数照样下降但推理效果一塌糊涂。这个问题极其隐蔽因为数值上模型似乎收敛了实际上颜色语义已经错乱。第二cv2.imread读取mask时必须加cv2.IMREAD_GRAYSCALE否则读进来一个三通道图后续除以255会得到一个三通道的浮点标签损失函数计算时直接报错。4.2 增强管道与训练的衔接用albumentations做大统一增强管道是当前比较省心的做法。它能把图像增强和mask变换封装在一起保证几何变换同步。我的增强管道配置如下train_transform A.Compose([ A.RandomResizedCrop(size(512, 512), scale(0.7, 1.0), ratio(0.9, 1.1)), A.Rotate(limit30, border_modecv2.BORDER_CONSTANT, value0, mask_value0), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.1), A.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.02, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])几个参数说下我的选择逻辑。输入尺寸设为512x512是因为印章区域在原图上占比不大但也不至于小到看不见512分辨率能兼顾训练速度和边缘细节如果印章特别小可以改成768甚至1024代价是显存和训练时间成倍上涨。Rotate的border_mode和mask_value一定要配套设置。旋转时图像边缘会产生空白区域border_mode用BORDER_CONSTANT会让空白区变成固定值0也就是黑色避免旋转产生仿射纹理干扰mask_value也必须设成0如果默认是255旋转后的mask边缘会凭空多出一圈白色背景噪声训练时模型会学着把这些白色当成印章区域。4.3 训练超参数和评估指标的选择训练超参数我列一个可复用的基线配置模型输入尺寸: 512 x 512 Batch Size: 16 优化器: AdamW 初始学习率: 1e-4 学习率策略: Cosine Annealing最小学习率 1e-6 训练轮数: 60 epochs含10轮warmup 损失函数: 0.5 * BCE 0.5 * Dice Loss 评估指标: IoU、Dice、PAPixel Accuracy为什么主要用Dice Loss而不是纯交叉熵因为印章区域在一张512x512图像里平均只占3%到5%的像素正负样本极不平衡。如果纯用CrossEntropyLoss模型会倾向于把所有像素判成背景因为即使全判背景loss也不会太高。Dice Loss直接优化分割区域的重叠程度对前景占比小的情况更友好。评估指标里IoU和Dice几乎是同一件事的两个视角。IoU是交集除以并集Dice是两倍交集除以面积和二者在数值上略有差异但趋势一致。实际操作中我会两个都看IoU更严格Dice对微小区域的波动更敏感。如果印章边缘分割不稳定Dice会出现明显抖动而IoU变化不大这时候需要回头检查边缘增强模块的权重。5. 常见问题与避坑指南5.1 标注阶段的高频错误第一个高频错误是多边形锚点过密。有人觉得锚点越多越精细结果一个圆章画了两百多个点边缘反而因为点间距太小而出现锯齿状噪声。我的经验是一个普通圆章控制在三十到五十个锚点之间转折剧烈处局部加密。第二个高频错误是不检查小印章。合同里经常有骑缝章一枚章可能只有半截露在页面边缘标注时很容易漏掉。建议在批量转换后认真看mask凡是原图边缘有红色痕迹而mask没有覆盖的都要补标。第三个高频错误是印章和背景上的红色元素混淆。发票里常有红色的“发票专用章”字样、红色水印、红色表格线标注时只选真正的印章区域不要把红色装饰圈进来。如果整张图里存在红色logo但又不是印章保留在背景里反而对模型有利能让它学会区分“红色”和“印章”。5.2 训练阶段的经典翻车案例我至少见过三次这样的翻车训练集里全是红章验证集里全是红章模型跑出0.95的IoU一部署到真实环境遇到蓝章就完全失效。原因很简单训练数据里蓝色印章样本太少EGEUNet的编码器学会了用颜色通道直接定位印章而不是学习形状和纹理特征。解决办法是在数据集中刻意加入至少15%的蓝色印章、10%的黑色印章样本并把验证集和测试集按相同比例混合。如果真实业务里蓝色印章出现频率不高可以不做平衡但至少要保证模型见过。另一个翻车点是印章半透明。有些印泥按压力度小章印在纸上能看到底下的文字如果标注时强行把所有半透明区域都算成前景模型会学到“所有带红色的像素都是印章”遇到浅色印章时误检率会很高。我处理这类样本的原则是以肉眼可辨的印章边界为准半透明边缘区域明确标入前景但底下的文字纹理不作为判断依据。5.3 数据生命周期的版本管理建议数据集这种东西最怕改来改去没有版本。我见过同事在同一个目录里反复增删图像最后训练出来的模型根本说不清用了哪批数据。我自己采取的办法很简单数据集压缩包命名为stamp_seg_dataset_v1之后每次增删样本都升版本号并且给每个版本写一份dataset_stats.txt记录图像总数、各类别数量、新增和删除的文件清单。这份统计文件长这样version: v1 total_images: 1768 train_images: 1414 val_images: 177 test_images: 177 single_stamp: 1254 multi_stamp: 348 no_stamp: 166 red_stamp: 1260 blue_stamp: 265 black_stamp: 243 created_by: internal notes: added no-stamp negatives, fixed 23 mislabeled masks训练时把版本号写进训练日志的文件名里比如egeunet_stamp_v1_b16_512这样三个月后回看实验谁用了哪份数据、跑了什么参数一目了然。6. 数据集获取方式与使用须知6.1 下载包内容与文件命名规则整理好的V1数据集压缩包命名为stamp_seg_dataset_v1.zip。解压后的根目录就是第二章展示的目录结构。这里再补充一下文件命名规则所有文件名统一为五位数字从00001.jpg开始递增原图是JPG对应mask是PNGlabels.json里的键名与文件名字完全一致不带路径。README.txt里写明了数据集的使用条款、标注规范和每个字段的含义dataset_stats.txt就是上一节显示的统计内容。压缩包整体大小约2.1GB其中原始JPG图像占约1.4GBPNG掩码占约0.7GB。建议拿到数据集后先跑一遍完整性校验核对图片总数、mask尺寸是否与原图一致、mask像素值是否只包含0和255。我写过一个几十行的小脚本做自动校验省去了大量人工核对的时间。6.2 获取方式与使用规范关于下载链接这里说明一下因为外部网盘链接放在正文里容易失效我把获取方式放在两个地方。一是在某开源社区数据集板块检索关键词stamp-seg-dataset-v1可以找到发布页二是直接私信本账号发送“印章数据集”四个字会自动回复当前有效的下载地址。压缩包较大建议使用支持断点续传的下载工具。使用规范就三条原则。第一数据集仅限学习研究和内部测试使用不用于任何形式的商业产品交付第二原始图像中的敏感信息已经脱敏处理使用方不得尝试还原或二次传播第三如果在论文、博客或内部报告中引用这份数据请注明来源和版本号。6.3 下一步EGEUNet模型训练与验证写到这里数据集这条线的主要工作就收尾了。我个人在实际项目里体会最深的一点是数据集的构建耗时通常会占整个项目的六成以上但这件事前期做扎实了后面训练阶段能省下大量返工时间。下一篇我会直接进入EGEUNet的网络结构解析和训练代码实现包括Ghost模块、边缘引导分支的具体写法以及如何在自定义数据上跑出一套可复用的训练脚本。到时候我会把训练日志、IoU曲线和不同场景下的分割效果图都放出来做一个直观的对照分析。如果你自己也在整理印章相关数据建议先把“无印章负样本”和“蓝色印章样本”这两类补齐这是我在多轮训练里验证过的、对最终效果影响最大的两个变量。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →