.NET 实战:调用千问视觉模型实现 OCR(车票识别完整教程)

很多人第一次接入 AI OCR,会踩几个典型坑:

  • ❌ 直接调接口,结果不稳定

  • ❌ AI 返回 JSON 不规范

  • ❌ 网络偶发失败没有兜底

  • ❌ 代码无法复用

这篇直接带你做一件事:

写一套"可复用 + 稳定 + 可扩展"的 OCR 调用组件


🧠 一、核心思路(先搞清本质)

千问视觉模型 ≠ 传统 OCR

它本质是:

"看图 + 按你要求生成结构化文本"

流程如下👇

复制代码
图片 → Base64 → 千问模型 → 文本(JSON) → 清洗 → DTO

🧩 二、请求结构(必须理解)

请求体核心结构👇

复制代码
{
  "model": "qwen-vl",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/jpeg;base64,xxx"
          }
        },
        {
          "type": "text",
          "text": "你的Prompt"
        }
      ]
    }
  ]
}

👉 重点:

  • 图片必须 Base64

  • Prompt 决定识别质量


🎯 三、示例:车票 OCR Prompt

复制代码
请识别图片中的火车票信息,并严格按JSON格式输出:

{
  "PassengerName": "",
  "TrainNumber": "",
  "FromStation": "",
  "ToStation": "",
  "DepartureTime": "",
  "Seat": "",
  "Price": ""
}

要求:
1. 只输出JSON
2. 不要解释
3. 金额只保留数字
4. 时间格式 yyyy-MM-dd HH:mm:ss
5. 缺失字段返回空字符串

🧱 四、核心设计(工程级)

这一套你可以直接复用到:

  • 发票

  • 水单

  • 提单

  • 合同


✅ 关键设计点

1. 通用泛型方法

cs 复制代码
Task<T> ExtractAsync<T>()

👉 一套代码支持所有 OCR 类型


2. JSON 清洗(必须)

处理:

  • 中文引号

  • 尾逗号


3. 重试机制

防:

  • 网络波动

  • 接口偶发失败


4. DTO 强类型

👉 不要返回 string,必须结构化


🛡️ 五、稳定性策略(精华)

你这套代码真正值钱的地方在这里👇

  • ✅ 重试机制(3次)

  • ✅ JSON 修复

  • ✅ 文件大小限制

  • ✅ 临时文件自动清理

  • ✅ 反序列化兜底


⚠️ 六、生产环境建议

🔐 Key 管理

不要写死,放:

  • 环境变量

  • 配置文件


⚡ 并发控制

OCR 是重资源操作:

👉 建议限流(SemaphoreSlim)


📊 日志

建议记录:

  • 原始返回

  • 清洗后 JSON


🧾 七、完整可运行代码(核心部分)

下面这份代码是精简 + 工程可用版本👇


🧩 1. OCR Helper

cs 复制代码
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

public static class OcrHelper
{
    private static readonly HttpClient _httpClient = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(60)
    };

    private const string ApiUrl = "你的API地址";
    private const string ApiKey = "你的Key";
    private const string Model = "你的模型名称";

    static OcrHelper()
    {
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
    }

    public static async Task<T> ExtractAsync<T>(string imagePath, string prompt)
    {
        var json = await ExtractRawAsync(imagePath, prompt);

        return JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });
    }

    public static async Task<string> ExtractRawAsync(string imagePath, string prompt)
    {
        byte[] bytes = await File.ReadAllBytesAsync(imagePath);

        if (bytes.Length > 5 * 1024 * 1024)
            throw new Exception("图片不能超过5MB");

        var base64 = Convert.ToBase64String(bytes);

        var requestBody = new
        {
            model = Model,
            messages = new object[]
            {
                new
                {
                    role = "user",
                    content = new object[]
                    {
                        new
                        {
                            type = "image_url",
                            image_url = new
                            {
                                url = $"data:image/jpeg;base64,{base64}"
                            }
                        },
                        new
                        {
                            type = "text",
                            text = prompt
                        }
                    }
                }
            },
            stream = false,
            extra_body = new
            {
                enable_thinking = false
            }
        };

        var content = new StringContent(
            JsonSerializer.Serialize(requestBody),
            Encoding.UTF8,
            "application/json"
        );

        var response = await SendWithRetryAsync(content);

        var message = ExtractContent(response);

        return CleanJson(message);
    }

    private static async Task<string> SendWithRetryAsync(StringContent content)
    {
        for (int i = 0; i < 3; i++)
        {
            try
            {
                var response = await _httpClient.PostAsync(ApiUrl, content);
                var text = await response.Content.ReadAsStringAsync();

                if (!response.IsSuccessStatusCode)
                    throw new Exception(text);

                return text;
            }
            catch when (i < 2)
            {
                await Task.Delay(1000 * (i + 1));
            }
        }

        throw new Exception("OCR请求失败");
    }

    private static string ExtractContent(string responseText)
    {
        using var doc = JsonDocument.Parse(responseText);

        return doc.RootElement
            .GetProperty("choices")[0]
            .GetProperty("message")
            .GetProperty("content")
            .GetString();
    }

    private static string CleanJson(string json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return "{}";

        json = json
            .Replace("```json", "")
            .Replace("```", "")
            .Replace(""", "\"")
            .Replace(""", "\"")
            .Trim();

        json = Regex.Replace(json, ",\\s*}", "}");
        json = Regex.Replace(json, ",\\s*]", "]");

        return json;
    }
}

🧾 2. DTO(车票)

cs 复制代码
public class TrainTicketDto
{
    public string PassengerName { get; set; }
    public string TrainNumber { get; set; }
    public string FromStation { get; set; }
    public string ToStation { get; set; }
    public string DepartureTime { get; set; }
    public string Seat { get; set; }
    public string Price { get; set; }
}

🎯 3. 调用示例

cs 复制代码
var prompt = @"请识别图片中的火车票信息,并按JSON输出:
{
  ""PassengerName"": """",
  ""TrainNumber"": """",
  ""FromStation"": """",
  ""ToStation"": """",
  ""DepartureTime"": """",
  ""Seat"": """",
  ""Price"": """"
}";

var result = await OcrHelper.ExtractAsync<TrainTicketDto>(
    "test.jpg",
    prompt
);

Console.WriteLine(result.PassengerName);

🧭 最后总结一句话

AI OCR 不难,难的是把"不稳定输出"变成"稳定系统能力"。

相关推荐
石榴树下的七彩鱼2 小时前
OCR 识别接口哪个好?2026 年主流 OCR API 对比评测(附免费在线体验)
图像处理·人工智能·后端·计算机视觉·ocr·api·文字识别
AI人工智能+2 小时前
表格识别技术通过深度学习与计算机视觉,实现复杂表格的自动化解析与结构化输出
深度学习·计算机视觉·ocr·表格识别
唐青枫2 小时前
C#.NET ValueTaskSource 深入解析:零分配异步、ManualResetValueTaskSourceCore 与使用边界
c#·.net
笨蛋©2 小时前
工程图纸数字化用什么工具?详解图片格式图纸识别与FAI自动化实战
ai·cad·质量管理·制造业·图纸识别
公子小六2 小时前
基于.NET的Windows窗体编程之WinForms事件简介
windows·microsoft·c#·.net
军训猫猫头2 小时前
7.带输入参数的线程启动 C# + WPF 完整示例
开发语言·前端·c#·.net·wpf
光泽雨2 小时前
c#数值类型之间的自动转换
java·算法·c#
davidson14712 小时前
VSCode配置Claude Code
vscode·ai·大模型·claude