效果前言为什么我们需要一个本地 OCR 在企业日常运营中证件、票据、合同等文档的处理一直是费时费力的重复性工作。传统方案通常依赖云端 API虽然接入方便但面临着数据隐私风险、网络延迟、持续的费用支出三大痛点。试想一下一份包含敏感信息的行驶证照片你真的放心把它上传到第三方服务器吗好消息是如今我们可以借助开源模型和本地推理引擎将这些 OCR 任务完全放在自己的机器上完成敏感数据再也不用离开内网。本文使用 C# WinForm 和 llama.cpp搭建一个完整的 Qianfan-OCR 本地推理。这个不仅能处理常规的文字识别还支持表格提取、图表理解、关键信息抽取以及行驶证、合格证等卡证的结构化 JSON 输出。Qianfan-OCR 是什么Qianfan-OCR 是百度千帆团队推出的4B 端到端文档智能模型。它将文档解析、版面分析、文字识别和语义理解统一到单一模型中抛开传统 OCR 系统“检测→识别→理解”的三段式流水线实现了从图像一步生成结构化结果的能力。这个模型的几个核心亮点评测领先在 OmniDocBench v1.5 上得分 93.12端到端模型中位列第一。Layout‑as‑Thought版面即思考通过特殊的think标记触发推理阶段模型会先生成带检测框和阅读顺序的版面分析再输出最终结果。低配可跑不到 5 GBQ8_0 量化。与 PaddleOCR‑VL‑1.50.9B相比Qianfan-OCR 参数量更大4B在表格、图表等复杂结构的理解上表现更稳定PaddleOCR‑VL‑1.5 则更轻量适合低算力场景快速 OCR。整体架构┌──────────────────────┐ HTTP (OpenAI API) ┌──────────────────┐ │ C# WinForm (客户端) │ ──────────────────────→ │ llama-server │ │ • RestSharp │ ←────────────────────── │ (llama.cpp) │ │ • Newtonsoft.Json │ JSON Response └────────┬─────────┘ └──────────────────────┘ │ ┌────────┴─────────┐ │ Qianfan-OCR GGUF │ │ 模型 mmproj │ └──────────────────┘llama-serverllama.cpp 提供的高性能 HTTP 服务器完全兼容 OpenAI Chat Completion API 格式负责加载模型并执行推理。C# WinForm 客户端承担图片选择、Base64 编码、发送请求、解析 JSON 响应、展示结果等全部用户交互逻辑。准备组件版本用途llama.cppb9101模型加载与推理Qianfan-OCR GGUFQ8_0 量化主模型Qianfan-OCR mmprojF16视觉投影器.NET Framework4.8C# 运行环境RestSharp114.xHTTP 客户端Newtonsoft.Json13.0.3JSON 处理服务端启动打开终端进入llama.cpp的目录执行llama-server.exe -m ../Qianfan-OCR-GGUF/Qianfan-OCR-Q8_0.gguf --mmproj ../Qianfan-OCR-GGUF/mmproj-Qianfan-OCR-Q8_0.gguf --port 8080 --host 0.0.0.0 --temp 0.1 -n 2048 -c 8196参数说明 -m主模型 GGUF 文件路径--mmproj视觉投影器文件多模态推理的必需项--host 0.0.0.0允许局域网其他设备访问若仅本机使用可设为 127.0.0.1--temp 0.1低温度保证输出确定性卡证结构化场景推荐 0.1–0.3-n 2048最大生成 token 数JSON 内容较长时可适当调大-c 8192上下文长度可挂载约 46 MB 的 Base64 图片启动成功后会看到main: model loaded main: server is listening on http://0.0.0.0:8080 main: starting the main loop... srv update_slots: all slots are idle客户端C#代码using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RestSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Threading.Tasks; using System.Windows.Forms; namespace OCR_Client { public partial class Form1 : Form { // 内部结果类包含识别文本及分阶段耗时 private class OcrResult { public string Text { get; set; } public Dictionarystring, long Timings { get; set; } new Dictionarystring, long(); } public Form1() { InitializeComponent(); } private string currentImagePath; private void btnSelectImage_Click(object sender, EventArgs e) { using (var dlg new OpenFileDialog()) { dlg.Filter 图片文件|*.jpg;*.jpeg;*.png;*.bmp; if (dlg.ShowDialog() ! DialogResult.OK) return; currentImagePath dlg.FileName; pictureBox1.Image new Bitmap(currentImagePath); txtResult.Text string.Empty; } } // 核心任务调度器接收提示词字符串 private async Task ExecuteOcrTask(string prompt) { var swTotal Stopwatch.StartNew(); try { OcrResult result await OcrImageAsync(currentImagePath, prompt); swTotal.Stop(); // 构建耗时分项信息 string timingDetails 【各阶段耗时】\r\n; foreach (var kvp in result.Timings) { timingDetails $ {kvp.Key}: {kvp.Value} ms\r\n; } // 换行显示问题 string displayText result.Text.Replace(\n, Environment.NewLine); txtResult.Text $【OCR 任务完成】\r\n $客户端总耗时{swTotal.ElapsedMilliseconds} ms\r\n timingDetails $——————————————\r\n displayText; } catch (Exception ex) { swTotal.Stop(); txtResult.Text $【OCR 任务失败】\r\n $客户端总耗时{swTotal.ElapsedMilliseconds} ms\r\n $错误信息{ex.Message}; } finally { SetButtonsEnabled(true); } } private void Form1_Load(object sender, EventArgs e) { } /// summary /// 按最大像素数等比缩放图片返回 JPEG 字节流 /// /summary private byte[] ResizeImageIfNeeded(string imagePath, int maxPixels 1003520) { using (var img new Bitmap(imagePath)) { int currentPixels img.Width * img.Height; if (currentPixels maxPixels) return File.ReadAllBytes(imagePath); double scale Math.Sqrt((double)maxPixels / currentPixels); int newWidth (int)(img.Width * scale); int newHeight (int)(img.Height * scale); using (var resized new Bitmap(img, new Size(newWidth, newHeight))) using (var ms new MemoryStream()) { resized.Save(ms, ImageFormat.Jpeg); return ms.ToArray(); } } } /// summary /// 通用 OCR 调用方法直接传入提示词 /// /summary private async TaskOcrResult OcrImageAsync(string imagePath, string prompt) { var result new OcrResult(); var sw Stopwatch.StartNew(); // 步骤1读取并缩放图片 byte[] imgBytes File.ReadAllBytes(imagePath); //byte[] imgBytes ResizeImageIfNeeded(imagePath, 1003520); result.Timings[读取文件] sw.ElapsedMilliseconds; sw.Restart(); // 步骤2Base64编码 string mime GetMimeType(Path.GetExtension(imagePath)); string base64Image $data:{mime};base64,{Convert.ToBase64String(imgBytes)}; result.Timings[Base64编码] sw.ElapsedMilliseconds; sw.Restart(); // 步骤3构造请求 var payload new { messages new[] { new { role user, content new object[] { new { type image_url, image_url new { url base64Image } }, new { type text, text prompt } } } } }; string jsonBody JsonConvert.SerializeObject(payload); result.Timings[构造请求] sw.ElapsedMilliseconds; sw.Restart(); // 步骤4发送 HTTP 请求 var options new RestClientOptions(http://localhost:8080); using (var client new RestClient(options)) { var request new RestRequest(/v1/chat/completions, Method.Post); request.AddHeader(Content-Type, application/json); request.AddParameter(application/json, jsonBody, ParameterType.RequestBody); RestResponse response await client.ExecuteAsync(request); result.Timings[网络请求] sw.ElapsedMilliseconds; sw.Restart(); if (!response.IsSuccessful) { string errorDetail string.IsNullOrEmpty(response.Content) ? response.StatusDescription : response.Content; throw new Exception($服务器错误 ({response.StatusCode}): {errorDetail}); } Console.WriteLine(response.Content); //用于调试 // 步骤5解析响应 JObject jResult JObject.Parse(response.Content); string content jResult[choices]?[0]?[message]?[content]?.ToString(); result.Timings[解析响应] sw.ElapsedMilliseconds; // 提取服务端推理耗时 JToken timingsToken jResult[timings]; if (timingsToken ! null) { double promptMs timingsToken.Valuedouble(prompt_ms); double predictedMs timingsToken.Valuedouble(predicted_ms); result.Timings[服务端编码(Prompt)] (long)promptMs; result.Timings[服务端生成(Predict)] (long)predictedMs; result.Timings[服务端总推理] (long)(promptMs predictedMs); } sw.Stop(); result.Text content ?? 未能提取到识别文本; return result; } } /// summary /// 根据扩展名获取MIME类型 /// /summary private string GetMimeType(string ext) { switch (ext.ToLower()) { case.jpg: case.jpeg: returnimage/jpeg; case.png: returnimage/png; case.bmp: returnimage/bmp; default: returnimage/jpeg; } } /// summary /// 统一设置所有功能按钮的启用/禁用状态 /// /summary private void SetButtonsEnabled(bool enabled) { btnOCR.Enabled enabled; } async private void btnOCR_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(currentImagePath)) { MessageBox.Show(请先选择一张图片, 提示, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (string.IsNullOrEmpty(txtPrompt.Text)) { MessageBox.Show(请先输出提示词, 提示, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } SetButtonsEnabled(false); txtResult.Text $正在进行 OCR 识别请稍候...; await ExecuteOcrTask(txtPrompt.Text); } } }