简介:一份面向OCR与文档自动化学习者的企业公章端到端识别系统完整实现,基于TrOCR预训练模型微调,涵盖圆形印章检测、印文文字识别与结果核验,并支持ONNX轻量级部署,适合个人课题、毕业设计或企业级印章识别预研。系统利用Transformer架构在大规模数据上的预训练优势,针对圆形印章进行专项微调,可自动判断文档中印章是否存在,并精确读取印文内容,有效减少人工核验差错。资源共28个文件,压缩包约615KB,核心为Python源码,包含模型训练、评估、推理与ONNX转换等脚本,以及图像增强、数据集构建、字典生成等辅助工具,另附说明文档、示例图像与备份文件,工程结构清晰,便于快速上手。目前已有79人学习,资源来自网络分享,仅供学习交流使用。配套说明文档中详细描述了两万枚真实印章样本的构建思路,覆盖不同样式、字体与背景环境,可帮助理解数据组织与模型泛化能力,并完整复现从微调训练到部署的流程,对文档自动化与电子签章核验场景有直接参考价值。
1. 一个红章毁掉通用OCR:TrOCR预训练模型微调要解决什么
被TrOCR这类预训练模型微调出来的识别器,和通用OCR最大的区别是它见过印章底纹,也见过弧形排版。一张合同扫描件里最难处理的往往不是正文,而是右下角那个红章:圆形边框、沿弧线排布的汉字、深浅不一的印泥,中间还压着五角星。通用OCR把整页图送进去,正文印刷体基本全对,章上的文字却可能全军覆没。这个标题指向的路线是:先用圆形印章检测把章从整页里抠出来,再用TrOCR预训练模型微调出一个专门读章的端到端识别器。所谓端到端,不是没有检测环节,而是检测之后不再走切字、单字识别、规则拼接的老流水线,直接由序列生成模型输出文本。这套方案适合做合同自动归档、票据验真、企业内部用印审计的团队,也适合想从通用OCR迁移到特定文档场景的算法工程师。
2. 为什么是TrOCR:预训练序列生成比逐字对齐更抗印章干扰
一枚公章上的文字通常不超过三十个,但干扰比一页印刷品复杂得多:弧线排版、印泥深浅、笔画断连、和背景文字重叠。选错识别模型,后面调多久都补不回来。这一章先把TrOCR的原理讲透,再说明它在这个场景里真正的边界。
2.1 从ViT编码到自回归解码:TrOCR把OCR变成了看图写话
TrOCR是微软开源的一个OCR模型族,结构上是ViT图像编码器加文本Transformer解码器。输入一张图,ViT先把图像切成16x16的patch序列做编码,解码器再像写一句话那样逐token生成识别文本。预训练分两个阶段:先在大规模印刷体和手写体上做训练,再在具体风格数据上继续训练。对印章场景来说,最有价值的是它的自回归解码:生成“有”字的时候,模型已经看过前面那个“公”字,所以“有限公司”这种固定搭配会被语言先验按住,不会因为“限”字笔画缺了一截就输出“公可”。
这一点是TrOCR和传统CRNN+CTC路线的本质差别。CRNN把图像压成时序特征,CTC做帧对齐,每个字符的识别基本只看局部感受野;遇到笔画缺损或印章底纹干扰,它只能硬着头皮猜,纠正形近字这种能力需要外部词典或规则去补。PP-OCR这类常用开源方案里的识别模型也是这个路数,检测做得很强,但识别部分对印章风格的缺损字符没有先验。TrOCR的生成式架构天然具备上下文修正能力,这也是为什么这个场景下预训练模型微调比从零训练更划算——预训练阶段已经灌进去大量字形到文本的对应关系,微调只需要把领域差异拉近。
还有一点常被忽略:TrOCR的预训练权重里已经包含了中文印刷体和手写体的混合知识。微调时即使只有几百张真实印章图,模型也能在这个基础上继续学,而不是从头开始理解什么是汉字。这点对项目排期很重要,因为企业公章数据很难大批量搞到,能支撑全参数微调的量级也就是几百到几千张。
2.2 前置检测器不是多余:端到端指的是识别链路端到端
需要先澄清一个概念:TrOCR本身不做定位,它的输入是一张图,输出是一段文本。直接把整张A4合同丢给它,模型会去读它看到的所有文字,公章只是其中一个区域,识别结果会被正文淹没。所以标题里的端到端识别系统,工程实现上仍然是一个检测加识别的两级链路:检测模型先找印章,识别模型再读文字。它和传统流水线的区别在后半段——不再把印章文字切成单字、做单字分类、再用规则拼回句子,而是检测框把章整体裁出来,TrOCR一次性输出完整文本。
检测环节的选型也有讲究。常见做法是YOLO系检测器或分割模型。我选YOLO有两个原因:一是印章是强边缘、纯色圆形目标,普通矩形检测框已经够用;二是YOLO生态成熟,预训练权重直接拉官方模型就能起步,不用自己从头训。分割模型能输出更精确的掩膜,方便后续做极坐标展开,但标注成本高,而且公章边缘被印泥晕开后,掩膜反而容易把文字一起抹掉。检测框出来之后,按长边扩展成正方形再裁剪,这一步不能省,因为公章带旋转,外接矩形不一定是正方形,直接resize会把圆压成椭圆,TrOCR读起来就失真了。
这里需要把预期管理好:检测模型负责召回,识别模型负责精度。如果检测框把章切歪了,TrOCR再强也救不回来;如果检测框带进来的背景太多,TrOCR会去读背景里的印刷体。所以后面第四章会专门讲裁剪和预处理,检测和识别两端的参数要一起调,不能各调各的。
3. 构建公章微调数据集:合成印章脚本与标注口径
公章数据是这整个项目里最卡脖子的部分。企业印章扫描件属于内部资料,能拿到的数量有限,标注更是要人眼去读弧线文字。所以在微调之前,先把合成数据管线搭起来——这也是预训练模型微调和从零训练在数据需求上的最大不同:预训练模型只需要一小部分领域数据就能迁移,但这一小部分数据必须覆盖真实场景里的干扰模式。合成数据负责覆盖数量,真实数据负责覆盖质感,两者按比例混用。
3.1 用PIL合成圆形公章:弧线排版与干扰叠加
企业公章的结构很固定:外圆、内圆、中央五角星、沿弧线排布的公司全称。用PIL就能画出一批形状正确的样本,关键是把弧线文字和盖印干扰做出来。
from PIL import Image, ImageDraw, ImageFont, ImageFilter import math, random def draw_star(draw, center, radius, fill=(200, 30, 30, 255)): cx, cy = center pts = [] for i in range(10): r = radius if i % 2 == 0 else radius * 0.45 a = math.radians(-90 + i * 36) pts.append((cx + r * math.cos(a), cy + r * math.sin(a))) draw.polygon(pts, fill=fill) def text_on_arc(draw, text, center, radius, font, fill=(200, 30, 30, 255), start_deg=-90, span=180): # 每个字符单独渲染后旋转到所在角度,模拟弧线排版 step = span / max(len(text) - 1, 1) for i, ch in enumerate(text): deg = start_deg + i * step rad = math.radians(deg) x = center[0] + radius * math.cos(rad) y = center[1] + radius * math.sin(rad) ch_img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) ImageDraw.Draw(ch_img).text((32, 32), ch, font=font, fill=fill) ch_img = ch_img.rotate(-deg, resample=Image.BICUBIC) draw.paste(ch_img, (int(x - 64), int(y - 64)), ch_img) def make_seal(text="XX建设集团有限公司", size=512): img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) cx = cy = size // 2 R = int(size * 0.39) draw.ellipse((cx - R, cy - R, cx + R, cy + R), outline=(200, 30, 30, 255), width=8) r2 = int(R * 0.68) draw.ellipse((cx - r2, cy - r2, cx + r2, cy + r2), outline=(200, 30, 30, 255), width=4) draw_star(draw, (cx, cy), R * 0.45) # 换成你机器上实际存在的中文字体路径 font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 56) text_on_arc(draw, text, (cx, cy), R * 0.82, font) return img def augment_seal(img, bg_img): # 旋转模拟盖章角度,叠加真实票据背景,加模糊模拟印泥晕开 img = img.rotate(random.uniform(-10, 10), resample=Image.BICUBIC) bg = bg_img.convert("RGBA").resize(img.size) img = Image.alpha_composite(bg, img) img = img.filter(ImageFilter.GaussianBlur(random.uniform(0, 1.5))) return img.convert("RGB")代码逻辑说明:draw_star用十个顶点交叉生成正五角星,这是公章的固定元素之一,涂实后能帮TrOCR建立“这是章不是普通圆形”的视觉锚点。text_on_arc的核心是每个字符单独渲染到128x128的画布上,再旋转到对应角度后paste回原图,PIL本身没有把文本排成弧线的API,这种逐字渲染是合成印章的通用做法。make_seal里R取size的0.39,文字半径为0.82R,内圆为0.68R,这是真实公章的常见比例;如果你手上的章样是“财务专用章”这类横排字,需要另外写一个横向摆放的函数,不要硬套弧线模板。
参数说明:size=512意味着印章直径约400像素,这个尺寸保证后续被TrOCR的processor缩放到384x384时,文字笔画不至于糊掉。start_deg=-90表示从正上方开始排字,span=180表示文字覆盖上半圆的半圈,这是企业全称章最常见的布局。augment_seal里高斯模糊的参数0到1.5是经验值,超过1.5文字边缘就化开了,TrOCR反而学不到清晰笔画。
3.2 标注口径:检测框、文本顺序、正负样本的决定
检测标注用YOLO矩形框,类别就一个seal。矩形框不要贴章边缘,留一点外边距,让识别器能看到章的边框纹理,这对学会读章反而有帮助。文本标注按视觉顺序写——弧线文字从左上开始顺时针或逆时针都行,关键是全数据集方向一致。这里有个坑:合成脚本从左到右排字,人工标注却是按阅读顺序从右往左写,两种顺序混在一起,TrOCR生成结果的顺序就是乱的。标注规范里必须写死一句话:所有印章文本按实际阅读顺序记录,方向以合成脚本生成时的字符顺序为准。
文本里标点、括号、数字都不能清洗。公章上常见“(1)”“〔2024〕”这类符号,如果清洗掉,模型在实际数据里碰到括号就会输出乱码。还有一个细节:标签里的空格要去掉。TrOCR预训练词表里带空格token,公章的文本没有空格,训练标签若出现空格,模型会学着把空格也生成出来,下游比对时又要多做一次清洗。标签长度控制在64字符以内,公章文本一般不会超过这个数,但preprocess里要做截断,防止公司全称特别长时把decoder的序列拉爆。
正负样本的问题要分开看。识别器不需要负样本,它只接收检测框裁剪后的区域;但检测器需要没有章的票据图作为负样本,否则模型会把票据上的圆形logo、圆形水印都当成章。负样本比例不用高,五分之一即可,太多了会让检测器倾向漏检。
3.3 微调集配比与增广:合成数据打底,真实数据校准
| 数据块 | 数量 | 作用 |
|---|---|---|
| 合成章(叠加票据背景) | 6000 | 让模型学会弧线文字和印章比例 |
| 合成章(纯色背景) | 2000 | 保证模型不依赖背景纹理 |
| 真实扫描/拍摄章 | 200~500 | 校正油墨质感、扫描噪声 |
| 无章票据负样本 | 500 | 给检测器做负样本 |
真实数据可以很少,但必须覆盖不同设备:扫描仪、高拍仪、手机拍摄。合成数据负责教会模型章的形态,真实数据负责把模型从合成感拉回地面。比例在8:1到10:1之间是常见做法,真实数据低于200张时,模型容易在合成数据上过拟合,现场遇到油墨偏浅的章就翻车。
注意:增广手段里最有效的是随机遮挡,用黑色粗线条模拟盖章时压到了表格线或正文文字,TrOCR必须学会从被遮盖的笔画中猜字——这和它预训练时的语言先验正好互补。纯旋转和缩放对TrOCR提升有限,因为Transformer对旋转的建模能力不如CNN直觉上那么强,加多了反而让解码器困惑。
4. 检测与识别微调落地:YOLOv8预训练权重和TrOCR训练参数
数据准备好了,这一章把两条模型链路串起来。检测用YOLOv8,识别用TrOCR,命令和参数按可复现的标准给。GPU资源有限的话,这一章的顺序很重要:先训检测器,因为检测结果直接决定识别器能看到什么。
4.1 检测器微调:YOLOv8从预训练权重起步的完整命令
先建数据集配置文件seal.yaml:
path: ./seal_data train: images/train val: images/val nc: 1 names: ['seal']训练命令:
yolo detect train \ model=yolov8s.pt \ data=seal.yaml \ imgsz=960 \ epochs=120 \ batch=8 \ lr0=0.005 \ device=0参数说明:model=yolov8s.pt直接加载官方预训练权重,这和“目标检测模型微调崩了”的常见原因正好相对——用预训练权重起步,比从头随机初始化稳得多。imgsz=960是因为公章在A4扫描图里通常只占1/10到1/5面积,分辨率太低小章会被当成噪声忽略;如果显卡显存只有8G,降到640、batch降到4,但漏检率会明显上升,尤其是手机拍摄的倾斜章样。epochs=120看着多,印章数据量小,模型收敛快,一般60轮以后mAP就不再涨。lr0=0.005比默认的0.01略低,防止第一轮就被大步长把预训练特征冲坏。
检测完裁剪这一步,我直接给出一个脚本片段:
import cv2 def crop_square(img, box, expand=0.15, target=384): # box: [x1, y1, x2, y2],来自YOLO输出 w = box[2] - box[0] h = box[3] - box[1] s = int(max(w, h) * (1 + expand)) cx = (box[0] + box[2]) / 2 cy = (box[1] + box[3]) / 2 x1 = max(0, int(cx - s / 2)) y1 = max(0, int(cy - s / 2)) crop = img[y1:y1 + s, x1:x1 + s] return cv2.resize(crop, (target, target), interpolation=cv2.INTER_CUBIC)这段按长边扩展15%再裁剪,扩展量是经验值:太少会切掉外圆边框,太多会把旁边表格线带进来。判据很简单——把裁剪结果画出来看,印章外圆与图像边缘之间应该有一圈明显留白,而不是顶着边。
4.2 TrOCR微调:加载trocr-base-printed并用Trainer训练
识别部分直接用HuggingFace的transformers,加载预训练模型做全参数微调:
from transformers import TrOCRProcessor, VisionEncoderDecoderModel, \ Seq2SeqTrainer, Seq2SeqTrainingArguments processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") model = VisionEncoderDecoderModel.from_pretrained("microsoft/trocr-base-printed") model.config.decoder_start_token_id = processor.tokenizer.cls_token_id model.config.pad_token_id = processor.tokenizer.pad_token_id model.config.eos_token_id = processor.tokenizer.sep_token_id model.config.vocab_size = model.config.decoder.vocab_size def preprocess(batch): images = [x.convert("RGB") for x in batch["image"]] enc = processor(images=images, text=list(batch["text"]), padding="max_length", max_length=64, truncation=True) labels = enc["labels"].clone() labels[labels == processor.tokenizer.pad_token_id] = -100 return {"pixel_values": enc["pixel_values"], "labels": labels}代码说明:TrOCRProcessor同时处理图像和文本,传入text时会返回labels,这样pixel_values和labels的batch维度天然对齐。把pad_token_id换成-100是损失计算的关键——-100位置的token不参与loss,模型不用费劲去学习输出填充符。model.config里那四个token_id设置是TrOCR微调的标准动作,不设置的话T5风格的decoder可能报错或生成异常。
训练参数:
training_args = Seq2SeqTrainingArguments( output_dir="./trocr-seal", learning_rate=5e-5, per_device_train_batch_size=16, per_device_eval_batch_size=16, gradient_accumulation_steps=2, num_train_epochs=10, fp16=True, predict_with_generate=True, generation_max_length=64, save_strategy="epoch", logging_steps=50, eval_strategy="epoch", # 旧版transformers写evaluation_strategy ) trainer = Seq2SeqTrainer( model=model, args=training_args, train_dataset=train_ds, eval_dataset=val_ds, tokenizer=processor, ) trainer.train()参数说明:learning_rate=5e-5是TrOCR微调比较稳的起点,比这个大一倍模型就容易在小数据集上过拟合,表现为训练loss降到0.1以下但验证集不涨。per_device_train_batch_size=16在16GB显存下勉强跑得动,显存不够就把batch降到8、gradient_accumulation_steps加到4,效果基本等价。fp16能省一半显存,但要注意如果loss出现NaN,先关掉fp16试试,很多时候是数据里混入了全角特殊字符导致数值不稳定。num_train_epochs=10对印章场景够了,真实章数据少,训练轮次再多就开始背训练集。
这里可以提一句:大模型微调里流行的LoRA在这个场景不是最优选择,TrOCR的参数量远小于LLM,全参数微调在几百张图上就能收敛,显存也扛得住。LoRA省下的显存在这个规模意义不大,反而多一个超参数要调。
4.3 识别前的极坐标展开:让环形文字先变直
公司全称沿圆周排布,TrOCR的预训练数据里基本都是水平文本,直接读弧线文字会让解码器困惑。一个常用的预处理是把环形区域极坐标展开:
def unwrap_seal(img, center, R): # 把以center为圆心、半径R的圆形区域展开成线性图像 h, w = img.shape[:2] return cv2.warpPolar( src=img, dsize=(h, w), center=center, maxRadius=R, flags=cv2.WARP_POLAR_LINEAR + cv2.INTER_LINEAR )cv2.warpPolar把圆环按角度展开成水平条带,弧线文字就变成从左到右的直线文字。参数center和maxRadius要准:center用检测框的中心近似即可,maxRadius取印章外圆半径。展开结果里文字方向可能是反的,验证集上跑一次就知道,不对就加一行np.fliplr。展开不是银弹,如果印章里是横排文字(比如典型的下方横排专用章),展开反而把版面拆坏;建议在训练时对合成数据做同样的展开,保持训练和推理分布一致。用不用展开,用一个几十张的小验证集跑一下对比就能定。
5. 印章识别微调避坑:5个让模型在现场翻车的细节
微调踩过的坑比模型结构本身更能决定项目成败。这一章写五个真实项目里高频出现的翻车点,每条按现象、原因、解决来拆。
5.1 背景文字渗入:合成章训练后把票据正文也读出来了
现象:模型在合成数据验证集上识别率超过95%,一到真实票据上就开始输出“合同编号”“甲方”“乙方”这些背景文字,公章内容反而被丢掉。
原因:合成章叠加背景时,背景上的印刷体文字没有被当作干扰处理。TrOCR是自回归生成,它分不清哪些文字属于章、哪些属于背景,只要检测框里出现了高对比度文字,解码器就有概率去读。
解决:合成时保证60%以上样本叠加带文字的票据背景,并且推理端把裁剪框的expand参数从0.15降到0.08,减少背景文字进入识别器的机会。另外可以在标注时把背景文字较多的裁剪图直接丢掉,不参与训练。
5.2 笔画断连误读:“股份”被读成“份”
现象:真章上“股份”两个字因为印泥不均匀,左边笔画浅到几乎看不见,模型输出“份”;合成章上同样的字却识别正常。
原因:TrOCR学到的字形是完整笔画,训练数据里没见过笔画大面积缺失的情况。公章盖章时受力不均,局部油墨缺失是常态,不是偶发。
解决:训练时对合成章做形态学腐蚀,用cv2.erode把笔画随机变细,再叠加随机擦除模拟油墨飞白。另外可以在增广里加一条:把图像局部区域的饱和度降为零,让红色章在那一块变成浅灰,模拟印泥不足的效果。
5.3 验证集95%、线上80%:分布不一致是最大隐患
现象:微调时验证集准确率很高,部署后发现大量误识别,尤其是手机拍摄的章,识别率比扫描图低一截。
原因:验证集是从同一批合成数据里随机split出来的,和训练集共享同一种背景模板、同一种字体渲染方式。真实场景的扫描噪声、透视畸变、拍摄角度,验证集里一样都没有。
解决:从项目一开始就留一个终测文件夹,只放真实设备采集的章图,微调全程不碰。每次训练完跑一次终测,以这个数字作为发版依据。对TrOCR这类生成模型,合成数据上的指标只能用来判断训练是否收敛,不能用来评估上线效果。
5.4 置信度阈值拍脑袋:识别置信度和检测置信度混为一谈
现象:把TrOCR的beam search输出概率当作置信度,发现印章文字短、概率普遍较低,阈值设低了误识别一堆,设高了又拒识过多。
原因:TrOCR自回归解码的概率是逐token累乘的,文本越长概率天然越小,直接拿概率绝对值做阈值没有意义。检测置信度是分类概率,识别置信度是序列概率,两者量纲完全不一样。
解决:用序列概率对长度做归一化,算每个token的平均对数概率,再设阈值。检测阈值一般设0.5左右,识别阈值通过终测集的错误接受率来定。宁可漏识,不可错识——审计场景里一个错字比一次拒识严重得多。
5.5 显存不足和训练崩溃:TrOCR比想象中吃显存
现象:per_device_train_batch_size设成16,还没跑几步直接OOM;开了fp16之后loss变成NaN,训练中断。
原因:TrOCR是ViT加Transformer decoder的结构,中间激活值占显存比同参数量CNN高不少。fp16的NaN多半是学习率偏大或数据里有异常像素值,不是fp16本身的锅。
解决:显存不够先开model.gradient_checkpointing_enable(),能省约三成显存,代价是训练速度慢20%左右。NaN先关fp16确认是不是数值问题,再把learning_rate降到3e-5。检查数据里有没有纯白或纯黑的图像,这类极端输入在fp16下容易梯度爆炸。
6. 验证口径与拒识兜底:让公章识别真正敢上线
6.1 用编辑距离和整串匹配给模型打分
公章文本短,评估指标不能只用字符错误率(CER)。一张章上十个字,错一个字CER是10%,看起来不高,但在“公司全称完全一致才算对”的业务规则里就是错误。建议同时统计三个数字:CER、整串精确匹配率、以及“按字符集合去重后匹配率”。最后这个指标用来识别一种特殊情况——文字顺序错了但字全对,这在弧线印章里很常见,业务上也不该判对,但能帮你快速定位是解码顺序问题还是字符识别问题。
我现在的习惯是每轮训练完跑四个维度:合成验证集、真实终测集、按设备分组的终测集、按章型分组的终测集。设备分组能看出手机拍摄和扫描仪的差距,章型分组能看出横排章和弧线章的差距。某一组掉了,优先检查数据,而不是调模型结构。
6.2 拒识比强行识别更值钱
落地上线时,我强烈建议加一个双阈值拒识:检测置信度低于0.5直接丢框,识别平均对数概率低于阈值直接输出空串。对不同业务场景设不同拒识阈值——用印审计场景要求零差错,阈值调高,多拒识没关系;合同归档场景希望多识出来,阈值调低,允许少量错误后续人工校验。这个阈值不要拍脑袋定,在终测集上画出“正确接受率-错误接受率”曲线,挑业务能接受的平衡点。
提示:盖章压线、背景文字重叠这类极端样本,模型硬读出来的结果往往错得离谱。拒识机制就是给系统留后悔药,把低质量的预测挡在上游,比让下游流程去清洗错误文本便宜得多。
一次真实项目里,某张章的“公司”两个字被背景表格线完全压住,模型硬读成了“公刮”,恰好检测置信度低被拒识机制拦下,审计那边才没有拿着错结果去对账。从那以后我所有OCR项目都默认带拒识,哪怕只是简单阈值。TrOCR模型本身很强,但强模型更需要一套防呆的验证和拒识逻辑配合,才敢在业务线上长期跑。希望帮到你。
本文还有配套的精品资源,点击获取