Github Copilot 实战: 从零开始用AI写一个OCR工具(1)——WPF+.NET桌面端识别流程拆解
1. 从零搭一个 WPF OCR 工具为什么值得用 Github Copilot 辅助Github Copilot 在桌面端项目里最实用的场景不是帮你写一个完整应用而是帮你把「我知道要做什么但懒得查 API」的那部分代码快速补全。WPF .NET 做 OCR 工具就是典型例子界面布局、拖拽事件、剪贴板处理、图像坐标映射这些逻辑你脑子里有轮廓但手写起来要翻不少文档。Copilot 能根据注释和上下文直接给出可运行的骨架你只需要在关键位置修正类型和参数。这篇文章要交付的是一个最小可用的 WPF OCR 工具支持拖拽图片、CtrlV 粘贴、点击选择文件三种输入方式调用 PaddleOCR 做中英文识别把识别框绘制回原图右侧同步显示文本。整个项目基于 .NET 9 WPFOCR 引擎用 Sdcb.OpenVINO.PaddleOCR图像处理用 OpenCvSharp。跑通之后我会把识别服务的调用通道切到 TaoToken 的统一 API 入口这样后续换模型、加多模态能力时不用改一堆 endpoint。适合谁看有 C# 基础、想快速验证桌面端 OCR 可行性的开发者已经在用 Github Copilot 但不知道怎么引导它生成正确类型代码的人以及想把本地 OCR 和云端模型调用串起来的工程实践者。下面从项目结构开始每一步都给可复制的配置和命令。2. 项目结构与依赖配置WPF .NET 9 的 csproj 怎么写先建一个 WPF 工程目标框架选 net9.0-windows。用命令行或者 Visual Studio 新建都行关键是 csproj 里的包引用要一次到位。我试过把 OpenCvSharp 和 PaddleOCR 的运行时包分开装结果在 x64 环境下缺 native dll后来统一用 runtime.win 包才稳定。Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet9.0-windows/TargetFramework Nullableenable/Nullable ImplicitUsingsenable/ImplicitUsings UseWPFtrue/UseWPF Platformsx64/Platforms /PropertyGroup ItemGroup PackageReference IncludeOpenCvSharp4.runtime.win Version4.11.0.20250507 / PackageReference IncludeSdcb.OpenVINO.PaddleOCR Version0.6.8 / PackageReference IncludeSdcb.OpenVINO.PaddleOCR.Models.Online Version0.6.2 / PackageReference IncludeSdcb.OpenVINO.runtime.win-x64 Version2025.0.0 / /ItemGroup /Project这里有个坑要注意Sdcb.OpenVINO.runtime.win-x64必须显式引用否则运行时会报找不到openvino.dll。另外Platforms建议锁 x64因为 OpenVINO 的 native 库目前对 AnyCPU 支持不好。项目目录结构建议这样组织MiOcr/ ├── MiOcr.csproj ├── App.xaml ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Services/ │ └── PaddleOCRService.cs └── Models/ └── OcrRegion.cs把 OCR 服务单独放 Services 目录后面要换成 TaoToken 通道时只改这一个文件。Models 里放识别结果的轻量封装避免 UI 层直接依赖 PaddleOCR 的类型。Copilot 在这里的用法在 csproj 里敲完PackageReference的前几个字符它会自动补全版本号在 Services 目录新建类时输入// 调用 PaddleOCR 识别图片返回文本列表和区域信息它会生成方法签名和基本调用链。但版本号一定要自己核对Copilot 有时会给过时的版本。3. 可复制的 OCR 服务配置从本地 PaddleOCR 到 TaoToken 统一通道先写本地 OCR 服务把识别链路跑通。这个类负责三件事把输入文件路径、URL、byte[]、Mat统一转成 Mat调用 PaddleOCR 模型返回文本列表和区域结果。using OpenCvSharp; using Sdcb.OpenVINO.PaddleOCR; using Sdcb.OpenVINO.PaddleOCR.Models; using Sdcb.OpenVINO.PaddleOCR.Models.Online; using System.Diagnostics; namespace MiOcr.Services; public class PaddleOCRService { public static bool IsUrl(string filename) { return Uri.TryCreate(filename, UriKind.Absolute, out var uriResult) (uriResult.Scheme Uri.UriSchemeHttp || uriResult.Scheme Uri.UriSchemeHttps); } public async Task(Liststring strings, PaddleOcrResult result) StartOCR(string filename) { if (string.IsNullOrEmpty(filename)) throw new ArgumentNullException(nameof(filename)); Mat src; if (IsUrl(filename)) { using var http new HttpClient(); var bytes await http.GetByteArrayAsync(filename); src Cv2.ImDecode(bytes, ImreadModes.Color); } else { src Cv2.ImRead(filename); } return await StartOCR(src); } public async Task(Liststring strings, PaddleOcrResult result) StartOCR(byte[] imageData) { ArgumentNullException.ThrowIfNull(imageData); var src Cv2.ImDecode(imageData, ImreadModes.Color); return await StartOCR(src); } public async Task(Liststring strings, PaddleOcrResult result) StartOCR(Mat src) { var resultText new Liststring(); FullOcrModel model await OnlineFullModels.ChineseV3.DownloadAsync(); using var all new PaddleOcrAll(model) { AllowRotateDetection true, Enable180Classification true, }; var sw Stopwatch.StartNew(); var result all.Run(src); Console.WriteLine($elapsed{sw.ElapsedMilliseconds} ms); foreach (PaddleOcrResultRegion region in result.Regions) { resultText.Add(region.Text); } src.Dispose(); return (resultText, result); } }模型下载是异步的第一次运行会拉取 ChineseV3 模型大概几十 MB。建议在 UI 上加一个「正在初始化 OCR 模型」的提示否则用户会以为程序卡死。现在说 TaoToken 通道的接入。TaoToken 提供统一的 API 入口Base URL 是https://taotoken.net/api你可以在控制台生成 Key然后在代码里把模型调用指向这个 endpoint。对于 OCR 场景如果你想把识别后的文本再交给大模型做结构化提取比如从发票图片里抽金额、日期就可以在本地 OCR 之后追加一次模型调用。配置方式有两种。第一种是环境变量适合本地开发set TAOTOKEN_API_KEYsk-你的Key set TAOTOKEN_BASE_URLhttps://taotoken.net/api第二种是配置文件适合团队协作。在项目根目录建appsettings.json{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, ModelId: claude-sonnet-4-20250514 } }然后在代码里读取using System.Text.Json; public class TaoTokenConfig { public string BaseUrl { get; set; } https://taotoken.net/api; public string ApiKey { get; set; } ; public string ModelId { get; set; } claude-sonnet-4-20250514; public static TaoTokenConfig Load(string path appsettings.json) { var json File.ReadAllText(path); using var doc JsonDocument.Parse(json); var root doc.RootElement.GetProperty(TaoToken); return new TaoTokenConfig { BaseUrl root.GetProperty(BaseUrl).GetString()!, ApiKey root.GetProperty(ApiKey).GetString()!, ModelId root.GetProperty(ModelId).GetString()!, }; } }三件套必须齐全Base URL 指向https://taotoken.net/apiKey 从控制台生成Model ID 按你实际要用的模型填。缺任何一个都会在请求时返回 401 或 model not found。如果你用的是 Claude Code 或者 Cline 这类工具做辅助开发配置逻辑是一样的Base URL 填 TaoToken 的 API 地址Key 填生成的 KeyModel ID 填对应模型。CC Switch 里切换配置时注意把这三项一起改不要只改 Key。4. 验证请求与成功结果跑通最小识别链路配置写好后先验证 OCR 服务本身能跑通。在 MainWindow 的构造函数里加一个测试调用或者单独写个控制台入口var service new PaddleOCRService(); var (texts, result) await service.StartOCR(C:\test\sample.png); Console.WriteLine($识别到 {texts.Count} 个文本区域); foreach (var t in texts) { Console.WriteLine(t); }第一次运行会触发模型下载控制台会输出下载进度。下载完成后你会看到类似这样的输出elapsed342 ms 识别到 5 个文本区域 发票代码 1234567890 开票日期 2025-01-15 金额 ¥1,280.00如果识别结果为空先检查图片路径是否正确、图片是否真的包含文字。PaddleOCR 对纯色背景上的小字识别率一般建议用截图工具截一块有明显文字的屏幕区域做测试。接下来验证 TaoToken 通道。写一个简单的 HTTP 请求把 OCR 识别出的文本发给模型做结构化提取using System.Net.Http.Headers; using System.Text; using System.Text.Json; public async Taskstring ExtractWithTaoToken(string ocrText) { var config TaoTokenConfig.Load(); using var http new HttpClient(); http.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, config.ApiKey); var payload new { model config.ModelId, messages new[] { new { role user, content $从以下OCR文本中提取金额和日期返回JSON\n{ocrText} } }, max_tokens 512 }; var json JsonSerializer.Serialize(payload); var content new StringContent(json, Encoding.UTF8, application/json); var response await http.PostAsync(${config.BaseUrl}/v1/messages, content); var body await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) throw new Exception($TaoToken 请求失败: {response.StatusCode} {body}); return body; }成功时返回的 JSON 里会有content数组里面是模型生成的文本。你可以用JsonDocument解析出content[0].text。验证顺序建议先本地 OCR 跑通再单独测 TaoToken 请求最后把两者串起来。这样出问题时能快速定位是 OCR 环节还是网络环节。5. 常见报错排查401、local proxy failed、reading choices、OAuth401 UnauthorizedKey 没填对或者 Base URL 写成了https://taotoken.net少了/api。检查appsettings.json里的ApiKey是否以sk-开头BaseUrl是否是https://taotoken.net/api。如果用的是环境变量确认set之后重启了终端或 IDE。local proxy failed / connection refused通常是本地网络配置问题。先确认能正常访问https://taotoken.net/api用curl测一下curl -X POST https://taotoken.net/api/v1/messages ^ -H Authorization: Bearer sk-你的Key ^ -H Content-Type: application/json ^ -d {\model\:\claude-sonnet-4-20250514\,\messages\:[{\role\:\user\,\content\:\hi\}],\max_tokens\:10}如果 curl 能通但代码不通检查代码里有没有设置HttpClient的Proxy属性或者系统代理是否干扰。reading choices / index out of range这个报错通常出现在解析模型返回时。TaoToken 返回的 JSON 结构里content是一个数组不是字符串。如果你直接content[0]当字符串用会报类型错误。正确做法是先JsonDocument.Parse再取root.GetProperty(content)[0].GetProperty(text).GetString()。OAuth / authentication failed如果你用的是 Claude Code 或 Cline 这类工具OAuth 报错说明工具在尝试用账号登录而不是 API Key。需要在工具设置里切换到 API Key 模式填入 TaoToken 的 KeyBase URL 填https://taotoken.net/api。CC Switch 里切换配置时确认三件套Base URL、Key、Model ID都改了。模型下载失败PaddleOCR 第一次运行要下载模型如果网络不稳定会超时。可以手动下载模型文件放到缓存目录或者多试几次。控制台会输出下载 URL用浏览器下好放到对应路径也行。识别框位置偏移如果绘制出来的框和文字对不上检查图片的 DPI 和控件缩放。WPF 的Image控件用StretchUniform时实际显示尺寸和原图尺寸不一致需要做坐标映射。简单做法是绘制时用原图尺寸显示时让Image控件自己缩放。6. 继续扩展把 OCR 结果交给模型做后处理最小链路跑通后你可以在这个骨架上加更多能力。比如识别完发票后自动调用 TaoToken 的模型接口做字段提取或者识别截图后让模型判断这段文字的情绪倾向。TaoToken 的模型对话入口在https://taotoken.net/api控制台可以生成和管理 Key接入文档里有各语言的调用示例。如果你打算长期做编码类项目Coding Plan 适合把模型调用集成到日常开发流里如果只是偶尔验证模型效果直接用模型对话页面测试就行。API Keys 页面管理所有 Key接入文档里有完整的 endpoint 列表和参数说明。这个 WPF OCR 工具的完整代码结构就是上面这些csproj 配好依赖PaddleOCRService 封装识别逻辑MainWindow 处理三种输入方式和结果绘制TaoTokenConfig 管理模型通道配置。你可以先把本地 OCR 跑通再把 TaoToken 的调用加进去每一步都有可验证的输出。