ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于WPF与腾讯云OCR的批量图片区域识别改名工具

基于WPF与腾讯云OCR的批量图片区域识别改名工具 你是不是也有一堆 JPG 图片文件名全是“IMG_20231025_093021.jpg”这种想找某一张图只能一张张点开预览我之前在整理扫描合同和产品截图时被这个问题折腾得够呛后来干脆花两个晚上做了一个 WPF 小工具先框住图片上要识别的区域程序自动调用腾讯云 OCR 把区域里的文字读出来再直接用这段文字把文件改名。今天就把完整思路、代码细节和踩坑记录分享一下有 C# 基础或者正在做 WPF 相关工具的朋友可以直接参考。这个工具解决的核心问题很明确批量、区域、自动识别、自动重命名。不是整张图一股脑识别而是只需要图片里某一块区域的文字。典型场景包括扫描件里的合同编号、票据里的发票号码、商品截图上的型号、图纸角落里的图号。这类文件只要名字里有这些关键信息后面检索起来会轻松很多。1. 需求拆解与技术选型为什么是 WPF 腾讯 OCR1.1 拆解“批量图片区域识别改名”这个需求实现之前先别急着写代码把需求拆开看。“批量图片区域识别改名”这句话里有四个关键点批量要处理的是文件夹里几十上百张图片单张识别用手机就够没必要做工具区域不是识别整张图片而是只读取图片里某个矩形区域内的文字。这是这个需求最容易被忽视、也最容易做错的地方识别要用 OCR 引擎把图片中的文字转换成字符串改名把识别出来的文字清洗成合法文件名替换掉原来的文件名保留 jpg 扩展名。如果把这四个点拆成独立模块整个程序的架构就清楚了一个负责文件遍历一个负责图片处理和区域裁剪一个负责 OCR 调用一个负责重命名和冲突处理剩下的就是 UI 交互。模块化拆分是这类小工具最值得坚持的原则后面不管替换 OCR 服务还是调整命名规则改动成本都很低。还有一个点很容易被忽略区域识别不是 OCR 服务提供的功能。常见的云 OCR 接口基本都是整图识别所谓“区域识别”本质上是先按用户框选的坐标把图片裁剪出来再把裁剪后的局部图片送给 OCR。理解这一点整个流程就顺了。1.2 为什么是 WPF而不是 WinForms 或 Web选 WPF 有几个实际原因不是因为它“新”而是因为它适合这类桌面小工具。第一WPF 的 UI 布局和样式定制能力比 WinForms 强太多了。这个工具需要左右两栏布局左边是文件列表右边是图片预览还要在预览图上拖拽画矩形框。WPF 里用 Canvas 加上鼠标事件就能做界面写出来也顺眼。WinForms 也能做但坐标换算和自绘逻辑要手动处理很多见效慢。第二这个工具需要读本地文件系统和文件管理器交互频繁。做成桌面程序比做成 Web 服务更直接。当然 Electron 也能做但为了一个几十 MB 的小工具引入整个 Chromium 运行时个人觉得不划算装个 100 多 MB 的东西只为了改图片名有点杀鸡用牛刀。第三WPF 的数据绑定体系适合这类“列表 状态更新”的交互。图片列表里的每一条记录有文件名、识别结果、处理状态用 ObservableCollection 加属性通知就能轻松做到界面实时刷新。用 WinForms 做这种列表刷新要手动操作 DataGridView代码会啰嗦不少。不过 WPF 有一个学习门槛XAML 和绑定语法。如果你之前只用过 WinForms可以直接用 Code-Behind 方式写不强制 MVVM 模式。工具类程序先跑起来比模式重要。1.3 OCR 方案对比腾讯云、百度、Tesseract、PaddleOCROCR 引擎是整个方案的灵魂选型时我认真对比了几种方案。方案部署方式识别率开发成本费用腾讯云通用印刷体 OCR云端 API印刷体识别率高中文支持好低官方 SDK 完善有免费额度超出后按量计费百度 OCR云端 API印刷体表现也很好低有免费额度接口文档成熟Tesseract本地开源对清晰印刷体可以复杂排版较弱中要处理语言包和预处理免费PaddleOCR本地开源精度不错尤其中文场景中高环境配置稍复杂免费我个人最终选了腾讯云原因有三点一是官方 .NET SDK 比较完善照着文档就能接入二是通用印刷体识别对中文和数字的识别率稳定三是免费额度对个人整理图片基本够用。Tesseract 我也试过最大的问题是它“任性”对白底黑字的清晰扫描件表现不错但遇到图片背景复杂、文字带颜色或者有轻微倾斜识别结果就变得离谱。PaddleOCR 本地部署效果确实好但需要 Python 环境或者 VC 运行库做 GUI 集成时打包发布比较折腾。如果纯粹想快速解决问题云端 OCR 是最省力的选择。2. 准备工作账号密钥、WPF 工程与整体结构2.1 开通腾讯云 OCR 与获取密钥使用腾讯云 OCR 需要有一个腾讯云账号然后在控制台搜索“文字识别”开通通用印刷体识别服务。开通后需要在访问管理里创建一对 API 密钥也就是 SecretId 和 SecretKey这两个字符串类似账号密码调用接口时用来做身份签名。创建密钥时有几个建议建议使用子账号密钥并只授予 OCR 相关权限避免主账号密钥泄露带来的风险不要把这些密钥硬编码在代码里。工具自己用无所谓但如果要发给别人至少做成配置文件地域参数一般填 ap-guangzhou腾讯云 OCR 接口签名时都需要带地域信息。密钥这东西一旦泄露别人就能用你的额度调用接口产生费用。所以本地存放时要谨慎不要随手传到 GitHub 仓库。2.2 创建 WPF 工程与 NuGet 依赖开发环境建议使用 Visual Studio 2022.NET 版本直接用 .NET 6 或 .NET 8。在 VS 里新建 WPF 项目目标框架选 net6.0-windows 或 net8.0-windows 都行。命令行也可以创建dotnet new wpf -n BatchImageOcrRename需要安装的 NuGet 包有两个腾讯云 OCR 的官方 SDK 包和一个 JSON 解析包。dotnet add package TencentCloudSDK.Ocr dotnet add package Newtonsoft.JsonTencentCloudSDK.Ocr 这个包会依赖 TencentCloudSDK.Common安装的时候会自动带上来不需要手动处理。这里提醒一下SDK 包的版本号在持续更新安装时建议使用最新稳定版。如果项目要发给别人运行还需要确保目标机器装了对应版本的 .NET 桌面运行时或者直接发布成自包含单文件这样对方机器不需要额外装环境。2.3 目录结构与核心类的职责一个小工具不需要复杂的架构但至少要让每个类只做一件事。我实际用的项目结构是这样BatchImageOcrRename/ ├── Models/ │ └── ImageItem.cs // 图片信息包含路径、预览、状态、识别结果 ├── Services/ │ ├── TencentOcrService.cs // 腾讯云 OCR 接口封装 │ ├── ImageCropService.cs // 图片加载、裁剪、Base64 转换 │ └── RenameService.cs // 文件名清洗、重命名、冲突处理 ├── MainWindow.xaml // 主界面 ├── MainWindow.xaml.cs // 窗口逻辑和事件处理 └── App.xaml解耦的收益在真机调试时会体现得很明显。比如最开始 I 用的是 Tesseract后来换腾讯云时只需要改 TencentOcrService 的内部实现界面和重命名逻辑完全不用动。ImageItem 这个模型用来承载文件列表里每一行的状态代码大致是这样public class ImageItem : INotifyPropertyChanged { public string FilePath { get; set; } public string OldName Path.GetFileName(FilePath); private string _newName; public string NewName { get _newName; set { _newName value; OnPropertyChanged(); } } private string _status; public string Status { get _status; set { _status value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string name null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }3. 界面与交互框选区域是核心体验3.1 主窗口布局三块区域怎么排这个工具的界面不需要花哨但布局要合理。我采用了三栏式结构从上到下、从左到右依次是左侧文件夹路径选择、加载按钮、图片列表图片列表上显示文件名和识别状态右侧上半部分图片预览区域用户在这里用鼠标拖拽框选识别区域右侧下半部分命名规则设置、识别结果预览、开始处理按钮和处理日志。XAML 布局大致如下Grid Grid.ColumnDefinitions ColumnDefinition Width320/ ColumnDefinition Width*/ /Grid.ColumnDefinitions !-- 左侧文件列表 -- DockPanel Grid.Column0 Margin10 StackPanel DockPanel.DockTop TextBox x:NameFolderPathTextBox Margin0,0,0,8/ Button x:NameLoadButton Content加载图片 ClickLoadButton_Click Margin0,0,0,8/ /StackPanel ListBox x:NameImageListBox DisplayMemberPathOldName/ /DockPanel !-- 右侧预览和设置 -- Grid Grid.Column1 Margin10 Grid.RowDefinitions RowDefinition Height*/ RowDefinition HeightAuto/ /Grid.RowDefinitions Border Grid.Row0 BorderBrushGray BorderThickness1 Grid x:NamePreviewHost Image x:NamePreviewImage StretchUniform/ Canvas x:NameOverlayCanvas BackgroundTransparent/ /Grid /Border StackPanel Grid.Row1 OrientationHorizontal Margin0,10,0,0 Button x:NameStartButton Content开始识别并改名 ClickStartButton_Click Width140 Margin0,0,10,0/ TextBlock x:NameLogTextBlock VerticalAlignmentCenter/ /StackPanel /Grid /Grid需要注意的一个细节是预览图片容器。图片使用StretchUniform时可能留有空白边这时候如果直接拿鼠标坐标对应图片像素坐标位置会偏。必须换算。3.2 实现鼠标框选并把坐标映射到原图鼠标框选区域是整个工具交互里的核心功能。实现思路并不复杂在图片上放一个透明 Canvas监听鼠标按下、移动、松开三个事件动态画一个矩形最后把这个矩形从屏幕坐标换算成图片原始像素坐标。先看矩形的绘制代码private Point _startPoint; private Rectangle _selectionRect; private void OverlayCanvas_MouseDown(object sender, MouseButtonEventArgs e) { _startPoint e.GetPosition(OverlayCanvas); _selectionRect new Rectangle { Stroke Brushes.Red, StrokeThickness 1.5, StrokeDashArray new DoubleCollection { 4, 2 } }; Canvas.SetLeft(_selectionRect, _startPoint.X); Canvas.SetTop(_selectionRect, _startPoint.Y); OverlayCanvas.Children.Add(_selectionRect); } private void OverlayCanvas_MouseMove(object sender, MouseEventArgs e) { if (_selectionRect null) return; var current e.GetPosition(OverlayCanvas); double x Math.Min(_startPoint.X, current.X); double y Math.Min(_startPoint.Y, current.Y); double w Math.Abs(_startPoint.X - current.X); double h Math.Abs(_startPoint.Y - current.Y); _selectionRect.Width w; _selectionRect.Height h; Canvas.SetLeft(_selectionRect, x); Canvas.SetTop(_selectionRect, y); } private void OverlayCanvas_MouseUp(object sender, MouseButtonEventArgs e) { if (_selectionRect null) return; // 把画布坐标换算成图片原始像素坐标 var pos new Rect( Canvas.GetLeft(_selectionRect), Canvas.GetTop(_selectionRect), _selectionRect.Width, _selectionRect.Height ); double scaleX PreviewImage.Source.Width / PreviewImage.ActualWidth; double scaleY PreviewImage.Source.Height / PreviewImage.ActualHeight; _cropRect new Rect( pos.X * scaleX, pos.Y * scaleY, pos.Width * scaleX, pos.Height * scaleY ); _selectionRect null; Log($已框选区域X{_cropRect.X:F0}, Y{_cropRect.Y:F0}, W{_cropRect.Width:F0}, H{_cropRect.Height:F0}); }这里的PreviewImage.Source.Width是图片原始像素宽PreviewImage.ActualWidth是控件上实际显示的宽两者相除就是缩放比例。如果图片被 Uniform 缩放后有留白更精确的做法是把图片的 ActualWidth 替换成图片在容器中实际占用的显示宽度这部分可以结合Arrange时的事件来算但小型工具可以粗略处理只要截图区域有一定冗余OCR 依然能识别。坐标换算这块是新人最容易踩的坑很多人的第一版工具识别结果总是不对就是因为这里没有做比例换算。框选的时候看着框住了文字实际传给 OCR 的裁剪区域却是错误位置。3.3 保存/复用识别区域坐标一次性工具可以直接把区域坐标写死但更实用的做法是让区域坐标可保存、可复用。因为批量处理同类型图片时每张图片需要识别的区域位置基本固定只有内容不同。我建议把坐标直接序列化成 JSON放在程序目录下public class RegionConfig { public double X { get; set; } public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } }点击“保存区域”按钮就把当前框选的区域写入 region.json程序启动时如果发现这个文件自动加载并绘制区域。实测下来对于同一批次的扫描件这个功能可以省掉重复框选的麻烦。4. 核心逻辑腾讯云 OCR 接入与批量重命名实现4.1 官方 SDK 调用通用印刷体识别腾讯云 OCR 的 .NET SDK 封装了签名鉴权过程不需要自己写签名算法。接入步骤实际上是三步创建客户端、构造请求、发起识别。先看一个最简单的单张图片识别代码using TencentCloud.Common; using TencentCloud.Ocr.V20181119; using TencentCloud.Ocr.V20181119.Models; public class TencentOcrService { private readonly OcrClient _client; public TencentOcrService(string secretId, string secretKey, string region ap-guangzhou) { Credential credential new Credential { SecretId secretId, SecretKey secretKey }; _client new OcrClient(credential, region); } public async Taskstring RecognizeAsync(string imageBase64) { var request new GeneralBasicOCRRequest(); request.ImageBase64 imageBase64; var response await _client.GeneralBasicOCR(request); if (response.TextDetections ! null response.TextDetections.Length 0) { return string.Join( , response.TextDetections.Select(d d.DetectedText)); } return string.Empty; } }GeneralBasicOCR是通用印刷体识别接口适合识别印刷出来的文字比如打印的合同、发票、屏幕截图里的文字。响应里的TextDetections是一个数组每一项包含识别出的文本和位置信息。如果你框选的区域里有多个文本行可以通过换行或者空格拼接命名时再统一清洗。证书部分有个细节需要注意如果你在真实服务里使用建议通过Environment变量或者配置文件读取密钥不要直接写在代码里。我这个工具是自用密钥写在 appsettings.json 里并且加了忽略提交的处理。4.2 裁剪图片区域并转 Base64调用 OCR 之前需要把框选的区域从原始图片中裁剪出来。使用 System.Drawing 库就可以完成这个库在 Windows 下做图片处理足够用。private string CropToBase64(string imagePath, Rect cropRect) { using (var bitmap new Bitmap(imagePath)) { // 防止区域越界 int x Math.Max(0, (int)cropRect.X); int y Math.Max(0, (int)cropRect.Y); int width Math.Min(bitmap.Width - x, (int)cropRect.Width); int height Math.Min(bitmap.Height - y, (int)cropRect.Height); width Math.Max(1, width); height Math.Max(1, height); using (var cropBitmap bitmap.Clone(new Rectangle(x, y, width, height), bitmap.PixelFormat)) using (var ms new MemoryStream()) { cropBitmap.Save(ms, ImageFormat.Jpeg); return Convert.ToBase64String(ms.ToArray()); } } }这里包含了几个实际操作中会踩到的坑坐标越界是必然会出现的情况框选区域可能超出图片边界裁剪前必须 clampClone之后得到的新 Bitmap 要记得 Dispose否则文件句柄被占用批量处理上百张图片后会出现“文件被占用”错误保存成 MemoryStream 再转 Base64是为了避免把临时文件写到磁盘。直接用文件路径也可以让 SDK 内部读取但先用 Base64 可以更明确地控制图片格式和质量。如果原始图片太大比如扫描件有 10MB直接转 Base64 会超过接口限制。腾讯云通用印刷体识别要求图片 Base64 后不超过 7MB且图片尺寸不能太大。对于超大图片可以先压缩再识别using (var resized new Bitmap(cropBitmap, new Size(targetWidth, targetHeight))) { resized.Save(ms, ImageFormat.Jpeg); }压缩比例按原图宽高等比缩小确保长边不超过 4000 像素基本就不会触发接口限制。4.3 识别文本清洗与文件名冲突处理OCR 识别出来的文本不能直接用做文件名需要先清洗。Windows 文件名不允许包含这些字符\ / : * ? |此外OCR 结果里可能混入空格、换行、特殊符号还有全角半角混用的情况。我的清洗函数如下public static string SanitizeFileName(string raw) { if (string.IsNullOrWhiteSpace(raw)) return 未识别; string invalidChars new string(Path.GetInvalidFileNameChars()); string result new string(raw .Where(c !invalidChars.Contains(c) c ! \r c ! \n) .ToArray()); // 去掉多余空格 result result.Trim(); result Regex.Replace(result, \s, _); // 去掉文件名里容易引起混淆的前后点 result result.Trim(.); if (string.IsNullOrWhiteSpace(result)) return 未识别; // 限制长度NTFS 单文件名最大 255 字符 if (result.Length 80) result result.Substring(0, 80); return result; }长度限制我故意设置得比系统上限小很多设为 80 个字符。原因很实际文件名太长在资源管理器里根本看不全完全失去命名的意义。如果你要保留更多信息可以到 120 左右再多我就不建议了。重名冲突处理也很关键。批量处理时两张图可能识别出一模一样的文字。如果直接用识别结果改名后面的文件会覆盖前面的文件这是毁灭性的。我采用追加序号的方式public static string GetUniqueFilePath(string dir, string fileName) { string baseName Path.GetFileNameWithoutExtension(fileName); string ext Path.GetExtension(fileName); string candidate Path.Combine(dir, fileName); int index 1; while (File.Exists(candidate)) { string suffix $_{index}; candidate Path.Combine(dir, ${baseName}{suffix}{ext}); index; } return candidate; }这一步一定不能省。哪怕你确认这批图片识别结果不会重名也建议保留这个逻辑因为总会有意外。4.4 批量处理的异步流程与并发控制批量处理的核心逻辑是遍历文件列表对每个文件依次执行加载图片 → 裁剪 → Base64 → 调用 OCR → 清洗 → 重命名。这里有两个性能要点第一异步处理。调用云端 OCR 是网络 IO 操作如果同步执行UI 线程会被卡住窗口会变成“未响应”。处理函数要写成 async并配合 await 调用private async Task ProcessOneAsync(ImageItem item) { item.Status 识别中...; try { string base64 CropToBase64(item.FilePath, _cropRect); string recognized await _ocrService.RecognizeAsync(base64); string cleanName RenameService.SanitizeFileName(recognized); item.NewName cleanName .jpg; string dir Path.GetDirectoryName(item.FilePath); string target RenameService.GetUniqueFilePath(dir, item.NewName); File.Move(item.FilePath, target); item.FilePath target; item.Status 完成; } catch (Exception ex) { item.Status 失败: ex.Message; } }第二并发控制。批量上百张图片时逐张串行虽然稳定但比较慢。可以引入 SemaphoreSlim 控制并发数避免同时发起几十个请求导致接口限流或本机内存暴涨private static SemaphoreSlim _semaphore new SemaphoreSlim(3); private async Task ProcessOneAsync(ImageItem item) { await _semaphore.WaitAsync(); try { // 同上 } finally { _semaphore.Release(); } }并发数控制在 3 到 5 之间比较合适。云端 OCR 接口有 QPS 限制并发太高容易触发限流并发太低又浪费网络带宽。实测 3 个并发处理 200 张图片每张耗时约 1 秒总耗时比串行快了一半以上。批量循环里还需要一个“跳过”机制。如果处理到一半程序崩了或者某个文件识别失败重跑时应该可以跳过已经改名的图片。最简单的做法是看文件名如果文件名不再是原始模式比如不再是 IMG_ 开头就跳过。更严谨的做法是记录处理日志失败的文件集中展示方便二次处理。5. 常见问题与排查技巧实录5.1 接口报错速查表实际开发中总会遇到各种接口报错这里整理了最常见的几个和排查思路。错误信息原因解决办法AuthFailure.SignatureFailure密钥错误或签名失败检查 SecretId 和 SecretKey 是否复制完整、是否有空格AuthFailure.SecretIdNotFoundSecretId 不存在去访问管理控制台确认密钥状态FailedOperation.ImageDecodeFailed图片无法解码确认 Base64 字符串前没有 data:image/jpg;base64, 前缀且图片不是损坏文件FailedOperation.ImageTooLarge图片超过限制压缩图片或裁剪后控制尺寸ResourceNotFound服务未开通或地域错误确认账号已开通对应 OCR 服务检查 region 参数RequestLimitExceeded超出 QPS 限制降低并发数增加重试等待时间有一个特别容易被坑的点从浏览器里复制的图片或者接口文档示例里拿到的 Base64字符串前面往往带着data:image/jpg;base64,这一大段前缀。直接把这个字符串塞给 OCR 接口一定报解码失败。在转 Base64 的时候需要把这个前缀去掉。if (imageBase64.Contains(,)) { imageBase64 imageBase64.Substring(imageBase64.IndexOf(,) 1); }5.2 识别不准的优化方向如果 OCR 识别出来的文字经常出错先别急着换服务按下面几步排查很多问题都能解决。一检查框选区域是否精准。区域范围太小文字不完整识别率会显著下降区域范围太大把旁边的无关文字也框进去了识别结果会多出很多噪声。我的经验是框选时上下左右留出 5 到 10 像素的冗余让文字整体处于区域中央。二注意图片方向。横版图片里的文字如果旋转了 90 度或者 180 度识别率会大打折扣。腾讯云通用印刷体识别接口可以配合旋转校正参数或者自己在程序里加一个旋转 90 度的按钮手动纠偏后再识别。三图片太暗或者对比度低时可以先做灰度化和提高对比度。System.Drawing 里处理灰度图比较繁琐但可以用最简单的 ColorMatrix 实现。对于白底黑字的扫描件灰度化之后识别率有明显提升。四针对同一批图片可以保存多个识别区域然后拼接结果。比如一张图片左上角是合同编号右下角是日期定义两个区域分别识别后再用下划线拼接成最终文件名。这个扩展非常实用我在第二版工具里加了这个功能。5.3 工具实用性与扩展建议这个工具虽然是围绕“图片改名”这个需求开发的但把 OCR 识别出来的文本用于任何场景都成立。比如扩展成“批量提取图片指定区域文字到 Excel”只需要把重命名逻辑替换成写表格也可以扩展成“识别快递单号并自动归类”识别出单号后自动把文件移动到对应文件夹。核心的裁剪和 OCR 模块不用动改的是下游的用途。还有一个建议正式批量处理前先拿三五张图片试跑一遍肉眼确认识别结果符合预期再点“全部处理”。这个习惯能帮你避免成批文件被错误名字污染。错误命名的文件要再改回来代价比多花一分钟试跑大得多。我最早用 Tesseract 本地识别做了一版后来换到腾讯云 OCR 后最大的感受是识别精度上了一个台阶而且省掉了大量图像预处理的琐碎工作。云 API 虽然要联网但对于整理图片这种场景完全够用。如果你要处理的是涉密或者敏感文件建议换 PaddleOCR 做全本地处理代码结构上只需要替换 TencentOcrService 这一个类其他模块完全不需要动。最后再分享一个小技巧识别结果清洗的时候可以针对你的业务场景加一个“关键词白名单”或“关键词优先级”。比如你只关心文件名里的日期那就在清洗时优先提取 20xx 开头的八位数字而不是把所有文字都塞进文件名。这个小改动会让最终的文件名干净很多也更好用。
返回列表