基于WPF和腾讯云OCR的批量图片区域文字识别重命名工具

发布时间:2026/9/20 9:44:44
基于WPF和腾讯云OCR的批量图片区域文字识别重命名工具 先把需求说清楚你手头有一批jpg图片每张图片的同一个位置有一段文字可能是截图里的编号、扫描件里的单号、商品图上的货号你想把这串文字自动读出来直接作为文件名。网上确实有一些截图OCR工具但大多只能全图识别或者不支持批量或者没法固定识别区域。真有需求的话自己用WPF加腾讯云OCR接口做一个工具反而最灵活。这个方案适合两类人一类是被重复劳动折磨的办公族比如每天要整理几十上百张扫描件的财务、档案岗位另一类是正在学C#桌面开发想找一个完整项目练手的开发者。整个过程不复杂核心就三步在图片上框出文字区域、调用OCR接口识别、把识别结果清洗后重命名。下面把每一步的坑和细节都写清楚。1. 需求剖析与整体方案设计1.1 这类批量识别改名的典型场景我最早遇到这个需求是帮朋友处理一批电商商品图。每张图右下角都有一串货号比如“AB20240815-CN”但文件名全是“IMG_001.jpg”这种乱码。几百张图如果一张张打开看图、复制货号、再改文件名至少两小时。用这个工具框一次区域泡杯茶的功夫就全改完了。类似的场景还有很多财务人员处理发票扫描件想用发票号码或者发票代码命名文件行政人员整理合同扫描件想用合同编号命名做标注的同学有一批数据集图片图片角落有样本编号甚至有人拿它整理截图截图上恰好有日期时间自动用时间命名。这些场景有个共同点要识别的文字只出现在图片的固定区域而且文字是印刷体不是手写体。这就非常适合用固定区域裁剪加通用OCR来处理。1.2 技术选型为什么是WPF加腾讯云API先说为什么不用现成软件。市面上不是完全没有类似工具但要么收费不低要么对区域识别支持不好。有些OCR小工具是全图识别识别完弹出一堆文字让你自己复制没法直接联动文件名。有些批量转换工具能改文件名但只能按时间戳或序号不能从图片内容提取。真要做区域固定、批量处理、直接改名这三个条件同时满足自己写一个反而最靠谱。桌面端框架方面我选了WPF而不是WinForms主要原因是WPF的界面布局能力更强。这个工具要在界面上显示图片、叠加一个可拖拽的选区矩形还要有图片列表、日志区WPF用XAML做这些比WinForms舒服得多。另外就是WPF的绑定性价比高做批量任务时用INotifyPropertyChanged更新进度条和日志代码写起来很顺手。OCR引擎方面我直接选择了腾讯云通用印刷体识别接口。原因有三第一识别准确率高尤其对中文和数字混合的文本比我试过的本地Tesseract好很多第二接入简单HTTP请求加签名就能用不需要在本地装模型第三有免费额度个人小批量使用基本够。当然你也可以换成其他云厂商的OCR接口调用逻辑基本一致只改签名和请求参数就行。1.3 整体处理流程设计整个工具的运作流程是这样的用户添加一张或多张jpg图片支持拖拽和文件夹导入图片加载到界面预览区用户用鼠标在图片上拖出一个矩形这个矩形就是“识别区域”在区域设置里保存这个矩形的位置和大小下次启动程序自动带出来点击“开始处理”按钮程序依次读取每张图片每张图片先按区域矩形裁剪出子图再转成Base64调用腾讯云OCR接口返回识别文本对识别文本做清理去掉换行、替换非法字符、干掉多余空格使用清理后的文本作为新文件名重命名原文件刷新界面日志记录每张图片的成功或失败状态。这个流程看起来简单但每一步都有细节。比如区域坐标换算界面上图片显示大小和实际图片像素大小不一定一样如果不做换算框的区域会偏。再比如重命名时的同名冲突如果两张图片识别出同一个名字直接改会覆盖文件。这些问题后面都会讲到。2. 环境准备与基础工程搭建2.1 创建WPF项目并配置依赖我用的开发环境是Visual Studio 2022.NET 6.0。你可以用更新的.NET 8.0差别不大。创建一个WPF应用程序项目项目名称就叫“RegionOcrRenamer”吧。创建项目之后需要引入两个NuGet包System.Drawing.Common用来做图片区域裁剪和格式转换Newtonsoft.Json用来解析腾讯云API返回的JSON用System.Text.Json也行但Newtonsoft用着更顺手。装好之后建议在项目里新建三个类文件后面代码都按这个结构放Models/OcrResult.csOCR识别结果的实体类Services/TencentOcrClient.cs封装腾讯云API调用包括签名、请求、响应解析Utils/ImageRegionHelper.cs图片裁剪和坐标换算的辅助方法。这种分层方式很朴素但对这种小工具来说足够清晰。以后想换OCR服务商只需要替换TencentOcrClient一个类。2.2 配置腾讯云密钥的正确姿势腾讯云OCR接口需要SecretId和SecretKey。在腾讯云控制台的“访问管理”里创建API密钥拿到这一对字符串。请注意SecretKey是非常敏感的凭据不要硬编码在代码里尤其是如果代码要传到Git仓库。我采取的做法是写一个appsettings.json文件放在程序目录下内容大致这样{ TencentOcr: { SecretId: 在这里填你的SecretId, SecretKey: 在这里填你的SecretKey, Region: ap-guangzhou } }然后在程序启动时读取public static AppConfig LoadConfig() { var path Path.Combine(AppDomain.CurrentDomain.BaseDirectory, appsettings.json); if (!File.Exists(path)) { return new AppConfig(); } var json File.ReadAllText(path); return JsonConvert.DeserializeObjectAppConfig(json); }Region默认填ap-guangzhou就行OCR接口在腾讯云所有地域都有节点广州一般响应速度不错。如果不确定地域可以用ap-guangzhou然后把appsettings.json和程序放一起方便以后改。3. 界面设计与区域框选交互实现3.1 主界面布局思路这个工具界面的核心是“选区域”这件事。为了让它好用布局上我分了三个区域左侧图片列表ListBox显示所有待处理的图片文件名中间图片预览区上面可以画选区矩形底部操作按钮添加图片、清除列表、开始处理以及一个日志文本框。图片预览区是最关键的控件。我在XAML里用一个Grid作为容器底部放Image显示图片上面放一个Canvas用来画选区矩形Grid x:NameImageContainer Background#1E1E1E Image x:NamePreviewImage StretchUniform / Canvas x:NameSelectCanvas / /GridStretchUniform会让图片等比缩放适应容器大小这带来了一个坐标换算问题。实际图片的像素坐标和界面上的显示坐标不是一一对应的所以必须在鼠标事件里做换算。3.2 在图片上框选识别区域的实现框选功能用Canvas加三个鼠标事件MouseLeftButtonDown、MouseMove、MouseLeftButtonUp。MouseDown时记下起点创建一个Rectangle控件加入Canvasprivate Point _startPoint; private Rectangle _selectRect; private void SelectCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (_previewBitmap null) return; _startPoint e.GetPosition(SelectCanvas); _selectRect new Rectangle { Stroke Brushes.Red, StrokeThickness 2, StrokeDashArray new DoubleCollection { 4, 2 } }; Canvas.SetLeft(_selectRect, _startPoint.X); Canvas.SetTop(_selectRect, _startPoint.Y); SelectCanvas.Children.Add(_selectRect); } private void SelectCanvas_MouseMove(object sender, MouseEventArgs e) { if (_selectRect null) return; var current e.GetPosition(SelectCanvas); double x Math.Min(_startPoint.X, current.X); double y Math.Min(_startPoint.Y, current.Y); double width Math.Abs(current.X - _startPoint.X); double height Math.Abs(current.Y - _startPoint.Y); Canvas.SetLeft(_selectRect, x); Canvas.SetTop(_selectRect, y); _selectRect.Width width; _selectRect.Height height; } private void SelectCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (_selectRect null) return; // 计算裁剪区域并保存 ... _selectRect null; }这里有个细节选区矩形画在Canvas里而Canvas和Image同处一个Grid但Canvas在上层所以鼠标事件要挂在Canvas上不要挂在Image上。否则拖拽时会触发Image的事件选区画不出来。3.3 坐标换算显示坐标转像素坐标前面说过StretchUniform下显示坐标和像素坐标不一致。换算思路是先算图片在界面上的实际显示尺寸再按比例映射。公式是这样的显示比例 实际显示宽度 / 图片原始宽度 像素坐标 显示坐标 / 显示比例其中实际显示宽度需要根据容器的可用空间算出来。因为Uniform拉伸下图片要么宽满要么高满要用ActualWidth和ActualHeight结合图片宽高比判断。完整代码如下public static Rectangle GetPixelRegion(Bitmap source, Point displayStart, Point displayEnd) { double imageWidth source.Width; double imageHeight source.Height; // 图片在界面上的实际显示尺寸 double viewWidth ...; // 从Image.ActualWidth获取 double viewHeight ...; // 从Image.ActualHeight获取 double scaleX imageWidth / viewWidth; double scaleY imageHeight / viewHeight; int x (int)Math.Floor(Math.Min(displayStart.X, displayEnd.X) * scaleX); int y (int)Math.Floor(Math.Min(displayStart.Y, displayEnd.Y) * scaleY); int width (int)Math.Ceiling(Math.Abs(displayEnd.X - displayStart.X) * scaleX); int height (int)Math.Ceiling(Math.Abs(displayEnd.Y - displayStart.Y) * scaleY); // 边界限制 x Math.Max(0, Math.Min(x, source.Width - 1)); y Math.Max(0, Math.Min(y, source.Height - 1)); width Math.Min(width, source.Width - x); height Math.Min(height, source.Height - y); return new Rectangle(x, y, width, height); }这一步不做直接拿界面坐标去裁剪结果会偏差很大尤其是图片被放大或缩小显示的时候。我一开始偷懒没做改出来的文件全是乱的后来才补上。3.4 区域记忆与默认值这个工具如果每次打开都要重新框选区域体验就很差。所以我把选区的坐标和大小保存到本地配置里程序启动时加载图片后直接画出来。保存方式最简单的是在appsettings.json里加一段SelectRegion: { X: 100, Y: 100, Width: 300, Height: 80 }每次MouseUp后更新这个配置保存到磁盘。下次添加图片时如果检测到配置里有有效的区域信息就直接在Canvas上画一个预设的红色矩形。对于绝大多数场景识别区域一般位于图片的角落或固定位置。如果不同批次的图片位置有变化重新框一次就行这个操作很快。4. 腾讯云OCR API接入与调用4.1 接口选型与调用前准备腾讯云的OCR接口很多我们这个场景用的是“通用印刷体识别”接口名为GeneralBasicOCR。腾讯云官方文档里有详细的请求参数说明基础请求参数如下Action固定为GeneralBasicOCRVersion固定为2018-11-19Region地域如ap-guangzhouTimestamp当前Unix时间戳Nonce随机正整数用于防重放SecretId你的密钥IDSignature签名串请求内容是一段JSON里面放图片的Base64编码或图片URL。我们这里是本地文件直接转Base64传过去。在调用前建议先去腾讯云控制台开通OCR服务并确认账号下已经创建了密钥。免费额度用完之后会按量计费个人用成本很低每千次大概几块钱但记得看一眼定价别产生意外账单。4.2 TC3签名生成与请求封装腾讯云的API签名用的是TC3-HMAC-SHA256算法。第一次看文档会被绕晕但封装一次之后就是固定模板。签名流程大致是拼接规范请求串CanonicalRequest用请求串生成待签名字符串StringToSign生成派生签名密钥SecretKey生成Signature拼出Authorization头。我直接贴核心代码这是整个项目里最容易写错的地方public async Taskstring RecognizeAsync(string imageBase64, CancellationToken ct default) { string endpoint ocr.tencentcloudapi.com; string service ocr; string action GeneralBasicOCR; string version 2018-11-19; string region _config.Region; string host endpoint; string algorithm TC3-HMAC-SHA256; DateTime now DateTime.UtcNow; string timestamp now.ToUnixTimeSeconds().ToString(); string date now.ToString(yyyy-MM-dd); string payload JsonConvert.SerializeObject(new { ImageBase64 imageBase64 }); // 1. CanonicalRequest string canonicalUri /; string canonicalQuery ; string canonicalHeaders $content-type:application/json; charsetutf-8\nhost:{host}\n; string signedHeaders content-type;host; string hashedPayload SHA256Hex(payload); string canonicalRequest $POST\n{canonicalUri}\n{canonicalQuery}\n{canonicalHeaders}\n{signedHeaders}\n{hashedPayload}; // 2. StringToSign string credentialScope ${date}/{service}/tc3_request; string hashedCanonicalRequest SHA256Hex(canonicalRequest); string stringToSign ${algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}; // 3. Signature string secretKey _config.SecretKey; byte[] secretDate HmacSHA256(Encoding.UTF8.GetBytes(TC3 secretKey), date); byte[] secretService HmacSHA256(secretDate, service); byte[] secretSigning HmacSHA256(secretService, tc3_request); string signature HexEncode(HmacSHA256(secretSigning, stringToSign)); // 4. Authorization string authorization ${algorithm} Credential{_config.SecretId}/{credentialScope}, SignedHeaders{signedHeaders}, Signature{signature}; using var client new HttpClient(); client.DefaultRequestHeaders.Add(Authorization, authorization); client.DefaultRequestHeaders.Add(X-TC-Action, action); client.DefaultRequestHeaders.Add(X-TC-Version, version); client.DefaultRequestHeaders.Add(X-TC-Timestamp, timestamp); client.DefaultRequestHeaders.Add(X-TC-Region, region); var content new StringContent(payload, Encoding.UTF8, application/json); var response await client.PostAsync($https://{host}, content, ct); return await response.Content.ReadAsStringAsync(); }SHA256Hex、HmacSHA256、HexEncode这三个辅助函数实现起来也不难网上模板很多照着抄就行。写完后用一个已知的SecretId做一次测试请求能返回正常识别结果就说明签名没问题。需要注意的一点请求头里的X-TC-Action、X-TC-Version是腾讯云API的通用参数和请求体里的Action重复了但一个都不能少。我第一次调用时漏了X-TC-Version折腾了半小时才搞明白。4.3 区域裁剪与Base64转换调用OCR接口前要把原图按选区裁剪出来。裁剪用System.Drawingpublic static byte[] CropToBytes(byte[] originalBytes, Rectangle region) { using var ms new MemoryStream(originalBytes); using var original new Bitmap(ms); if (region.Width 0 || region.Height 0) throw new ArgumentException(识别区域无效); if (region.X region.Width original.Width || region.Y region.Height original.Height) throw new ArgumentException(识别区域超出图片范围); using var cropped original.Clone(region, PixelFormat.Format24bppRgb); using var outMs new MemoryStream(); cropped.Save(outMs, ImageFormat.Png); return outMs.ToArray(); }这里我把裁剪结果保存为PNG而不是JPEG主要是为了识别的清晰度。JPEG压缩会引入噪点对OCR效果有一点影响尤其文字很小时。PNG虽然体积大一点但OCR识别时这点体积无所谓清晰度才是关键。拿到裁剪后的字节数组转Base64再传给OCR接口string imageBase64 Convert.ToBase64String(croppedBytes);腾讯云对Base64字符串有大小限制一张裁剪后的PNG子图一般不会超过几MB远低于限制所以基本不用担心。不过为了保险实际处理前可以检查一下Base64长度超过限制就转成JPEG再传。4.4 识别结果解析与容错OCR接口返回的JSON结构是这样的{ Response: { TextDetections: [ { DetectedText: AB20240815-CN, Confidence: 99 } ], RequestId: xxxx } }TextDetections是一个数组可能有多行文字。对于我们的命名场景通常只关心识别结果里的核心字符串。因为区域是我们框出来的里面一般只有一行或少数几行文字所以直接把所有DetectedText拼接起来就行。我的解析代码很简单public class TencentOcrResponse { public ResponseInfo Response { get; set; } } public class ResponseInfo { public ListTextDetection TextDetections { get; set; } public ErrorInfo Error { get; set; } } public class TextDetection { public string DetectedText { get; set; } } public class ErrorInfo { public string Code { get; set; } public string Message { get; set; } }解析时有一个容易踩的坑接口调用失败时Response里会有Error字段而不是TextDetections。所以必须先判断Error是否为空再取文本var result JsonConvert.DeserializeObjectTencentOcrResponse(json); if (result.Response?.Error ! null) { throw new Exception($OCR识别失败: {result.Response.Error.Code} - {result.Response.Error.Message}); } if (result.Response?.TextDetections null || result.Response.TextDetections.Count 0) { return string.Empty; } string text string.Join(, result.Response.TextDetections.Select(t t.DetectedText)); return text;识别失败时不要直接抛出异常让程序崩溃应该记录到日志里继续处理下一张。批量任务最怕中途挂掉宁可后面统一看日志重试。5. 批量处理与文件重命名实现5.1 批量遍历与识别队列这一步其实不需要引入什么复杂队列一个foreach循环加await就能实现。不过UI上需要保证“开始处理”按钮点击后界面不卡死进度条能跳动。我把批量处理放在一个异步方法里private async void OnStartProcessing(object sender, RoutedEventArgs e) { if (_fileList.Count 0) return; _isProcessing true; DisableButtons(); int total _fileList.Count; int current 0; foreach (var file in _fileList.ToList()) { if (_cancelled) break; current; UpdateProgress(current, total); try { string newName await ProcessSingleFileAsync(file); AppendLog($[成功] {file} - {newName}); } catch (Exception ex) { AppendLog($[失败] {file}{ex.Message}); } } _isProcessing false; EnableButtons(); AppendLog(处理完成); }注意这里用了async void因为这是事件处理器。ProcessSingleFileAsync内部会依次执行裁剪、转Base64、调用OCR、清洗文本、重命名文件。每个环节有异常都向上抛由外层捕获后记日志。5.2 文件名清洗与冲突处理OCR识别出来的文本不能直接当文件名。我总结了几类必须处理的问题首尾空格和换行符要去掉否则文件名不可见路径非法字符\/:*?|要替换成下划线或空字符文件名末尾的点和空格要处理Windows不允许文件名长度不要超过100个字符避免额外的系统路径限制问题识别结果可能是中文、英文、数字混排尽量保留原样不要做多余的大小写转换。我的清洗代码大概是这样public static string CleanFileName(string rawText) { if (string.IsNullOrWhiteSpace(rawText)) return 未识别; string fileName rawText.Trim(); char[] invalidChars Path.GetInvalidFileNameChars(); foreach (char c in invalidChars) { fileName fileName.Replace(c, _); } fileName fileName.TrimEnd(., ); if (fileName.Length 80) { fileName fileName.Substring(0, 80); } return string.IsNullOrWhiteSpace(fileName) ? 未识别 : fileName; }重名冲突处理是另一个重点。同一批图片里如果两张识别出同样的文本直接重命名会把前一个文件覆盖。我在处理前维护一个HashSetstring保存已经用过的文件名遇到重名就自动加序号后缀public static string GetUniqueFileName(string directory, string desiredName, string extension, HashSetstring usedNames) { string candidate desiredName; int index 1; while (usedNames.Contains(candidate) || File.Exists(Path.Combine(directory, candidate extension))) { candidate ${desiredName}_{index}; index; } usedNames.Add(candidate); return candidate extension; }注意这里同时检查了usedNames集合和磁盘上是否已经有同名文件。因为用户可能把工具拖到已经有文件的目录里如果目标目录已存在同名文件也会导致覆盖。这个双重检查是必要的。5.3 重命名失败的处理策略重命名文件时最常见的失败原因是文件被占用了。比如用户正在Windows资源管理器里预览某张图片或者图片被某个程序打开这时File.Move会抛出IOException。处理策略有两个一是捕获异常后记录日志让用户手动处理这几张二是在重命名前先尝试打开文件流测试一下是否被占用。我选择前者因为测试占用也会有竞态问题不如直接尝试重命名失败就记录。另外要提醒一点重命名原文件会让原来的顺序变乱。如果处理完后发现结果不理想想恢复原来的文件名那就麻烦了。所以稳妥起见处理前先备份原文件名列表可以通过写一个“处理清单”日志来实现。我在工具里加了一个配置项开启后每次处理都会生成一个CSV文件记录“原文件名, 新文件名”万一要回滚还能查。5.4 识别结果的UI展示批量处理时用户最关心的几个信息当前处理到第几张、每张处理成功还是失败、失败的提示是什么。我把这些信息放在右侧的日志文本框里同时用进度条展示整体进度。日志文本框用TextBox加IsReadOnlyTrue追加日志时用Dispatcher.BeginInvoke切回UI线程更新private void AppendLog(string message) { Dispatcher.BeginInvoke(new Action(() { TxtLog.AppendText(${DateTime.Now:HH:mm:ss} {message}\r\n); TxtLog.ScrollToEnd(); })); }为什么需要Dispatcher因为批量处理是在异步方法里执行的可能跑在线程池线程上直接操作UI控件会抛异常。这一点新手经常踩坑写了很久发现界面不更新原因就在这里。6. 常见问题与排查技巧实录6.1 识别率低下的几个原因用了腾讯云OCR之后如果识别率还是很低大概率不是API的问题而是截图质量的问题。我整理了一个排查顺序选区是否准确区域框大了把无关文字也框进来识别结果就会混入多余字符。区域框小了文字被截断识别也不准。图片是否正放如果图片旋转了90度或180度OCR识别效果会急剧下降。可以先在预览区人工确认图片方向。图片是否模糊如果原图分辨率低或者文字周围有噪点可以先尝试将裁剪区域放大1.2倍再识别。文字颜色与背景对比度深色背景浅色文字有时候识别不好可以在裁剪后先灰度化、二值化处理。不过对于大多数正常的截图和扫描件腾讯云OCR的识别率都很高基本能做到99%以上。6.2 区域坐标偏移的定位思路很多用户遇到的问题不是识别不出来而是识别的区域不对。坐标偏移的典型表现是在预览区圈了A区域程序实际读取的是B区域的内容。这种情况基本都是坐标换算出了问题。排查思路很简单在ProcessSingleFileAsync里把裁剪出来的子图保存成临时文件看看程序到底裁剪了哪个区域。File.WriteAllBytes(debug_crop.png, croppedBytes);这一条调试语句能解决90%的坐标问题。定位到是换算错误后回到GetPixelRegion方法重新检查缩放比例。最常见的原因是忘了考虑StretchUniform下图片并没有占满整个容器实际显示区域比容器小。6.3 API调用常见错误速查我用腾讯云OCR接口时遇到过不少报错整理成下表方便排查错误代码含义解决办法AuthFailure.SignatureFailure签名计算错误检查SecretId/SecretKey是否配置正确检查签名算法是否漏了步骤FailedOperation.ImageDecodeFailed图片无法解码检查裁剪后保存的格式是否为PNG/JPEG确认图片字节数组有效RequestLimitExceeded请求频率超限批量处理时增加延迟比如每张图片之间Sleep 200ms免费额度用完后也会报这个错ResourceNotFound服务未开通去腾讯云控制台开通OCR服务再试UnsupportedOperation不支持的操作检查接口Action和Version字段是否拼写正确其中RequestLimitExceeded最常见尤其当你一次性处理几百张图片时。我后来在循环里加了个小延迟大大降低了触发概率。6.4 程序启动时的其他坑最后说几个和OCR本身无关、但实际使用中一定会遇到的问题。如果你的图片路径包含中文裁剪和重命名时要注意编码问题。幸运的是.NET 6下的File系列API对UTF-8的支持很好基本不会出乱码。另外如果你要用这个工具处理非常大的图片比如8000×6000的扫描件裁剪时内存会飙升。建议在JPG解码时先降低分辨率或者直接用原图限制处理避免内存溢出。还有一点是目标目录的权限。如果程序是被管理员权限运行的而图片在普通用户目录下File.Move可能会因为权限不一致失败。我处理这类情况的办法是把整个处理逻辑包在try/catch里不管什么原因失败日志里都会记录明确原因用户不会一头雾水。7. 这个工具还能往哪些方向扩展做完这个工具之后我实际又加了不少实用功能。顺着这个方向你能扩展出很多变体一是把“区域识别改名”扩展成“区域识别分类”。比如识别出图片上的部门名称或项目代码然后自动移动到对应文件夹。改个名只需要在重命名那里多加一步创建目录和移动文件。二是支持多区域识别。有些图片的编号信息分布在左上角和右下角可以同时框两个区域识别结果拼接成一个文件名。实现上并不难把选区信息从单个矩形改成矩形数组就行。三是加一个“预览重命名结果”的模式。先识别所有图片把结果展示为“原文件名 → 新文件名”的表格用户手动勾选或修改后再执行改名。这样更安全适合图片量不大但对准确性要求很高的场景。四是把结果导出为Excel清单。我可以让工具在批量处理结束后输出一个包含图片路径、识别文本、重命名结果的CSV文件方便后续归档和审核。这个功能对财务和档案岗位特别有用因为他们对“留痕”要求很高。就我个人而言这个工具最让我满意的地方不是技术有多高深而是真正解决了一个反复出现的实际问题。它涉及的WPF界面、坐标换算、API签名这些知识点单独拎出来都是很常见的开发需求组合起来就变成了一个能省下大量重复劳动的小产品。如果你也经常被这种“没技术含量但特别费时间”的任务困扰不妨照着这个方案自己做一个踩一遍坑之后你会发现自己在桌面应用开发上的理解也会上一个台阶。