简介这份资源面向具备一定C#基础的开发者聚焦离线式OCR文字识别场景解决不依赖在线服务、在本地完成图片文字提取的需求。内容围绕C#调用本地OCR库展开涉及图像预处理、识别参数设置、结果后处理与文本导出等环节可应用于发票解析、纸质资料数字化等文档自动化方向。压缩包共62个文件约4.31MB以dll动态库、xml配置、cs源码、nupkg包及exe可执行文件为主另含sln解决方案、csproj工程文件与少量资源文件构成可直接运行的完整项目结构。目前已有4536人学习下载。通过研读源码读者可掌握OCR引擎初始化、图片加载、识别执行与结果处理的完整流程理解灰度化、二值化等预处理手段对识别率的影响并借鉴工程组织方式提升C#编程与OCR技术实践能力。1. 离线 OCR 在 C# 里到底怎么落地一份能直接编译的源码包很多做 C# 上位机、桌面工具或者内部管理系统的朋友都遇到过同一个需求客户给了一张截图、一份扫描件或者现场拍的照片要求把里面的文字抠出来而且明确说不能联网、不能把图片传到第三方接口。这时候在线 OCR 的 API 就全部出局了只能走离线方案。这份源码包解决的正是这个场景——用 C# 在本地完成图片文字识别不依赖网络请求不调用云端服务拿到就能编译运行。它适合三类人一是做 WinForm/WPF 上位机、需要集成识别功能的开发者二是手里有大量图片要批量提取文字、又不想写 Python 脚本的 .NET 工程师三是刚接触 OCR、想找一个能跑通的 C# 最小可复现工程来改的新手。整份资源的核心价值在于「离线」和「可改」——引擎跑在本地源码结构清晰识别流程从图片加载到文本输出是一条完整的链路不是那种只给一个 DLL 让你猜怎么用的黑匣子。2. 离线 OCR 引擎选型为什么是 Tesseract 而不是别的2.1 离线识别的三条技术路线对比在 C# 生态里做离线 OCR能走的路其实不多常见的就是三条Tesseract、PaddleOCR 的 C# 封装、以及 Windows 自带的系统 OCR API。这三条路各有各的脾气选错了后面全是返工。Tesseract 是最老牌的开源 OCR 引擎最早由 HP 实验室开发后来交给 Google 维护现在由社区持续更新。它的最大优势是纯本地、跨平台、有成熟的 .NET 封装库NuGet 上直接装就能用。缺点是中文识别率在复杂背景下不如深度学习模型需要配合图像预处理才能达到可用水平。PaddleOCR 的识别精度确实高尤其是中文场景但它的 C# 封装要么依赖 PaddleInference 的原生库要么走 ONNX Runtime 自己搭推理管线部署体积大、依赖多对只想在 WinForm 里加个识别按钮的人来说太重了。Windows 系统 OCR API 是 Win10 之后内置的能力通过 Windows.Media.Ocr 命名空间调用优点是零依赖、速度快缺点是只支持系统已安装的语言包而且 API 是 UWP 风格的在传统 WinForm 里调用需要绕一层灵活性差。这份源码选的是 Tesseract 路线。原因很实际它的 NuGet 包成熟稳定中文语言包chi_sim可以直接下载放到本地目录整个方案不依赖任何外部服务编译出来的 exe 拷到别的机器上也能跑。对于「离线式」这个核心诉求来说Tesseract 是投入产出比最均衡的选择。2.2 Tesseract 在 .NET 里的两种调用方式Tesseract 在 C# 里主要有两个 NuGet 包可以用一个是Tesseract这是社区维护的 .NET 封装API 友好直接new TesseractEngine()就能用另一个是TesseractOCR封装层次不同用法略有差异。这份源码用的是前者也是目前 GitHub 上 star 数最多、文档最全的那个。安装方式很简单在 Visual Studio 的 Package Manager Console 里执行Install-Package Tesseract -Version 5.2.0或者用 .NET CLIdotnet add package Tesseract --version 5.2.0装完之后项目里会多出 Tesseract 的原生 DLL 依赖。这里有个关键点Tesseract 的 .NET 封装本身是托管代码但底层引擎是 C 写的所以 NuGet 包会自动带上 x86 和 x64 两个版本的原生库。如果你的项目是 AnyCPU 编译运行时会根据系统架构自动选但如果你显式指定了 x86 或 x64就要确保对应的原生 DLL 被正确拷贝到输出目录。2.3 语言包chi_sim 和 eng 怎么放Tesseract 的识别能力靠语言包traineddata 文件驱动。英文包eng.traineddata大概 4MB 左右中文简体包chi_sim.traineddata大概 40MB 上下。这些文件不能随便扔必须放在一个叫tessdata的目录里然后在代码里通过TesseractEngine的构造函数指定这个目录的路径。常见做法是在项目根目录下建一个tessdata文件夹把下载好的.traineddata文件放进去然后在 csproj 里设置这些文件「始终复制到输出目录」。这样编译出来的程序tessdata目录会和 exe 在一起运行时直接读相对路径就行。ItemGroup None Updatetessdata\eng.traineddata CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /None None Updatetessdata\chi_sim.traineddata CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /None /ItemGroup这段 csproj 配置的意思是把tessdata目录下的语言包文件标记为「如果比输出目录的新就复制过去」。PreserveNewest比Always更合理避免每次编译都无脑拷贝大文件。如果你只识别英文可以只放eng.traineddata程序体积能小很多。注意语言包的版本要和 Tesseract 引擎版本匹配。Tesseract 5.x 用的是 LSTM 模型格式的语言包如果你从旧教程里下载了 3.x 时代的语言包加载时会报错或者识别结果异常。3. 从图片到文本识别流程的代码拆解3.1 加载图片与创建引擎实例整个识别流程的第一步是把图片读进来然后初始化 Tesseract 引擎。这里有个容易翻车的地方Tesseract 引擎的创建是有开销的如果你在循环里每次都new一个批量处理几十张图就会明显变慢。正确的做法是把引擎实例复用或者至少在一个批次内复用。using Tesseract; using System.Drawing; public class OcrService { private readonly string _tessDataPath; public OcrService(string tessDataPath) { // tessDataPath 指向包含 .traineddata 文件的目录 _tessDataPath tessDataPath; } public string Recognize(string imagePath, string language chi_simeng) { // 用 Pix.LoadFromFile 加载图片支持常见格式 using (var engine new TesseractEngine(_tessDataPath, language, EngineMode.Default)) using (var img Pix.LoadFromFile(imagePath)) using (var page engine.Process(img)) { // GetText 返回识别出的纯文本 return page.GetText(); } } }这段代码里几个参数值得说清楚。_tessDataPath是语言包所在目录的绝对或相对路径如果路径不对构造函数会直接抛异常。language参数用连接多个语言包chi_simeng表示同时加载中文简体和英文识别时会自动判断字符归属。EngineMode.Default让 Tesseract 自己选模式通常就是 LSTM 加传统引擎的组合。Pix.LoadFromFile是 Tesseract 封装里加载图片的方法它内部会把图片转成引擎能处理的格式。如果你是从内存流或者 Bitmap 对象来的图片可以用PixConverter.ToPix(bitmap)转换这个在源码里也有体现。3.2 图像预处理识别率的分水岭直接拿一张手机拍的照片扔给 Tesseract识别结果大概率惨不忍睹。Tesseract 对图像质量很敏感尤其是中文笔画密集背景稍微复杂一点就糊成一片。所以预处理不是可选项是必选项。源码里做的预处理主要有三步灰度化、二值化、去噪。灰度化是把彩色图转成灰度图减少颜色干扰二值化是按阈值把灰度图变成黑白两色让文字和背景彻底分开去噪是去掉一些孤立的像素点避免被误识别成标点。using System.Drawing; using System.Drawing.Imaging; public Bitmap Preprocess(Bitmap source) { // 转灰度 var gray new Bitmap(source.Width, source.Height); using (var g Graphics.FromImage(gray)) { var matrix new ColorMatrix(new float[][] { new float[] {0.299f, 0.299f, 0.299f, 0, 0}, new float[] {0.587f, 0.587f, 0.587f, 0, 0}, new float[] {0.114f, 0.114f, 0.114f, 0, 0}, new float[] {0, 0, 0, 1, 0}, new float[] {0, 0, 0, 0, 1} }); var attrs new ImageAttributes(); attrs.SetColorMatrix(matrix); g.DrawImage(source, new Rectangle(0, 0, source.Width, source.Height), 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attrs); } // 简单阈值二值化阈值 128 可根据实际图片调整 var binary new Bitmap(gray.Width, gray.Height); for (int y 0; y gray.Height; y) { for (int x 0; x gray.Width; x) { var c gray.GetPixel(x, y); int avg (c.R c.G c.B) / 3; binary.SetPixel(x, y, avg 128 ? Color.White : Color.Black); } } return binary; }灰度化用的是标准的亮度加权公式0.299R 0.587G 0.114B这是人眼对绿色最敏感、对蓝色最不敏感的近似。二值化的阈值 128 是个经验值实际项目里如果图片偏暗或者偏亮这个值要调。源码里留了注释说明可以改成自适应阈值但为了保持代码简洁默认用的是固定阈值。GetPixel和SetPixel在像素级操作里性能很差如果图片尺寸大这段代码会明显拖慢速度。生产环境常见做法是用LockBits直接操作内存或者用 OpenCVSharp 的Threshold函数。源码为了降低阅读门槛用的是直观的逐像素写法你在实际项目里可以按需替换。3.3 识别结果的后处理与输出Tesseract 返回的文本里经常夹杂一些无意义的换行和空格尤其是中文和英文混排的时候。后处理的目标是把这些噪音去掉让输出更接近人眼看到的排版。public string CleanResult(string raw) { // 去掉首尾空白 var text raw.Trim(); // 把连续多个换行合并成一个 text System.Text.RegularExpressions.Regex.Replace(text, \n{2,}, \n); // 去掉行首行尾多余空格 var lines text.Split(\n) .Select(l l.Trim()) .Where(l !string.IsNullOrWhiteSpace(l)); return string.Join(\n, lines); }这段后处理逻辑不复杂但很实用。\n{2,}的正则把两个以上的连续换行压成一个避免输出里出现大片空白。按行拆分后再过滤空行能去掉 Tesseract 在段落之间插入的冗余换行。如果你的场景对格式要求更高比如要保留表格结构那后处理就要更复杂可能需要根据坐标信息重建排版这超出了这份源码的范围但思路是通的。4. 避坑与排查离线 OCR 最常见的五个翻车点4.1 报错「Failed to find library loader」或找不到 leptonica DLL现象是程序一运行就抛异常提示找不到leptonica-1.82.0.dll或者类似的本地库文件。原因通常是 NuGet 包的原生依赖没有被正确复制到输出目录或者项目的目标平台和原生库架构不匹配。解决方法是检查bin目录下有没有x86和x64两个文件夹里面应该有对应的 DLL。如果用的是 AnyCPU确认这两个文件夹都在如果指定了 x64确认x64文件夹里的 DLL 存在。另一个常见原因是 Visual Studio 的「首选 32 位」选项被勾选了导致 AnyCPU 实际跑在 32 位模式下但原生库只有 64 位的。4.2 识别结果全是乱码或者空字符串现象是page.GetText()返回的内容完全不可读或者干脆是空的。原因有几个一是语言包路径不对引擎加载了错误的 traineddata二是图片预处理过度二值化阈值把文字也一起干掉了三是图片本身分辨率太低文字像素太少引擎无法识别。排查顺序是先确认tessdata目录路径正确再打印一下预处理后的图片看看文字是否还清晰最后检查原图的分辨率一般建议文字高度不低于 20 像素。4.3 中文识别率明显低于英文这是 Tesseract 的已知短板。中文笔画复杂字形相近的字多Tesseract 的 LSTM 模型在中文上的表现不如专门的深度学习模型。改善方法有几个一是确保用的是chi_sim而不是chi_tra除非你确实要识别繁体二是提高图片分辨率中文识别对像素密度更敏感三是限制识别区域如果图片里只有一小块是文字先用裁剪把无关区域去掉四是调整PageSegMode比如用PageSegMode.SingleBlock告诉引擎这是单块文本减少版面分析的干扰。4.4 批量处理时内存持续增长现象是循环识别几十张图之后程序占用的内存越来越大最后可能 OOM。原因是TesseractEngine、Pix、Page这些对象都实现了IDisposable如果没有用using包起来原生资源不会被及时释放。解决方法是确保每个实现了IDisposable的对象都在using块里创建和使用。另外TesseractEngine本身可以复用不需要每次识别都新建复用时注意线程安全——Tesseract 引擎不是线程安全的多线程场景下每个线程要有自己的引擎实例。4.5 编译时报「未能加载文件或程序集」或版本冲突现象是编译能过但运行时报程序集加载失败或者提示某个 DLL 的版本和引用不一致。原因通常是项目里同时引用了多个版本的 Tesseract 相关包或者间接依赖了不同版本的System.Drawing.Common。解决方法是清理 NuGet 缓存检查packages.config或 csproj 里的包版本是否统一。在 .NET Core/.NET 5 项目里System.Drawing.Common在非 Windows 平台上有兼容性问题如果目标是跨平台建议用ImageSharp或SkiaSharp替代System.Drawing做图像预处理。5. 进阶技巧让离线 OCR 更稳的几个实操习惯5.1 用 PageSegMode 控制版面分析Tesseract 默认会做版面分析猜测图片里文字是怎么排布的。这个功能在文档扫描件上表现不错但在截图或者界面截图上经常帮倒忙。PageSegMode枚举提供了十几种模式常用的有这几个模式适用场景说明Auto通用默认模式自动分析版面SingleBlock单块文本图片里只有一段连续文字不做复杂版面分析SingleLine单行文本只识别一行适合验证码或标签SingleWord单个词只识别一个词速度最快SparseText稀疏文本文字分散在图片各处没有明显段落结构在代码里设置的方式是在engine.Process之前指定using (var page engine.Process(img, PageSegMode.SingleBlock)) { return page.GetText(); }如果你发现识别结果里多了很多莫名其妙的换行或者文字顺序乱了先试试换成SingleBlock。如果是识别验证码那种单行短文本SingleLine能明显提速。5.2 用置信度过滤低质量结果Tesseract 的Page对象提供了GetMeanConfidence()方法返回一个 0 到 1 之间的浮点数表示引擎对这次识别的整体置信度。这个值在批量处理时很有用可以帮你快速筛出那些识别质量差的图片决定是否需要人工复核。using (var page engine.Process(img)) { float confidence page.GetMeanConfidence(); string text page.GetText(); if (confidence 0.6f) { // 置信度太低标记为需要人工检查 Console.WriteLine($低置信度 {confidence:F2}: {text}); } return text; }0.6 这个阈值不是固定的根据你的场景调。对准确性要求高的场景可以提到 0.75对召回率要求高的可以降到 0.5。源码里没有默认加这个过滤因为不同项目的容忍度不一样但这是个很实用的扩展点。5.3 我踩过的一个坑路径里的中文和空格最后说一个血泪经验。Tesseract 的原生库对路径里的非 ASCII 字符处理得不太好如果你的tessdata目录路径里包含中文或者空格有时候会加载失败报的错还特别模糊就一句「初始化失败」。我当初把项目放在D:\我的项目\OCR工具\下面折腾了半天才发现是路径问题。后来养成的习惯是tessdata目录永远放在纯英文、无空格的路径下如果实在要用中文路径就在代码里先转成短路径或者用Path.GetFullPath处理一下。从那以后我每次新建 OCR 项目第一件事就是把语言包目录设成C:\tessdata或者项目输出目录下的tessdata再也没在这上面翻过车。希望这份源码和这些经验能帮你少走点弯路直接跑通离线识别这条链路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?