简介面向企业文档自动化与电子签章核验场景这份基于预训练模型微调的端到端公章识别系统资料适合有一定深度学习基础、希望实现圆形印章检测与印文识别的开发者或学习者。资源共二十八个文件以Python脚本为核心覆盖模型训练、评估、开放神经网络交换格式部署、数据增强等环节并配有示例图片、说明文档及备份文件压缩包整体仅六百一十五千字节体积小巧便于快速下载与本地实践。目前已有七十九人学习浏览。压缩包内附带约两万枚真实印章样本的数据集说明、词表文件、初始化模型及训练代码还提供基于开放神经网络交换格式的轻量级部署方案与配套操作指南可帮助理解圆形印章检测、文字提取任务的完整落地流程。对于正在开展光学字符识别相关课题研究或企业印章自动核验功能开发的技术人员这是一份可直接参考的实战资料。1. 一枚歪斜的公章图为什么TrOCR预训练模型微调成了唯一能打通的路扫描件里一张企业公章圆形、红色、字绕圆心排成闭环有些图偏转五度、有些盖在文字上。拿通用OCR直接跑出来的是一串乱码甚至把“有限责任公司”认成“有限贡任公司”。这不是OCR引擎不行而是排版先决条件没满足——印章文字沿圆周分布通用检测框只认横向文本。后来我用TrOCR预训练模型微调搭了端到端链路前置一个圆形印章检测头再跑文字识别才把一批歪斜公章图稳定变成结构化文本。下面这套链路就是这次实践的完整拆解从检测、微调到部署、避坑都过一遍。适合做档案数字化、票据核验、企业证件识别的工程同学纯算法新手也能照步骤复现。核心就一句话印章识别不是OCR参数题是前置几何处理加检测头选型的组合题。2. 圆形印章为什么不能直接套通用OCR检测与识别的分工边界先给结论把一张公章图丢进通用文字识别接口十个里有八个会翻车而且翻在检测环节而不是识别环节。通用OCR的长处是横向、排版规整的印刷体默认文本框是水平矩形。公章是环形文字加五角星加防伪底纹这个先验直接被击穿。所以把它拆成“检测—摆正—识别”三段每一段用各自擅长的模型才谈得上可用。2.1 通用OCR在圆形排版上的三个失效点第一文本框假设失效。通用检测器输出的是轴对齐矩形框印章文字沿圆环排列水平框切出来的区域里混着底纹、相邻字符和圆心空白。识别头拿到的输入本身就是劣质的后面再强也没用。第二旋转方向问题。同一批扫描件里公章可能旋转了 15 度、170 度甚至 350 度。通用OCR 模型没有见过大量大角度旋转的文本遇到倒置文字时会把“验收专用章”读成“章用专收验”而且这个错误非常稳定不是偶发。第三背景高频干扰。五角星、边框弧线、防伪编码这类纹理和文字挤在同一环形区域检测器容易把弧线当成文字候选或者把文字区域和五角星区域合并成一个框。合并框进入识别模型后字符之间还夹杂着图形笔画识别结果自然不可信。2.2 方案选型YOLO预训练模型负责检测TrOCR预训练模型负责识别我把几个候选方案放在一张对比表里过了一遍这里直接给出筛选结果。方案检测方式识别方式对这个项目的判断YOLO预训练模型 微调TrOCRYOLO检测框Vision-Encoder-Decoder生成文本检测成熟、识别可控推荐PaddleOCR 全套 PP-OCRv4DB文本检测CRNN/注意力识别横向文本强环形排版需额外改检测头不推荐DBNet 检测 CRNN 识别任意方向文本检测序列识别检测能扛但CRNN对长文本印章表现一般数据需求量大单模型端到端直接预测框文字模型隐式完成生成式显存和调参成本高检测框质量难约束选型时我比较看重两个点。一是检测头不要重新发明轮子YOLO预训练模型下载下来就能跑印章候选框这种小目标检测任务直接用它的 backbone 特征就够了二是识别头必须是生成式的因为印章上同一家公司的名称、统一社会信用代码、专用章字样重复率极高TrOCR 这类 Transformer 生成模型对上下文的利用比 CRNN 好微调后泛化更稳。2.3 圆形印章的几何预处理椭圆拟合与角度摆正很多教程把这一步跳过了直接拿检测框里的原图喂给识别模型。实际效果是识别模型在训练时看到的是摆正后的环形文字推理时却接收到歪斜图精度立刻崩掉。我一般会在检测和识别之间加一个几何摆正步骤代码不复杂。import cv2 import numpy as np def align_stamp_ellipse(img_bgr): # 红色印泥在HSV空间里有明显的双段分布分别取低红色区间和高红色区间 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) lower_red1 np.array([0, 60, 60]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([160, 60, 60]) upper_red2 np.array([179, 255, 255]) mask cv2.bitwise_or( cv2.inRange(hsv, lower_red1, upper_red1), cv2.inRange(hsv, lower_red2, upper_red2), ) # 开闭运算去掉点状噪声再用闭运算把断裂的环形轮廓连起来 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((9, 9), np.uint8)) # 取面积最大的外轮廓做椭圆拟合 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) c max(contours, keycv2.contourArea) (cx, cy), (MA, ma), angle cv2.fitEllipse(c) # 按主轴方向旋转摆正angle 是长轴与水平方向的夹角 M cv2.getRotationMatrix2D((cx, cy), angle, 1.0) aligned cv2.warpAffine(img_bgr, M, (img_bgr.shape[1], img_bgr.shape[0])) return aligned, (cx, cy, MA, ma, angle)这段代码里有两个参数需要特别说明。lower_red1/upper_red1和lower_red2/upper_red2是两段式红色阈值因为红色在 HSV 的 Hue 通道取值范围跨越 0 和 180只取一段会漏掉偏粉或偏暗的印泥。MORPH_CLOSE的核取9x9是针对 1000 像素级别扫描图的经验值如果输入是手机拍摄的 3000 像素大图建议放大到15x15。椭圆拟合后还有一个隐藏问题盖章时用力不均匀圆形印章会变成椭圆。这时仅旋转不够还需要把长短轴端点映射到正圆位置做一次透视校正。常见做法是取椭圆长短轴两端四个点目标点是按印章半径计算的正圆四象限点然后求单应矩阵。这一步不做识别模型看到的文字宽窄不一微调效果会被打断。3. TrOCR预训练模型微调实战数据集制作、LoRA与训练参数TrOCR 的结构是 ViT 视觉编码器加 Transformer 文本解码器预训练阶段见过大量印刷体和手写体直接拿它推理中文公章主要问题是字体和排版的领域偏移而不是模型能力不足。微调的意义是把解码器的输出分布拉向“公司名称、防伪码、专用章后缀”这个目标空间。这里我按数据集、训练脚本、参数权衡三块来做。3.1 标注格式与数据集增强检测模型和识别模型需要分开准备数据集。检测侧用 YOLO 格式每张图对应一个 txt内容是类别和归一化框坐标。识别侧我统一用 JSONL每行一条样本路径和文本字段分离。{image: data/train/001.jpg, text: 北京某某科技有限公司} {image: data/train/002.jpg, text: 合同专用章}关键点是文本顺序。环形文字从哪个起点读会影响模型学习。我统一按“顺时针优先、从检测框中心向右 12 点钟方向开始”的规则人工标注训练时再配合随机旋转增强让模型不依赖绝对起点。数据增强列表如下随机旋转-20 到 20 度模拟盖章角度偏差径向扭曲幅度0.05模拟按压不均匀HSV 色相抖动-5 到 5模拟印泥深浅差异叠加 10% 透明度的灰度背景文字模拟盖在文件正文上的场景。注意不能对文本内容做增强比如不能把“有限公司”随机替换成“股份公司”这会造成标签噪声。另外同一个印章主体不能既进训练集又进验证集公章同一枚章有很多张扫描件场景不同文本完全相同混在一起会让验证集指标虚高这一点非常容易忽视。3.2 训练脚本用 transformers 微调 TrOCR我用 Hugging Face 的transformers做训练加载microsoft/trocr-large-chinese作为中文起点。脚本侧核心是pixel_values和labels的构造。import json from datasets import Dataset from PIL import Image from transformers import ( TrOCRProcessor, VisionEncoderDecoderModel, Seq2SeqTrainer, Seq2SeqTrainingArguments, default_data_collator, ) def load_jsonl(path): samples [] with open(path, r, encodingutf-8) as f: for line in f: samples.append(json.loads(line)) return samples processor TrOCRProcessor.from_pretrained(microsoft/trocr-large-chinese) model VisionEncoderDecoderModel.from_pretrained(microsoft/trocr-large-chinese) model.config.decoder_start_token_id processor.tokenizer.cls_token_id model.config.pad_token_id processor.tokenizer.pad_token_id def preprocess(sample): image Image.open(sample[image]).convert(RGB) pixel_values processor(image, return_tensorspt).pixel_values[0] labels processor.tokenizer( sample[text], paddingmax_length, max_length32, truncationTrue, return_tensorspt, ).input_ids[0] return {pixel_values: pixel_values, labels: labels}decoder_start_token_id必须显式设置为cls_token_idTrOCR 默认配置在加载后不会自动带上这个值漏掉这一行训练会在第一步就崩掉。max_length32覆盖了绝大多数公章的文本长度如果你们的章上有超过 30 个字的防伪编码建议放到 48。train_ds Dataset.from_list(load_jsonl(data/train.jsonl)).map(preprocess) eval_ds Dataset.from_list(load_jsonl(data/eval.jsonl)).map(preprocess) training_args Seq2SeqTrainingArguments( output_dir./trocr-stamp-final, per_device_train_batch_size8, per_device_eval_batch_size16, learning_rate5e-5, num_train_epochs20, evaluation_strategyepoch, save_strategyepoch, logging_steps50, fp16True, predict_with_generateTrue, generation_max_length32, ) trainer Seq2SeqTrainer( modelmodel, argstraining_args, train_datasettrain_ds, eval_dataseteval_ds, tokenizerprocessor.tokenizer, data_collatordefault_data_collator, ) trainer.train()这里predict_with_generateTrue是评估时真正走生成路径而不是只看 decoder 的交叉熵 loss。很多教程不开这个开关验证集 loss 一路下降实际推理出来的文字还是乱的典型的评估与推理不一致。3.3 LoRA微调与全参微调怎么选显存、收敛速度与最终效果全参微调 20 轮在 24G 显存上大约需要 6 到 8 小时。如果显存只有 12G 或者开了很多业务服务LoRA 微调是更稳的路子。LoRA 的本质是冻结原模型权重只训练注入的低秩矩阵可训练参数占比通常少于 5%显存占用降一半以上而且相当于给模型加了一层隐式正则在小数据集上不容易过拟合。我的经验是公章样本在 2000 张以下时全参微调的过拟合风险明显高于 LoRA特别是像“有限公司”这种高频词模型很容易把所有输出都拉向它。LoRA 的r8、alpha16在大多数公文字符集上表现够用。如果你们数据量超过 5000 张再考虑解冻 ViT encoder 的深层做全参微调。这个决策顺序建议反着来的人不少——一上来就全参微调刷高验证集指标后又发现真实场景泛化不行绕了一圈又回到 LoRA。4. 把模型接到企业业务流程推理封装、结构化输出与性能压测模型训练完只是第一步交付给业务时要解决的三个问题检测、摆正、识别怎么串成一条可调用的管线识别结果怎么落到业务字段批量跑历史档案时性能怎么控制。这一章把这三件事说透。4.1 推理管线封装检测、摆正、识别三段式我习惯把检测模型导出为 TorchScript推理时不再依赖原始训练框架避免版本冲突。识别侧保持 PyTorch 加载 TrOCR两个模型串成函数输入是原始图片输出是结构化字典。import torch DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) DETECTOR None MODEL None PROCESSOR None def run_stamp_ocr(image, det_thr0.5): # 检测模型输入是RGB图输出是Nx6的tensor前4列是坐标 boxes DETECTOR(image, conf_thresholddet_thr) results [] for box in boxes: x1, y1, x2, y2 [int(v) for v in box[:4]] crop image[y1:y2, x1:x2] # 检测框内再做椭圆摆正保证识别模型看到的文字是横向展开的 aligned, _ align_stamp_ellipse(crop) pixel_values PROCESSOR(aligned, return_tensorspt).pixel_values output_ids MODEL.generate( pixel_values.to(DEVICE), max_new_tokens32, num_beams4, no_repeat_ngram_size2, ) text PROCESSOR.batch_decode( output_ids, skip_special_tokensTrue )[0] results.append({ box: [x1, y1, x2, y2], text: text, }) return resultsnum_beams4是识别质量与速度的折中点beam 越大越慢公章文字本身较短4 足够。no_repeat_ngram_size2用来抑制生成式模型最常见的毛病——同一个词反复输出比如“有限公司有限公司”。这段代码里的det_thr0.5是检测阈值如果漏检多就往下调到 0.35代价是误检增多需要在后续结构化输出里过滤。4.2 结果结构化输出识别文本、置信度与坐标对齐业务方不关心模型只关心四个字段印章主体名称、印章类型、置信度、坐标。我在输出层统一成固定 schema字段类型说明seal_namestr识别出的公司名称或部门名称seal_typestr合同专用章 / 发票专用章 / 公章 等confidencefloat生成序列的平均 logit 概率bboxlist[int]检测框像素坐标rotate_anglefloat椭圆拟合得到的主轴角度从识别文本里拆seal_name和seal_type是件脏活。最简单可靠的做法是做后缀词典匹配比如文本以“合同专用章”结尾就把前缀当名称。如果文本里有统一社会信用代码用正则提 18 位字符再做校验位验证能一次性确认印章主体。我还会把同一张扫描件里重复出现的同一个印章按检测框的中心距离做聚类距离小于直径 30% 的视为同一枚章只保留置信度最高的那条避免一次扫描盖章两次时产生重复记录。4.3 批处理与性能压测卡在识别而非检测实际跑批时性能瓶颈几乎都在 TrOCR 解码端。一张 3000 像素的扫描图检测和摆正加起来不到 30 毫秒TrOCR 生成 20 个 token 要 150 毫秒以上。批处理优化我做了两件事。第一图像缩放。检测阶段把长边缩到 1280识别阶段保持摆正图的短边在 384 左右识别精度不会掉速度几乎翻倍。第二用梯度关闭和半精度推理模型推理时包在torch.no_grad()下model.to(torch.float16)。这两步做完单卡 A10 上吞吐能从每秒 3 张提到每秒 10 张左右。更大的优化空间不在模型而在并发方式——用线程池跑检测把识别请求排进 GPU 队列而不是每个请求都重新加载模型。5. TrOCR微调踩坑实录乱码、过拟合、漏检的排查路径这一章写的是我实际踩过且花了最长时间排查的几个坑每条都按现象、原因、解决来写。能提前避一个是一个这些坑不看日志真的很难定位。5.1 自动乱码解码出来全是[UNK]和繁体字混杂现象微调训练跑完验证集上 loss 很低但真实图片推理结果里出现大量[UNK]偶尔还混着繁体字。原因TrOCR 的中文 tokenizer 词汇表里简体中文覆盖率不是 100%生僻字、异体字被映射成[UNK]。训练时这类 token 被当作普通目标学习模型学到的是输出一个未知 token推理时自然乱码。解决微调前先跑一遍数据集的 token 覆盖率检查把所有不在词汇表里的字符找出来扩充自定义词典或者把少见字符归一到常用简体写法。我后来直接在预处理里加了一个字符映射表把“臺”映射成“台”“發”映射成“发”覆盖率从 92% 提到 99.6%。5.2 模型只会输出“有限公司”数据不平衡导致的高频词坍缩现象无论输入是哪家公司的章识别结果都带“有限公司”有些甚至整句就是“有限公司”。原因训练集里超过 60% 的样本文本含“有限公司”模型发现输出这个序列损失下降最快于是收敛到局部最优把所有解码结果都拉向高频词。验证集里同样有大量“有限公司”指标看上去还挺好一上业务数据就露馅。解决按文本样本做重采样让各印章主体的样本数基本均衡同一种公司名后缀最多占训练集 25%。同时把背景干扰样本、带防伪编码的样本比例提上去迫使模型关注字符本身而不是猜大概率词。5.3 检测框半包住印章YOLO微调崩在和预处理矛盾现象目标检测模型微调之后检测框经常只框住半个印章或者框住五角星区域识别端拿到的图像残缺文字断裂。原因YOLO 预训练模型下载后直接拿原始扫描图训练但扫描图里印章只占画面一小部分且大角度旋转的印章在锚框匹配阶段就被当成难例忽略。另外我只给了坐标标签没有给椭圆角度标签检测器学不到“旋转目标”的表达。解决第一训练前先做缩放让印章短边至少占图像短边的 40%避免小目标采样不足第二把检测框的标注改成外接水平框而不是手工标的前景矩形两者差异会导致 loss 震荡第三如果印章形变严重给检测头接一个 angle 回归分支输出旋转角度供下游摆正使用。5.4 置信度 0.95 但文字完全不对生成式识别的置信度陷阱现象推理结果里有一条文本置信度 0.95但人工核对发现内容完全错误且错误很离谱与真实公章内容毫无关系。原因TrOCR 是生成式模型解码时每个 token 的概率都很高最终置信度是 token 概率的均值对于短文本模型哪怕只学到了高频词的局部模式也能输出高置信度但语义无关的序列。这和分类模型里“置信度越高越可信”的直觉完全不同。解决不再单看平均置信度加一个文本级规则校验。比如公文中必须含“公司”“银行”“章”之一统一社会信用代码必须过 18 位校验位算法识别文本里的公司名称要与数据库做前缀匹配。规则不通过时标记为低置信样例进入人工复核队列而不是直接落库。6. 上线前做三件事指标勾稽、失败样本看板与 badcase 闭环模型在测试集上跑完不等于能上线我还差最后一道工序给业务方一个可以解释的验收依据。这套三重校验流程每次上线识别模型都会强制走一遍哪怕只是改了学习率重新微调。第一件指标勾稽。不只看字符准确率要看业务字段的勾稽关系。我会写一个扫描脚本把识别文本里的统一社会信用代码提取出来按 18 位校验位算法做自动化验证。校验通过率低于 98% 就直接打回重训。def is_valid_credit_code(code): if len(code) ! 18: return False weights [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28] chars 0123456789ABCDEFGHJKLMNPQRTUWXY total 0 for i, ch in enumerate(code[:-1]): total chars.index(ch) * weights[i] check (11 - total % 11) % 11 return code[-1] X if check 10 else code[-1] str(check)这段代码的weights是国标 GB 32100-2015 规定的加权因子字符表里特意去掉了 I、O、S、V、Z 这几个易混字母。勾稽校验的价值是把“模型说识别对了”变成“业务上确实对得上”。第二件失败样本看板。把所有推理失败的样本按检测置信度、识别置信度、印章角度、图片分辨率四个维度落成数据表画四张散点图。一张看板上如果能看出失败样本集中在“角度大于 60 度”或“分辨率低于 800 像素”的区域说明问题出在预处理而不是模型调整空间就清楚了。第三件badcase 闭环。每个失败样本保存一张现场截图截图上画检测框、摆正前图像、摆正后图像、识别文本、人工复核结论统一存成 JSONL。下一轮微调时直接把这些样本按 1:1 比例并入训练集。这个习惯坚持三个月后识别准确率提升的幅度会明显大于单纯增加随机增强强度。从那以后我每次上线识别模型都强制走一遍这三件事哪怕只是改了学习率重新微调。这个习惯帮我挡掉过至少三次上线后才发现的数据泄漏事故希望帮到你。本文还有配套的精品资源点击获取