C# 身份证图片信息识别源码:从照片到结构化字段的落地路径
简介:这份资源是面向 C# 与 WinForm 开发者的身份证图片信息识别源码工具,适合需要在本机快速实现证件信息提取的初学者与中级开发者。它调用 Windows 自有接口完成图片识别,再解析出姓名、年龄、出生年月日、身份证号码、民族及住址等关键字段,并在前台界面中直观展示,可用于门禁登记、信息录入等场景。压缩包共 33 个文件,约 13.13MB,以 cs 源码、exe 可执行程序、resx 与 resources 资源文件、bmp 与 jpg 示例图片为主,另含 sln 解决方案、csproj 工程文件及少量 dat、pdb 等辅助文件,结构完整可直接编译运行。目前已有 1490 人学习下载。下载后若需调试,只需将 Form1_DragDrop 方法中的图片地址改为本机身份证图片路径即可;方案免费、识别率尚可,实际效果主要取决于图片清晰度与拍摄角度,便于读者快速理解接口调用与信息解析的完整流程。
1. C# 身份证图片信息识别源码:从一张手机照片到结构化字段的落地路径
拍一张身份证照片,扔进程序,几秒后拿到姓名、性别、民族、出生日期、住址、身份证号——这个需求在访客登记、酒店入住、政务自助终端、金融开户等场景里反复出现。C# 身份证图片信息识别源码要解决的,就是把这个过程做成一个可集成、可调试、可上线的模块,而不是停留在「调个接口就完事」的层面。适合谁看?正在做 C# 上位机、WinForm/WPF 桌面端、ASP.NET Core 后端服务的开发者,手里有 OCR 需求但不确定选型、预处理和字段校验怎么串起来的人。这篇笔记按「选型 → 预处理 → 识别 → 解析 → 避坑 → 进阶」的顺序展开,代码可以直接抄,参数可以按你的场景改。
2. 选型先定死:C# 做身份证 OCR 的三条技术路线
2.1 本地推理、云 API、混合方案怎么选
在 C# 里做身份证识别,绕不开三条路。第一条是纯本地推理,用 PaddleOCRSharp、OpenCvSharp 配合 ONNX Runtime 加载检测和识别模型,整个流程跑在客户端,不依赖网络。第二条是调云厂商的 OCR API,把图片 base64 编码后 POST 出去,拿回 JSON 结果。第三条是混合:本地做检测和裁剪,云端做识别,或者反过来。
选型不是拍脑袋,看四个维度:数据合规、网络环境、单次成本、维护成本。身份证属于敏感个人信息,很多项目在需求评审阶段就被要求「图片不出内网」,这时候云 API 直接出局。如果部署环境是工厂车间、政务大厅内网,网络本身就不通外网,也只能走本地。反过来,如果是一个日调用量几百次的小工具,本地部署模型文件动辄几百 MB,还要处理 GPU/CPU 推理环境,维护成本反而高于按量付费的云 API。
我一般会这样判断:日均识别量低于 500 次、且没有强合规要求,先用云 API 快速验证业务闭环;一旦量上来或者合规收紧,再切本地。混合方案适合「本地检测 + 云端识别」这种折中,但要注意两次传输带来的延迟叠加。
| 路线 | 依赖 | 单次成本 | 合规风险 | 适合场景 |
|---|---|---|---|---|
| 本地推理 | ONNX 模型 + OpenCvSharp | 一次性部署 | 低 | 内网、高频、敏感数据 |
| 云 API | HTTP + 网络 | 按量计费 | 中高 | 快速验证、低频 |
| 混合 | 两者都有 | 中 | 中 | 检测本地化、识别云端化 |
2.2 本地推理的最小依赖组合
本地路线里,PaddleOCRSharp 是目前 C# 生态里封装比较完整的方案,它把 PaddleOCR 的检测、方向分类、识别三个模型打包成 NuGet 包,调用时不需要自己写 ONNX 会话管理。另一个选择是直接用 Microsoft.ML.OnnxRuntime 加载自己导出的模型,灵活但工作量大。
最小依赖组合是这样的:PaddleOCRSharp 负责文字检测和识别,OpenCvSharp4 负责图片预处理(灰度、二值化、透视校正),Newtonsoft.Json 或 System.Text.Json 负责结果解析。这三个包通过 NuGet 安装即可,不需要额外配置 Python 环境。
dotnet add package PaddleOCRSharp dotnet add package OpenCvSharp4 dotnet add package OpenCvSharp4.runtime.win dotnet add package Newtonsoft.Json这里要注意OpenCvSharp4.runtime.win是 Windows 下的原生库包,Linux 部署要换成对应的 runtime 包。PaddleOCRSharp 的模型文件默认随包携带,首次运行会在输出目录生成inference文件夹,里面是 det、cls、rec 三个模型。如果部署到没有写权限的目录,需要提前把模型文件复制到可写路径,并通过配置指定模型目录。
2.3 云 API 路线的接入要点
云 API 路线的核心是把图片转成 base64,构造请求体,处理返回的 JSON。不同厂商的字段名不一样,但结构大同小异:一个words_result数组,每个元素包含words和location。身份证识别通常还会返回id_card_number、name、address等结构化字段,省去自己解析的麻烦。
接入时最容易翻车的地方是图片大小限制和编码格式。多数云 API 要求 base64 后的字符串不超过 4MB,对应原图大约 3MB。手机拍的照片动辄 5MB 以上,直接传会报错。常见做法是先压缩到长边 1280 像素、JPEG 质量 80,再编码。另外,base64 字符串里的+和/在 URL 传输时会被转义,如果走 GET 请求要 URL 编码,走 POST 放在 body 里则不需要。
// 压缩图片到指定长边,返回 JPEG 字节数组 public static byte[] CompressImage(string imagePath, int maxSide = 1280, int quality = 80) { using var src = Cv2.ImRead(imagePath); int w = src.Width, h = src.Height; double scale = Math.Min(1.0, (double)maxSide / Math.Max(w, h)); using var dst = new Mat(); Cv2.Resize(src, dst, new Size((int)(w * scale), (int)(h * scale))); Cv2.ImEncode(".jpg", dst, out byte[] buf, new ImageEncodingParam(ImwriteFlags.JpegQuality, quality)); return buf; }maxSide控制长边像素,身份证文字在 1280 像素下通常足够识别;quality是 JPEG 压缩质量,80 是清晰度和体积的平衡点。如果识别率不理想,先把maxSide提到 1600 再试,不要一上来就调质量参数。
3. 图片预处理:决定识别率的上限
3.1 透视校正与边缘检测
身份证照片最大的问题是拍摄角度。斜着拍、有透视变形、背景杂乱,都会让 OCR 的检测框偏移。OpenCvSharp 里做透视校正的流程是:灰度化 → 高斯模糊 → Canny 边缘检测 → 找轮廓 → 筛选四边形 → 计算透视变换矩阵 → warpPerspective。
关键在筛选四边形。身份证是矩形,面积占比大,长宽比接近 1.58:1。用Cv2.ApproxPolyDP逼近轮廓后,判断顶点数是否为 4、面积是否大于图片面积的 20%、长宽比是否在 1.3 到 1.9 之间。满足条件的轮廓才认为是身份证边缘。
// 对输入图片做透视校正,返回校正后的 Mat public static Mat CorrectPerspective(Mat src) { using var gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); Cv2.GaussianBlur(gray, gray, new Size(5, 5), 0); using var edges = new Mat(); Cv2.Canny(gray, edges, 50, 150); Cv2.FindContours(edges, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); foreach (var c in contours.OrderByDescending(c => Cv2.ContourArea(c))) { var approx = Cv2.ApproxPolyDP(c, 0.02 * Cv2.ArcLength(c, true), true); if (approx.Length != 4) continue; double area = Cv2.ContourArea(approx); if (area < src.Width * src.Height * 0.2) continue; var rect = Cv2.MinAreaRect(approx); double ratio = Math.Max(rect.Size.Width, rect.Size.Height) / Math.Min(rect.Size.Width, rect.Size.Height); if (ratio < 1.3 || ratio > 1.9) continue; return Warp(src, approx); } return src.Clone(); // 没找到四边形就返回原图 }0.02 * ArcLength是轮廓逼近的精度参数,值越小逼近越精细,但可能把矩形逼近成五边形;值越大越粗糙,可能把矩形逼近成三角形。1.3 到 1.9 的长宽比范围是给拍摄角度留的余量,正拍时接近 1.58,斜拍时会偏离。如果身份证有圆角,ApproxPolyDP可能返回 8 个顶点,这时候要改用MinAreaRect直接取最小外接矩形。
3.2 灰度化与自适应二值化的参数
透视校正之后,图片可能还有光照不均的问题。手机闪光灯打在身份证上,中间亮、边缘暗,全局阈值二值化会把暗部文字丢掉。这时候用自适应阈值:Cv2.AdaptiveThreshold,块大小取 31 或 45,常数 C 取 10 到 15。
// 自适应二值化,增强文字对比度 public static Mat Binarize(Mat src) { using var gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); using var blurred = new Mat(); Cv2.MedianBlur(gray, blurred, 3); var binary = new Mat(); Cv2.AdaptiveThreshold(blurred, binary, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 31, 12); return binary; }MedianBlur的核大小 3 是去椒盐噪声的,如果图片本身干净可以去掉。AdaptiveThreshold的块大小必须是奇数,31 对应大约 31 像素的局部区域,身份证文字高度通常在 20 到 40 像素之间,块大小取文字高度的 1 到 1.5 倍比较合适。常数 C 从阈值中减去,值越大二值化后黑色区域越多,文字容易断;值越小白色区域越多,背景噪声容易保留。12 是一个保守起点,识别率低时先调这个参数。
提示:预处理不是越多越好。如果原图质量已经很好,透视校正和二值化反而可能引入伪影。建议在识别前加一个质量判断:拉普拉斯方差大于 100 且亮度均值在 80 到 180 之间,直接跳过预处理。
3.3 裁剪与 ROI 提取
身份证的字段位置是固定的:姓名在左上,性别和民族在右上,出生日期在中间偏下,住址在下方,身份证号在最底部。如果检测阶段已经拿到了文字框坐标,可以按坐标裁剪出每个字段的 ROI,再单独识别。这样做的好处是识别模型只需要处理小图,速度快,而且可以针对每个字段设置不同的识别参数。
裁剪时要注意留边距。文字框的坐标是紧贴文字的,直接裁剪可能把边缘笔画切掉。常见做法是向外扩 5 到 10 像素,或者按文字框高度的 20% 扩展。如果文字框本身有倾斜,裁剪后还要做一次小角度旋转校正。
// 按文字框坐标裁剪 ROI,并向外扩展边距 public static Mat CropRoi(Mat src, Rect box, int padding = 8) { int x = Math.Max(0, box.X - padding); int y = Math.Max(0, box.Y - padding); int w = Math.Min(src.Width - x, box.Width + padding * 2); int h = Math.Min(src.Height - y, box.Height + padding * 2); return new Mat(src, new Rect(x, y, w, h)); }padding取 8 是一个经验值,对应 1280 像素宽图片上的文字框。如果原图更大,按比例放大。裁剪后的 ROI 可以直接送入识别模型,也可以先做一次锐化再识别。锐化用Cv2.Filter2D配合一个 3x3 的卷积核,中心权重 5,周围 -1,能增强笔画边缘。
4. 识别与字段解析:从文字行到结构化数据
4.1 PaddleOCRSharp 的调用与参数配置
PaddleOCRSharp 的调用分三步:初始化引擎、设置参数、执行识别。引擎初始化时可以指定模型路径、是否使用 GPU、线程数。参数里比较关键的是DetDBThresh(检测阈值)、DetDBBoxThresh(检测框阈值)、DetDBUnclipRatio(检测框扩张比例)和RecImgH(识别输入高度)。
// 初始化 OCR 引擎并识别图片 var engine = new PaddleOCREngine(new OCRModelConfig { det_infer = "inference/det", cls_infer = "inference/cls", rec_infer = "inference/rec", keys = "inference/ppocr_keys_v1.txt" }, new OCRParameter { use_gpu = false, cpu_threads = 4, det_db_thresh = 0.3f, det_db_box_thresh = 0.5f, det_db_unclip_ratio = 1.6f, rec_img_h = 48, rec_img_w = 320 }); var result = engine.DetectText(compressedBytes); foreach (var block in result.TextBlocks) { Console.WriteLine($"{block.Text} | 置信度: {block.Score:F3} | 位置: {block.BoxPoints}"); }det_db_thresh是分割网络的二值化阈值,值越低检测框越多,但可能引入噪声;0.3 是默认值,图片干净时可以提到 0.4 减少误检。det_db_box_thresh是检测框的置信度过滤,低于这个值的框会被丢弃,0.5 是平衡点。det_db_unclip_ratio控制检测框向外扩张的比例,1.6 意味着扩张 60%,这是为了把文字边缘包进去,如果文字被切掉可以提到 2.0。rec_img_h是识别模型输入的高度,48 是标准值,身份证文字高度足够,不需要改。
4.2 身份证字段的正则匹配与校验
OCR 返回的是一行行文字,要变成结构化字段,需要按位置和内容做匹配。姓名和住址是中文,性别是「男」或「女」,民族是「汉」「回」等,出生日期是「YYYY年MM月DD日」或「YYYY.MM.DD」,身份证号是 18 位数字加最后一位可能是 X。
正则匹配的顺序很重要。先匹配身份证号,因为它的模式最固定:\d{17}[\dXx]。匹配到之后,从身份证号里提取出生日期和性别,再和 OCR 识别的出生日期、性别做交叉验证。如果两者不一致,以身份证号为准,因为 OCR 可能把「1」识别成「l」或「I」。
// 从 OCR 结果中解析身份证字段 public static IdCardInfo Parse(List<TextBlock> blocks) { var info = new IdCardInfo(); var allText = string.Join(" ", blocks.Select(b => b.Text)); // 身份证号 var idMatch = Regex.Match(allText, @"\d{17}[\dXx]"); if (idMatch.Success) { info.IdNumber = idMatch.Value.ToUpper(); info.BirthDate = info.IdNumber.Substring(6, 8); info.Gender = int.Parse(info.IdNumber.Substring(16, 1)) % 2 == 1 ? "男" : "女"; } // 姓名:取身份证号之前、长度 2-4 的中文串 var nameMatch = Regex.Match(allText, @"[\u4e00-\u9fa5]{2,4}"); if (nameMatch.Success) info.Name = nameMatch.Value; // 住址:包含「省」「市」「区」「县」「路」「号」等关键词的长串 var addrMatch = Regex.Match(allText, @"[\u4e00-\u9fa5]{6,}(省|市|区|县|路|号)[\u4e00-\u9fa5\d]*"); if (addrMatch.Success) info.Address = addrMatch.Value; return info; }姓名匹配用[\u4e00-\u9fa5]{2,4}是因为汉族姓名通常 2 到 4 个字,但少数民族姓名可能更长,这时候要放宽到 6 个字,或者结合位置信息——姓名通常在身份证号上方、住址上方。住址匹配的关键词列表可以扩展,加上「镇」「乡」「村」「组」「栋」「单元」「室」等。如果 OCR 把「省」识别成「者」,正则就匹配不到,这时候要用模糊匹配或者编辑距离。
4.3 置信度过滤与人工复核队列
OCR 不是 100% 准确,低置信度的结果要过滤出来。PaddleOCRSharp 返回的每个 TextBlock 都有 Score 字段,范围 0 到 1。身份证号、姓名这类关键字段,置信度低于 0.85 就应该进入人工复核队列,而不是直接入库。
复核队列的实现方式取决于你的系统架构。如果是桌面端,弹一个窗口让用户确认;如果是后端服务,把图片和 OCR 结果存到一张待复核表,提供一个管理界面。复核时要展示原图、OCR 结果和置信度,让用户快速判断是改还是重拍。
// 按置信度分流:高于阈值自动通过,低于阈值进入复核 public static void Dispatch(List<TextBlock> blocks, double threshold = 0.85) { var autoPass = new List<TextBlock>(); var needReview = new List<TextBlock>(); foreach (var b in blocks) { if (b.Score >= threshold) autoPass.Add(b); else needReview.Add(b); } Console.WriteLine($"自动通过 {autoPass.Count} 条,待复核 {needReview.Count} 条"); foreach (var b in needReview) Console.WriteLine($"待复核: {b.Text} (置信度 {b.Score:F3})"); }阈值 0.85 是一个保守起点。如果业务对准确率要求极高(比如金融开户),可以提到 0.92;如果只是访客登记,0.75 也能接受。注意置信度是模型输出的概率,不是真实准确率,实际准确率还受图片质量影响。建议在测试集上跑一遍,画出置信度和准确率的关系曲线,再定阈值。
5. 避坑与排查:身份证 OCR 落地时最容易翻车的五个点
5.1 识别率忽高忽低,同一张图两次结果不一样
现象:同一张身份证照片,第一次识别姓名正确,第二次识别成乱码,或者身份证号少一位。
原因:PaddleOCRSharp 的检测阶段有随机性,尤其是det_db_thresh接近临界值时,检测框的位置和数量会波动。另外,如果图片预处理里用了Cv2.Canny,边缘检测对光照敏感,不同次读取的图片如果有轻微压缩差异,边缘也会变。
解决:固定随机种子,把det_db_thresh调到远离临界值的位置(比如 0.35 或 0.25),预处理阶段尽量用确定性算法,避免Canny的双阈值设得太窄。如果还是不稳定,把检测和识别拆开,检测结果缓存下来,只对识别做多次投票。
5.2 身份证号里的「1」被识别成「l」或「I」
现象:身份证号110101199001011234识别成ll0l0l19900l0ll234。
原因:OCR 模型在训练时对数字和字母的区分不够,尤其是身份证号这种长数字串,字体细、对比度低时容易混淆。另外,如果图片经过二值化,数字「1」的顶部横线可能被腐蚀掉,变成竖线。
解决:在解析阶段做字符替换,把l、I、|统一替换成1,把O、o替换成0,把S替换成5。替换后再用身份证号的校验位算法验证,如果校验不通过,说明替换有误,进入人工复核。校验位算法是:前 17 位加权求和,权重为[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2],和对 11 取模,映射到[1,0,X,9,8,7,6,5,4,3,2]。
// 身份证号校验位验证 public static bool ValidateIdNumber(string id) { if (id.Length != 18) return false; int[] weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 }; char[] checkCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' }; int sum = 0; for (int i = 0; i < 17; i++) sum += (id[i] - '0') * weights[i]; return checkCodes[sum % 11] == id[17]; }5.3 住址字段被截断或串行
现象:住址只识别出「XX省XX市」,后面的「XX区XX路XX号」丢了,或者和出生日期串在一起。
原因:身份证住址可能有两行甚至三行,OCR 按行返回,如果按单个 TextBlock 解析,只能拿到一行。另外,如果检测框把两行文字合并成一个框,识别结果会串行。
解决:解析住址时,把所有 TextBlock 按 Y 坐标排序,取身份证号上方、姓名下方的所有文字块,按行拼接。如果检测框合并了多行,用det_db_unclip_ratio调小到 1.2 到 1.4,让检测框更贴合单行文字。拼接时注意去掉行尾的标点,避免「XX路,」这种带逗号的结果。
5.4 部署到 Linux 后模型加载失败
现象:Windows 上跑得好好的,部署到 Linux 服务器后报DllNotFoundException或模型文件找不到。
原因:PaddleOCRSharp 的原生库是平台相关的,Windows 的 NuGet 包不包含 Linux 的.so文件。另外,模型文件的路径在 Windows 上用反斜杠,Linux 上用正斜杠,硬编码路径会出问题。
解决:安装PaddleOCRSharp的 Linux runtime 包,或者手动把libpaddle_inference.so放到输出目录。模型路径用Path.Combine拼接,不要写死分隔符。如果用的是 Docker,基础镜像选mcr.microsoft.com/dotnet/runtime,然后在 Dockerfile 里apt-get install libgdiplus和libc6-dev,这两个是 OpenCvSharp 的依赖。
5.5 并发调用时内存暴涨
现象:单次识别正常,但 10 个请求并发时内存从 200MB 涨到 2GB,甚至 OOM。
原因:PaddleOCRSharp 的引擎实例不是线程安全的,每次new PaddleOCREngine都会加载一份模型到内存。如果每个请求都新建引擎,内存会成倍增长。另外,Mat对象如果不释放,非托管内存不会自动回收。
解决:引擎用单例,全局初始化一次,所有请求共享。Mat对象用using包裹,或者手动调用Dispose()。如果并发量高,用SemaphoreSlim限制同时识别的请求数,比如限制为 CPU 核心数。识别完成后,把结果转成 POCO 对象,不要持有Mat或TextBlock的引用。
// 单例引擎 + 并发限制 private static readonly PaddleOCREngine _engine = new PaddleOCREngine(config, parameter); private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(Environment.ProcessorCount); public static async Task<IdCardInfo> RecognizeAsync(byte[] imageBytes) { await _semaphore.WaitAsync(); try { var result = _engine.DetectText(imageBytes); return Parse(result.TextBlocks); } finally { _semaphore.Release(); } }6. 进阶技巧:用模板匹配兜底和批量验证
6.1 模板匹配兜底低置信度字段
当 OCR 对某个字段的置信度低于阈值时,除了人工复核,还可以用模板匹配做兜底。身份证的字段位置固定,可以预先截取一张标准身份证的姓名区域、身份证号区域作为模板,用Cv2.MatchTemplate在待识别图片上找匹配位置,然后裁剪出来单独识别。
模板匹配的局限是它对旋转和缩放敏感。如果待识别图片的透视校正做得好,模板匹配的准确率很高;如果校正不到位,匹配分数会很低。我一般把模板匹配作为第二道防线,只在 OCR 置信度低于 0.7 时启用,匹配分数低于 0.6 就放弃,直接进人工队列。
// 模板匹配定位字段区域 public static Rect? MatchTemplate(Mat src, Mat template, double threshold = 0.6) { using var result = new Mat(); Cv2.MatchTemplate(src, template, result, TemplateMatchModes.CCoeffNormed); Cv2.MinMaxLoc(result, out double minVal, out double maxVal, out Point minLoc, out Point maxLoc); if (maxVal < threshold) return null; return new Rect(maxLoc.X, maxLoc.Y, template.Width, template.Height); }CCoeffNormed是归一化相关系数,范围 -1 到 1,越接近 1 越匹配。阈值 0.6 是经验值,实际项目里要根据模板质量和图片质量调整。模板本身要从多张标准身份证里截取,取平均或者选最清晰的一张。
6.2 批量验证与准确率统计
上线前一定要做批量验证。准备 100 到 200 张真实身份证照片,覆盖不同光照、角度、背景、手机型号,跑一遍识别,统计每个字段的准确率。准确率的计算方式是:字段完全正确才算对,错一个字也算错。
统计结果按字段分开看。身份证号和出生日期的准确率通常最高,因为模式固定;住址的准确率最低,因为字数多、分行、有生僻字。如果住址准确率低于 90%,优先优化预处理和检测参数,而不是换模型。
// 批量验证并输出各字段准确率 public static void BatchValidate(string imageDir, string labelFile) { var labels = LoadLabels(labelFile); // 格式: 文件名,姓名,身份证号,住址 int nameOk = 0, idOk = 0, addrOk = 0; foreach (var (file, expected) in labels) { var bytes = File.ReadAllBytes(Path.Combine(imageDir, file)); var actual = RecognizeAsync(bytes).Result; if (actual.Name == expected.Name) nameOk++; if (actual.IdNumber == expected.IdNumber) idOk++; if (actual.Address == expected.Address) addrOk++; } Console.WriteLine($"姓名准确率: {(double)nameOk / labels.Count:P2}"); Console.WriteLine($"身份证号准确率: {(double)idOk / labels.Count:P2}"); Console.WriteLine($"住址准确率: {(double)addrOk / labels.Count:P2}"); }批量验证的标签文件要人工标注,标注时注意住址的格式统一,比如是否包含省市区前缀、是否去掉空格。如果标签和 OCR 输出的格式不一致,准确率会虚低。建议在比对前做一次归一化:去掉所有空格和标点,统一全角半角。
6.3 我踩过的坑和现在的习惯
最早做身份证识别时,我迷信「模型越新越好」,直接上了最新的检测模型,结果在低质量图片上还不如老模型稳定。后来才明白,身份证识别是封闭场景,字段固定、字体固定,模型不需要太强的泛化能力,反而需要在这个特定分布上足够鲁棒。现在我的习惯是:先用默认模型跑一遍批量验证,看哪个字段拖后腿,再针对性地调预处理参数,最后才考虑换模型。
另一个习惯是永远保留原图和 OCR 原始结果。用户反馈识别错误时,没有原图就无法复现,没有原始结果就无法判断是检测错了还是识别错了。存储成本很低,但排查问题时能省下大量时间。
希望帮到你。
本文还有配套的精品资源,点击获取