尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#实现OCR:图片与扫描PDF文本提取完整指南

C#实现OCR:图片与扫描PDF文本提取完整指南 做了这么多年 C# 开发最让我觉得“不起眼但极度救场”的功能就是给图片和扫描PDF提取文本。这种需求几乎每个项目周期都会遇到客户发来一批扫描合同要检索关键字、上位机要读仪表屏幕截图、或者档案系统要把旧的纸质材料变成可搜的电子文本。用 C# 读取图片和扫描PDF中的文本本质上就是两个技术点找一个能用的OCR引擎再把扫描PDF当成“一张张图片”逐页喂给它。这篇博文就围绕这两个点把我自己踩过的坑和最终沉淀下来的可复用方案讲清楚。如果你正在做文档归档系统、自动录入工具或者资料检索功能这篇文章可以直接照着抄作业也能帮你少走不少弯路。1. 需求拆解扫描PDF和图片提文本难在哪1.1 扫描PDF和普通PDF的本质区别先说一个很容易让新手栽跟头的概念扫描PDF本质上不是“电子文档”它是“图片的集合”。普通的PDF里面保存的是文字、字体、排版指令你用PDF阅读器选中文字可以直接复制。但扫描PDF是扫描仪或者手机拍完之后直接合成的一个PDF每一页其实是一整张像素图。正因为如此直接从扫描PDF里读取文字是不可能的必须先把页面渲染成图片再做OCR识别。这个区别决定了技术路径如果项目里既有普通PDF又有扫描PDF你不能统一走同一条解析通道。先说明这一点后面处理逻辑就好理解了。在我实际做的文档管理系统里我会先尝试用文本提取库读取PDF如果提取出来全是空字符串基本可以断定它没有文本层再自动切换到“渲染OCR”通道。这个判断逻辑非常实用能防止你把原本就有文本层的PDF也白白消耗OCR时间毕竟OCR比文本提取慢得多。1.2 这类需求常见的业务场景说实话能用得上“读取图片和扫描PDF文本”的地方太杂了。我从自己接过的项目和周围同行聊到的情况里挑几个高频场景档案数字化公司把十年来的纸质合同、报销单、人事档案扫描成PDF需要建立全文检索功能关键词能定位到具体页面。发票与证件识别财务要提取发票号、金额、税率人事要提取身份证号和姓名这本质是图片OCR之后再做正则结构化。上位机与工业视觉很多设备屏幕没有数据接口只能通过摄像头抓图或截屏然后把读到的数字文本回填到MES系统。这就是C#上位机里常说的“读图取数”。手机拍照资料入库用户随手拍的照片有脏背景、歪斜、光照不均OCR之前还得做图像预处理。这些场景的共同点在于文本以“图像”形式存在必须识别而不是“读取”。所以掌握一套稳定的C# OCR方案在你做任何数据类项目时都是加分项。而且这类需求一旦上手很容易形成一套通用的工具类后面新项目直接复用性价比非常高。2. 技术选型C#生态下OCR和PDF渲染库怎么搭配2.1 OCR引擎为什么我选TesseractC#生态里能用的OCR引擎其实不多主要就是Tesseract的封装库、Windows自带的OCR接口、以及一些商用SDK。商用SDK识别率高但收费接口还得看厂商文档Windows.Media.Ocr在UWP里能用但放到普通控制台服务或跨平台部署时会有兼容性麻烦。所以工程上最稳的还是Tesseract社区活跃、语言包全、离线可用而且基于LSTM的识别模型在中文场景下表现已经相当够用。在NuGet里对应的包叫Tesseract作者的封装做得比较清晰。需要注意版本老项目里常见的是3.x封装对应Tesseract 3引擎新项目尽量用5.x底层是Tesseract 5识别准确度和速度都有提升。我项目里用的Tesseract NuGet版本是5.2.0支持net8.0完全没有问题。如果只是做一个小工具也可以省掉封装库直接调Tesseract原生命令行但那样在并发、结果解析和异常处理上会麻烦很多所以我不建议。2.2 扫描PDF渲染Pdfium全家桶怎么选扫描PDF要转成图片这一步我用的是Pdfium也就是Google开源的PDF解析引擎的封装。选择理由很简单开源、解析稳定、渲染质量高、支持跨平台。NuGet上围绕Pdfium的呈现选择有好几个我整理了一个对比表格包名特点适用场景PdfiumViewer老牌封装包含WinForms控件但更新较慢桌面工具、需要界面预览时PDFtoImage基于Pdfium呈现代码简洁跨平台服务端批量渲染推荐Docnet.Core偏底层文档少定制性强需要深度定制渲染逻辑时我自己在项目里用PDFtoImage更多因为它API非常简洁。核心就一行把PDF路径传进去它会返回每一页的图片流然后直接对图片做OCR。如果你做的是WinForms工具需要界面预览那PdfiumViewer更合适。这个选择并不冲突工具型项目和服务型项目各用各的就好。不必纠结哪个更高级关键是看你的程序在哪跑、界面要不要展示。2.3 语言包、运行库和部署准备Tesseract默认只带一套英文语言数据或者某些环境只装了eng所以中文识别很容易出现问题。你需要单独下载chi_sim.traineddata也就是简体中文语言包放到程序的tessdata目录下。官方仓库分了tessdata_fast和tessdata_best两个版本区别很明显tessdata_fast体积小识别速度快适合批量处理、对精度要求不太高的场景。tessdata_best体积大识别精度更高适合证件、票据等需要精确结果的场景。我建议先下载tessdata_fast跑通流程如果准确率不够再替换成tessdata_best重测。另外如果你处理的材料里中英文混杂引擎语言参数要写chi_simeng不能只写中文否则英文单词会被拆散断句乱掉。部署时还有一个坑服务器上的语言模型目录一定要跟着程序一起走或者在配置里明确指定绝对路径。我遇到过同事把tessdata忘了拷贝程序部署到服务器后直接抛异常报“failed to load language chi_sim”排查半天才发现是模型文件没带上去。这类问题其实很好定位但头一回遇到时确实容易慌以为是代码写错了。3. 核心实现从图片和扫描PDF中提取文本的完整流程3.1 项目搭建与NuGet包安装直接用.NET 8的控制台项目演示命令行如下dotnet new console -n OcrDemo cd OcrDemo dotnet add package Tesseract --version 5.2.0 dotnet add package PDFtoImage --version 4.0.2 dotnet add package SixLabors.ImageSharp如果你用的是Visual Studio直接在NuGet包管理器里搜以上三个包即可。ImageSharp不是必需的但它用来做图像预处理非常方便而且跨平台不会出现System.Drawing在Linux上不能用的问题。如果你只是Windows上做个小工具用System.Drawing.Common也没问题但要注意在.NET 6之后它在非Windows平台不受支持服务端部署就可能是个大坑。3.2 图片OCR最基础也最核心的调用图片读取文本的代码非常简短但每个细节都有讲究using Tesseract; using var engine new TesseractEngine(./tessdata, chi_simeng, EngineMode.LstmOnly); using var pix Pix.LoadFromFile(D:\samples\invoice.png); using var page engine.Process(pix); string rawText page.GetText(); Console.WriteLine(rawText); foreach (var word in page.GetSegmentedRegions(PageIteratorLevel.Word)) { Console.WriteLine($[{word.X1},{word.Y1},{word.X2},{word.Y2}]); }这段代码里几个点要解释一下EngineMode.LstmOnly表示只用LSTM神经网络模型识别比默认的LegacyLSTM混合模式要快一些实际效果差别不大。Pix.LoadFromFile是Tesseract封装库提供的图片类型支持常见图片格式。如果你手上的图是Base64字符串可以用Pix.LoadFromMemory先转一下。GetText拿到的就是一个整页纯文本带换行符但一般不带坐标。GetSegmentedRegions能返回每个词的边界框这个能力在做区域定位、表单录入的时候非常有用。不少新手会问为什么我的图片识别出来全是乱码大多数时候不是引擎问题而是图片质量太差。OCR的本质是模式识别像素模糊、噪点多、对比度低再强的模型也白搭。所以在真实项目中我通常会在调用OCR之前先做一次图像预处理。3.3 扫描PDF逐页处理渲染识别组合拳扫描PDF的处理流程就是“页面渲染”和“图片OCR”的组合。用PDFtoImage把PDF每一页渲染成高分辨率图片然后循环调用Tesseract识别。下面是一个能直接运行的完整代码using PDFtoImage; using Tesseract; using var engine new TesseractEngine(./tessdata, chi_simeng, EngineMode.LstmOnly); // PDFtoImage 4.x 的调用方式 var pages Conversion.ToImages(D:\samples\scanned.pdf, dpi: 300); int pageIndex 0; foreach (var pageImage in pages) { using var memoryStream new MemoryStream(); pageImage.SaveAsPng(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); using var pix Pix.LoadFromMemory(memoryStream.ToArray()); using var page engine.Process(pix); string pageText page.GetText(); Console.WriteLine($ 第 {pageIndex 1} 页 ); Console.WriteLine(pageText); File.WriteAllText($page_{pageIndex 1}.txt, pageText); pageIndex; }看着简单但有三个细节必须处理到位。第一dpi参数设成300。渲染分辨率过低OCR识别率直线下降过高又会导致图片内存暴涨。我做过对比300 DPI是一个性价比很高的临界点A4纸页面渲染出来大约是2480x3508像素既能识别出小号字体内存占用也还在可控范围。第二PDFtoImage返回的页面对象要记得释放。它内部用的是非托管资源如果不小心处理批量处理几百页的PDF很容易把内存拖垮。上面代码里我用using和局部变量约束生命周期是个基本习惯。第三不要在一个循环里反复创建TesseractEngine。引擎初始化时要加载语言模型非常耗时一次初始化可能需要几百毫秒。正确做法是程序启动时创建一次engine整个处理过程复用它最后再释放。这一点在并发和批量场景下尤其重要。3.4 指定矩形区域识别和文本裁剪有时候你不需要识别整张图片而只需要提取图片里某一块区域的内容。比如上位机里的屏幕截图有用的数字只占屏幕一角或者扫描表单里客户只需要填写的备注区。这种情况下比较聪明的做法是先裁剪区域再喂给Tesseract识别速度和准确率都能提升。用ImageSharp可以做区域裁剪代码也不复杂using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using var image Image.Load(D:\samples\screen.png); var cropRect new Rectangle(100, 200, 400, 120); // X, Y, Width, Height image.Mutate(x x.Crop(cropRect)); using var ms new MemoryStream(); image.SaveAsPng(ms); ms.Seek(0, SeekOrigin.Begin); using var pix Pix.LoadFromMemory(ms.ToArray()); using var page engine.Process(pix, PageSegMode.SingleLine); Console.WriteLine(page.GetText());这里有两个好处一是排除了区域外文字的干扰二是页面分割模式可以换成SingleLine或者SingleWord这样识别单行数字、设备编码时准确率会比整页识别高很多。关于矩形区域的坐标怎么获取简单粗暴的办法是用画图工具打开图片鼠标悬停就能看到坐标工业场景里也可以做成可视化配置界面让操作员手工框选。3.5 结果保存和结构化处理提取出的裸文本通常不是最终产物。真实业务里你往往还需要把文本落库、转JSON、或者根据规则提取关键字段。我常用的处理思路有三个整页文本入库直接把每页文本写入数据库或Elasticsearch用来做全文检索。去掉冗余空白字符用正则把连续换行、空格压缩掉避免检索时被空白干扰。关键字段抽取针对身份证、发票这类固定版式可以先用GetSegmentedRegions拿到字段坐标再结合正则从文本中抽取需要的号码。举个例子识别一张发票原始文本里有“发票号码12345678”这样的串。你不能直接整段入库就算完最好再用Regex.Match(text, 发票号码[:]\s*(\d))把号码抽出来单独存成一个字段。这样业务方查询和统计都能直接用不用每次翻全文。结构化处理虽然不是OCR的核心但往往决定了这套工具最终能不能真正落地到业务流程里。4. 实战中的坑中文识别、DPI设置、内存和并发问题4.1 中文识别结果全变成乱码或者方块这个问题出现频率最高。如果Tesseract报错“Failed loading language chi_sim”那基本是语言包没找到检查tessdata目录是否在程序运行目录下或者是否设置了TESSDATA_PREFIX环境变量。如果没报错但输出乱码则要看输入图片本身。拍照件里的倾斜、透视、阴影都会让识别率明显下降建议先做旋转矫正、裁边、提亮预处理。还有一个很隐蔽的坑页面分割模式会影响中文识别。Tesseract里tessedit_pageseg_mode默认是PAGING_MODE_AUTO适合普通文档但如果识别的是单行数字、表格单元格或者固定版式的卡片可能需要手动指定模式。比如识别仪表数字可以用PageSegMode.SingleLine识别身份证这种分区明确的可以用SingleBlock。using var page engine.Process(pix, PageSegMode.SingleBlock);建议你在做一个新类型图片时花个15分钟把几种常用模式都跑一遍记录下哪种模式准确率最高。这个经验积累下来后面每个新需求都能少折腾。我把常用场景和推荐模式整理成了下表场景推荐PageSegMode整页文档Auto单行数字/条码SingleLine分段明显的表单SingleBlock只识别一个词SingleWord带表格的发票Auto4.2 扫描件清晰度差300 DPI也没救怎么办低清扫描件是很多传统行业的老大难。扫描仪质量一般、纸张泛黄、字迹浅这种图直接喂给OCR效果肯定差。我处理这类问题的三板斧是灰度化、二值化、缩放。灰度化就是把彩色图片转成黑白灰避免颜色干扰二值化是把像素点分成黑和白两种有利于OCR模型定位笔画边缘缩放则是把过小的文字放大。用ImageSharp可以非常方便地做这些操作using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using var image Image.Load(D:\samples\dark_scan.png); image.Mutate(x x.Grayscale().BinaryThreshold(0.55f).Resize(1.6f)); image.SaveAsPng(D:\samples\preprocessed.png);这里BinaryThreshold(0.55f)就是把亮度阈值设为55%高于阈值的像素转白、低于的转黑。阈值怎么定需要看实际材料我的经验是先按0.5-0.6试扫描件泛黄严重的可以调到0.45左右。颜色模型转换、缩放比例这些参数最好做成配置文件让现场人员微调别写死在代码里。4.3 PDF渲染出的图片是空白或者全黑用PDFtoImage渲染某些扫描PDF时偶尔会碰到渲染出来的图片一片黑或一片白。我遇到过两种典型情况。一种是PDF页面的旋转标记没被正确处理。扫描仪生成的PDF可能带旋转元数据渲染库执行时如果没应用旋转图片内容就偏了看起来像异常。排查方法是把渲染出的图片用看图工具打开如果能看到内容但方向不对考虑在OCR之前调用图片旋转。另一种是颜色空间问题。个别扫描仪的PDF用的是特殊色彩配置渲染库默认输出RGB时内容丢失。这种情况下可以试试调整渲染参数或者在PDFtoImage的Issue区找一下对应版本的兼容性讨论。如果只是极个别文件异常稳妥办法是换一个渲染库做交叉验证比如Docnet.Core再跑一遍看是否同样白屏。白屏问题多数不是OCR的问题而是PDF解析层的问题记住这一点能少绕很多弯。4.4 批量处理时内存飙升线程卡死真实项目里很少只处理单个文件往往是拖入一个文件夹几百个PDF。如果不做并发控制直接把所有PDF页面加载到内存程序很快就OOM。我踩过一次很深的坑一个同事写的批量工具同时并行处理20个PDF每个PDF约100页跑了不到两分钟整个服务内存涨到3GB以上最后直接被杀掉。正确的做法是限制并发数。用SemaphoreSlim控制最多同时处理2到4个文档并且每个文档内部逐页处理、逐页释放。还可以给每页识别加一个超时机制因为OCR耗时长的多半是异常文件超时后记录下来继续处理而不是让整批任务卡死。using var semaphore new SemaphoreSlim(2); async Task ProcessDocumentAsync(string pdfPath) { await semaphore.WaitAsync(); try { // 渲染OCR处理 } finally { semaphore.Release(); } }这段代码看起来简单但它能保证任何时刻最多只有2个文档在跑OCR内存使用率稳定任务也不会因为一个坏文件拖垮整体。5. 生产环境里的优化预处理、后处理与批量任务兜底5.1 图像预处理的优先级和顺序很多教程把预处理写得很玄乎我个人用下来最重要的三件事就是尺寸、对比度、边框干扰。尺寸决定文字是否足够大对比度决定笔画是否清晰边框干扰则是扫描件中常见的页边缘黑边、装订孔、污渍。处理顺序我习惯是先缩放到合适尺寸再灰度化去色再做二值化或对比度增强最后用矩形裁剪去掉边缘黑边。顺序不能乱比如先二值化再做尺寸缩放会把噪声连带放大效果反而差。当然OCR识别这种环节预处理不是越多越好。图像滤镜加得太多有时候会把真实笔画误伤。我的判断标准是处理完的图片用肉眼看是否比原图更“干净清晰”。如果你自己都觉得处理完更糊那这一步就是在帮倒忙。5.2 文本后处理和版面重排OCR识别出来的文本是“流式”的它并不理解原稿的排版结构。表格行、两栏排版、首行缩进都会被打乱。如果你只是做全文检索问题不大但如果要还原版面就得花更多功夫。我常用的一个简单方案是按“行”维度识别而不是一次性给整页文本。用GetSegmentedRegions拿到每个词或每一行的坐标然后根据Y坐标排序、X坐标对齐重新拼接成表格结构。这个方法在识别表单和发票时很管用但代码量会多一些。如果没有版面还原需求建议直接拿整页文本不做过度处理效率和效果都更可控。后处理还有一个关键点是字符纠错。OCR输出的中文经常会出现“0/O、1/l/I、8/B”混淆尤其是在数字、字母混合的场景。你可以针对业务字段做白名单校验。比如识别身份证号先校验18位且最后一位符合校验规则不满足就标记为“需人工复核”不要让脏数据直接进库。5.3 批量任务的异常兜底和重试机制批量处理文件不能太天真地认为每个文件都能成功。我见过太多次因为1个损坏PDF导致整批任务中断的情况。所以我的批量工具里一定会有三个机制失败重试、失败记录、跳过继续。失败重试一般是识别超时或渲染异常才触发重试1到2次即可太多会拖慢整体进度。失败记录是必须的把出问题的文件路径、异常类型、当时处理到哪一页统一写到日志和失败清单里。跳过继续保证主流程不受单个文件影响。try { await ProcessDocumentAsync(pdfPath); } catch (Exception ex) { File.AppendAllText(failed.log, ${pdfPath}: {ex.Message}{Environment.NewLine}); continue; }这套兜底逻辑看起来简单但在实际交付中非常加分。因为它意味着程序可以无人值守地跑一整夜第二天早上只需要看失败清单而不是去查一个已经崩掉的服务。最后分享一点我在实际项目里的体会图片和扫描PDF的文本识别工具链本身并不复杂真正拉开差距的是对图片质量的判断、对字段结构化处理的深度以及对异常文件的兜底能力。我一开始做OCR工具时总觉得效果不好是库不够强后来发现是把过多精力放在调参上反而忽略了预处理和后续业务逻辑的重要性。如果你也是刚入门C# OCR这条路建议你先拿10张真实业务图片跑通整个流程再把识别准确率、失败率记录下来一步步优化。工具能自动做的程度永远有限但你把它放在正确的流程里它就能稳定帮你省下大量人工录入时间。
返回列表