简介OCR.rar是一个基于C#的百度OCR图像识别示例项目面向希望在.NET应用中快速接入文字识别能力的开发者适合初学AI接口调用或需要做文本提取、票据识别的WinForms场景。项目演示了从申请百度AI密钥、构造HTTP请求、上传图片到解析JSON响应并提取文字的完整链路代码中通过Ocr.cs封装核心识别逻辑、Form1.cs提供界面交互配合Program.cs入口即可运行。压缩包共29个文件以cs源文件、exe可执行程序、dll依赖库为主另有pdb调试符号、resx/resources资源文件、sln工程及settings配置等整体仅261KB结构精简适合直接打开工程对照学习。已有224人学习下载。通过学习这份示例可掌握C#调用第三方AI服务的通用模式理解图像Base64编码、JSON数据解析和API密钥管理细节还能将识别结果导出为文本以对接后续业务处理是一份轻量实用的入门参考。1. 解压 OCR.rar 只看到 C# 源码百度 OCR 接入先从一条 Token 链路说起拿到 OCR.rar 却跑不起来通常不是代码编译不过而是这个 C# 程序里的百度 OCR 调用缺了百度AI开放平台的访问密钥。压缩包解出来是一套 C# 工程注释里写着“百度OCR”“百度图像识别”但 API Key、Secret Key、Access Token 全是空的自然一运行就报错。这套程序要做的事很简单把本地图片交给百度 AI 的 OCR 接口拿回识别文本。适合谁WinForm 上位机开发者、做桌面小工具的工程师、以及在产线系统里想快速加一道文字识别能力的团队。今天这篇就把这条链路完整拆开先理清百度AI平台上的 OCR 能力到底是什么再写出最小可编译的 C# 调用代码最后把五个高频坑一个个排掉。2. 为什么选百度 OCR在线识别、图像识别能力和离线方案的取舍2.1 百度AI开放平台里的“百度图像识别”不是单接口百度AI开放平台上OCR 归属于“图像识别”这个大分类但点进去能看到一串能力通用文字识别、通用文字识别高精度版、带位置信息的识别、身份证识别、发票识别、驾驶证识别、银行卡识别、车牌识别甚至还有表格文字识别。每个能力对应不同的 REST 接口路径价格和额度也不一样。C# 程序里最常用到的是通用文字识别 general_basic 和通用文字识别高精度版 accurate_basic。两者的差异主要体现在识别率和对图片清晰度的容忍度上高精度版对模糊、倾斜、低对比度的图更友好但免费额度更低QPS每秒查询数限制也更紧。做上位机工具时我一般先用通用版跑通流程觉得识别率不够再切高精度只改接口路径和参数不用动整体代码结构。还有一个很容易混淆的点百度AI平台里的“图像识别”除了 OCR还有图像分类、物体检测、图像搜索这些能力。但标题里提到的“百度图像识别”在 C# 开发者的需求里大多数指的就是 OCR 文字识别尤其是印刷体中文识别。百度 OCR 对中文印刷体的识别效果比很多本地开源方案的默认模型好直接调用不需要训练这是选它的第一理由。2.2 和 Tesseract、PaddleOCR、anytxt OCR 比在线方案赢在哪做桌面工具前我先把几个常见方案摆在一起比过一轮方案中文识别效果部署成本网络依赖适合场景百度 OCR在线接口好印刷体中文尤其强低只写 HTTP 请求需要在线访问WinForm/上位机快速接入Tesseract OCR一般需要下载或训练中文语言包中本地 C 库C# 需封装无简单扫描件、纯英文PaddleOCR很好但模型依赖较重高需要 Python 环境或推理引擎无批量离线识别、服务端部署anytxt OCR桌面工具属性适合个人使用中一般内置离线模型个人文档扫描不适合程序集成C# 项目接 Tesseract 最难受的是要处理 native 依赖和字符集问题接 PaddleOCR 需要搭 Python 或 ONNX 推理环境部署组会有意见。而百度 OCR 的接入方式就是 HTTP 请求加上 JSON 解析C# 原生就能搞定。代价是图片内容会经过在线服务所以涉及隐私或内网隔离数据的场景才需要转向离线方案一般工具型程序在线接口是更快的路。2.3 创建百度AI应用四个步骤、三个参数、一个 30 天令牌在百度智能云控制台开通文字识别服务后需要创建一个应用来拿凭证。这个流程很模板化但每个字段都要对上步骤操作要拿到的信息1注册并登录百度智能云完成实名认证账号2控制台搜索“文字识别”或“OCR”进入对应产品页服务列表3点击“创建应用”名称随意按默认勾选接口权限应用4创建完成后在应用列表里查看API Key、Secret Key三个参数里API Key 和 Secret Key 是应用身份的标识它们不能直接用来调用 OCR 接口。OCR 接口需要一个 Access Token而 Access Token 必须拿着 API Key 和 Secret Key 去百度AI的鉴权接口换有效期默认 30 天。过期后要重新换取不是永久有效这也是很多 C# 程序跑了一段时间突然报错的原因。Secret Key 一定不要硬编码在 release 版程序里至少放到配置文件并加上访问权限控制。2.4 先用 10 行 C# 换到 access_token验证 Key 可用创建完应用先不要急着写识别逻辑第一步是确认 Key 能不能换到 Access Token。用下面这段代码拉起一个最小的换 token 请求using System.Net.Http; string apiKey 你的 API Key; string secretKey 你的 Secret Key; string url https://aip.baidubce.com/oauth/2.0/token ?grant_typeclient_credentials client_id apiKey client_secret secretKey; using (HttpClient client new HttpClient()) { string json await client.GetStringAsync(url); Console.WriteLine(json); }这段代码的逻辑很直白grant_type 固定为 client_credentialsclient_id 填 API Keyclient_secret 填 Secret Key。如果返回的 JSON 里有 access_token 字段说明凭证可用如果返回 error 字段先检查 Key 是不是填反了——这是我见过最多的翻车原因。Access Token 在 30 天内有效建议在 C# 程序里用一个静态字段缓存而不是每张图片都重新去鉴权一次毕竟换取 token 本身也有网络开销和频率限制。注意拿到 access_token 后不要急着写进代码里测试先看看返回里的 expires_in 字段确认过期时间再决定缓存策略。3. C# 调用百度 OCR 识别一张图最小可编译代码与参数说明3.1 接口路径与请求参数一次通用文字识别请求由什么构成通用文字识别接口的 REST 路径是 /rest/2.0/ocr/v1/general_basic请求方式为 POST需要把 access_token 拼在 URL 的查询参数里。请求体用 application/x-www-form-urlencoded 格式不需要搞成 JSON 请求体这是很多新手第一次就踩进去的坑。请求体的关键参数整理如下参数必填说明image是图片的 Base64 编码字符串并且要做 URL 编码language_type否默认 CHN_ENG表示中英文混合识别detect_direction否true 时自动检测图像旋转方向截图类图像建议打开paragraph否true 时返回段落信息和起始位置probability否true 时返回每行文字的平均置信度image 参数的坑最多。直接读取图片字节转 Base64 后不能直接放到请求体里必须先 UrlEncode否则遇到 Base64 字符串里的 、/、 会被服务端解析成错误数据。另外服务端对图片大小有限制Base64 后的字符串不能超过 4M且图片最短边建议不小于 15 像素否则识别结果基本没法用。3.2 完整控制台代码读图、识别、打印文字下面给一份完整可编译的 .NET 6 控制台程序覆盖换 token、调用 OCR、解析结果三段逻辑using System.Net.Http; using System.Text; using System.Text.Json; string apiKey 你的 API Key; string secretKey 你的 Secret Key; string imagePath args.Length 0 ? args[0] : test.png; string token await GetAccessTokenAsync(apiKey, secretKey); Console.WriteLine($Token: {token.Substring(0, 12)}...); string json await RecognizeAsync(token, imagePath); PrintWords(json); static async Taskstring GetAccessTokenAsync(string apiKey, string secretKey) { string url https://aip.baidubce.com/oauth/2.0/token $?grant_typeclient_credentialsclient_id{apiKey}client_secret{secretKey}; using (HttpClient client new HttpClient()) { string response await client.GetStringAsync(url); using JsonDocument doc JsonDocument.Parse(response); return doc.RootElement.GetProperty(access_token).GetString(); } } static async Taskstring RecognizeAsync(string token, string imagePath) { byte[] imageBytes File.ReadAllBytes(imagePath); string base64 Convert.ToBase64String(imageBytes); string body image Uri.EscapeDataString(base64) language_typeCHN_ENG detect_directiontrue probabilitytrue; string url https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token token; using (HttpClient client new HttpClient()) { using HttpContent content new StringContent(body, Encoding.UTF8, application/x-www-form-urlencoded); HttpResponseMessage resp await client.PostAsync(url, content); return await resp.Content.ReadAsStringAsync(); } } static void PrintWords(string json) { using JsonDocument doc JsonDocument.Parse(json); JsonElement root doc.RootElement; if (root.TryGetProperty(words_result, out JsonElement words)) { foreach (JsonElement item in words.EnumerateArray()) { string text item.GetProperty(words).GetString(); double prob 0; if (item.TryGetProperty(probability, out JsonElement p) p.TryGetProperty(average, out JsonElement avg)) { prob avg.GetDouble(); } Console.WriteLine(${text} [{prob:P1}]); } } else { Console.WriteLine($OCR 调用失败: {root.GetRawText()}); } }这段代码把整条调用链分成了三个职责清晰的函数。GetAccessTokenAsync 负责换 token返回的 JSON 里直接取 access_token 字符串RecognizeAsync 把图片读成字节、转 Base64、拼请求体然后 POST 到 OCR 接口PrintWords 负责解析返回的 words_result 数组。代码里用 token.Substring(0, 12) 只打印 token 前 12 位能验证 token 已取到又不会把完整凭证刷到控制台。解析部分需要留意 JsonDocument 的嵌套结构。words_result 是数组数组里每个元素包含 words 字符串字段如果打开了 probability还会有一个 probability 对象里面是 average 字段。用 TryGetProperty 做容错比直接 GetProperty 更抗接口返回变化。如果返回 JSON 里没有 words_result说明是错误响应此时应该把整个 root 打印出来看 error_code 和 error_msg。3.3 在 WinForm 上位机里调用异步不卡界面结果放进 List控制台验证通过后把这段逻辑挪进 WinForm 就是常规操作了。但有一个原则必须守住点击“识别”按钮后绝对不能在 UI 线程里同步调用 HTTP 接口。一次 OCR 请求在网络状况一般时可能耗时 2 到 5 秒同步调用会让整个窗体像死掉一样。正确的做法是把识别方法做成 async并且用 await 等待结果。private async void btnRecognize_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(txtToken.Text)) return; using OpenFileDialog dlg new OpenFileDialog(); if (dlg.ShowDialog() ! DialogResult.OK) return; pictureBox.Image Image.FromFile(dlg.FileName); btnRecognize.Enabled false; try { string json await RecognizeAsync(txtToken.Text, dlg.FileName); using JsonDocument doc JsonDocument.Parse(json); Liststring lines new Liststring(); if (doc.RootElement.TryGetProperty(words_result, out JsonElement words)) { foreach (JsonElement item in words.EnumerateArray()) { lines.Add(item.GetProperty(words).GetString()); } txtResult.Lines lines.ToArray(); } } finally { btnRecognize.Enabled true; } }这里的识别结果我存进了 List 而不是 string 数组。原因很简单OCR 返回的行数是未知的可能 3 行也可能 30 行List 会自动扩容不需要预先猜长度。如果固定只识别一个 3 行表格区域用固定数组也可以但现实里的图片很少那么规整。C# 里数组和集合的核心区别就在这里数组定长、内存连续适合数量固定且要频繁按下标访问的场景集合是可变长度适合数据量动态变化的场景。上位机里收 OCR 结果List 最省心。4. 百度 OCR 接入排查避坑110 错误到“远程主机强迫关闭”的五个现场4.1 返回 110access_token 失效Key 配反是头号原因现象程序刚跑通时一切正常第二天再运行就返回 JSON 里带 error_code 110提示 Access token invalid or no longer valid。原因先去排查是不是 Key 填反了。API Key 和 Secret Key 外观很像都是 32 位左右的十六进制串复制的时候但凡串位token 接口虽然也会返回 access_token但用这个 token 调用 OCR 时就会报 110。另一个高频原因是 Access Token 过期。默认 30 天有效期写死在配置文件里的 token 到期后自然失效。解决用第 2.4 节的换 token 代码重新跑一次看返回是否正常然后把程序改成“启动时检查 token 为空就重新换取并缓存到静态字段”。不要在每次识别时都重新换 token那样一方面增加网络耗时另一方面频繁换取可能触发鉴权接口频率限制。我做上位机时习惯在程序启动时拉一次 token识别过程中如果捕获到 110 再重新换一次并重试当前图片。4.2 “远程主机强迫关闭了一个连接”连接池里的坏连接没有后悔药现象单个识别没毛病一到批量识别第 10 到 20 张图片时C# 抛 HttpRequestException内容带“无法将数据写入传输连接: 远程主机强迫关闭了一个连接”。用 RestClient 或 HttpClient 都会遇到。原因HttpClient 默认复用了 TCP 连接服务端可能有空闲超时策略长时间高频请求后服务端主动断开连接而客户端连接池里保留的仍是那个已经断开的连接下一次写入就直接抛异常。这个问题多发生在循环识别场景。解决给 OCR 请求加一层重试。捕获 HttpRequestException 后等待 1 秒再试最多三次同时把单张请求的超时时间从默认 100 秒缩短到 30 秒避免一张坏图卡死整个批处理。for (int attempt 0; attempt 3; attempt) { try { return await RecognizeAsync(token, imagePath); } catch (HttpRequestException) when (attempt 2) { await Task.Delay((attempt 1) * 1000); } }重试不能变成无脑死循环间隔用递增退避第一次 1 秒、第二次 2 秒。这个重试结构同样适用于 QPS 限流场景只要在 catch 里判断 error_code 是 17 或 18也可以按同样方式退避重试。4.3 识别结果乱码、丢标点透明图层和编码姿势同时背锅现象截屏存成 PNG 的图片识别出来中文字基本对但英文和数字偶尔被识别成其他字符甚至某些行整个丢失。原因第一PNG 截屏图经常带透明通道透明区域在服务端重新采样后可能变成干扰噪声第二请求体构造时没有对 image 的 Base64 做 URL 编码导致部分字符被解析错误。解决识别前把图片统一转成白底 24 位 RGB 的 JPEG。用 System.Drawing 处理即可using (Bitmap src new Bitmap(originalPath)) using (Bitmap rgb new Bitmap(src.Width, src.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)) using (Graphics g Graphics.FromImage(rgb)) { g.Clear(Color.White); g.DrawImage(src, 0, 0, src.Width, src.Height); rgb.Save(tempJpgPath, ImageFormat.Jpeg); }要再提速可以用 LockBits 拿 BitmapData 逐像素拷到新画布但对大多数截图和拍照图来说Graphics.DrawImage 转白底已经足够。转换后再重新读字节、转 Base64请求体里用 Uri.EscapeDataString 做编码。这套组合拳打下来乱码类问题基本绝迹。4.4 QPS 限流17/18并发上去了才看到东墙现象单张测试稳定一旦开多线程同时识别返回里出现大量 error_code 17Open api qps request limit reached和 error_code 18Open api total request limit reached。原因百度 OCR 免费额度对 QPS 有硬性限制。通用版一般并行只能到 2 到 5高精度版更严格。自己写循环时感觉不到一接入批量队列就立刻暴露。解决用 SemaphoreSlim 把并发数限制在 QPS 以内这是最直接的办法。SemaphoreSlim gate new SemaphoreSlim(2); async Taskstring CallWithLimitAsync(string token, string imagePath) { await gate.WaitAsync(); try { return await RecognizeAsync(token, imagePath); } finally { gate.Release(); } }SemaphoreSlim(2) 表示同时最多只有两个请求在飞。先限制并发再结合上一节的重试逻辑处理偶发限流。上线前先算一下峰值每天多少张、集中在哪个时段、免费 QPS 够不够。不够就升级付费配额或者在代码里做任务队列削峰而不是硬顶着限流反复重试。4.5 WinForm 卡死 10 秒同步调用的代价现象点击识别按钮后 WinForm 窗体无法拖动鼠标变成转圈状态持续时间从 2 秒到 10 秒不等识别完成后恢复。原因在按钮事件里用了 HttpClient 的同步方法比如 .Result 或 .Wait()这个调用占住的正是 UI 线程。一旦网络稍慢UI 线程就被阻塞整个界面失去响应。解决按钮事件改成 async void所有 HTTP 调用都用 await。如果项目用的是旧版 .NET Framework 4.5 以上都支持 async/await不需要额外包。还有一个细节await 之后会回到 UI 线程所以更新 TextBox 或 PictureBox 不需要手动 Invoke但前提是全程都用 await而不是把同步方法包进 Task.Run 里再用 .Result 等。5. 进阶识别结果按坐标画框批量任务串成可控队列5.1 把 words_result 的坐标转成矩形框如果想知道每行文字在图片的哪个位置通用文字识别 general_basic 是不够的它不返回坐标。要换用带位置信息的接口路径是 /rest/2.0/ocr/v1/general_location返回的 words_result 里每个元素多了 location 对象包含 left、top、width、height 四个整数。using (Graphics g Graphics.FromImage(canvas)) using (Pen pen new Pen(Color.Red, 2)) { foreach (JsonElement item in words.EnumerateArray()) { JsonElement loc item.GetProperty(location); int left loc.GetProperty(left).GetInt32(); int top loc.GetProperty(top).GetInt32(); int width loc.GetProperty(width).GetInt32(); int height loc.GetProperty(height).GetInt32(); g.DrawRectangle(pen, left, top, width, height); } }画框的意义在于验证识别结果和原图是否对齐也能配合鼠标点击实现“点哪行拿哪行”的交互。注意画框用的坐标系统是原始图片像素所以在 PictureBox 上叠加时先确认 SizeMode 不是缩放模式否则框会错位。5.2 批量目录识别SemaphoreSlim 控制并发批量场景下把目录里所有图片跑一遍 OCR输出和图片同名的 .txt 文件。用 SemaphoreSlim 限制并发每个文件独立重试批次之间留意 token 是否过期。我在实际项目里的习惯是每处理完一张图立刻释放引用不把 Bitmap 对象留在内存里攒着否则连续跑上千张小图内存涨得很明显。最后说一个我自己的教训早期做批量识别时我总是不压缩图片直接转 Base64结果单张图片刚过 4M 就被服务端拒绝整批任务在 80% 进度时翻车。后来在识别前先检查文件大小和长边像素超过阈值就用 DrawImage 缩放到长边 1024 以内再转 JPEG。这个预检查步骤救过不少次场。另一个习惯是 access_token 每次启动时重新换取绝不把旧 token 序列化到本地少踩很多 110 的坑。希望帮到你。本文还有配套的精品资源点击获取