ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

基于WPF与腾讯云OCR的批量图片区域识别重命名工具实现 做这个“批量图片区域识别改名”工具其实是因为我自己遇到一个很折腾的场景手头攒了一批产品实拍图每张图右下角都盖着一行编号/日期/型号信息我要按这个编号归档但Windows资源管理器里压根没有“按图片内容重命名”这种功能更别说是针对某个固定区域的文字识别了。手动一张张看、一张张改几百张下来眼睛直接废掉。所以我就用WPF做界面接了腾讯云OCR的API做了一个能在指定区域里自动识别文字、自动改文件名的Windows小工具。这篇文章不是讲“哪个软件好用”而是把完整的技术方案、踩坑点、关键代码思路都写出来。适合两类朋友一类是手里积压了大量带固定文字区域的图片要做归档另一类是准备在WPF项目里接入OCR、把云API用起来的.NET桌面开发初学者。看完你至少能知道为什么选WPF而不是其他技术栈、腾讯云OCR的接入方式、批量任务的并发与异常处理、以及一批我实际踩过的坑。1. 方案设计为什么是WPF加腾讯云OCR1.1 选型背后的真实考量先把需求拆开你会发现它其实由三件事组成批量读文件、对图片指定区域做OCR识别、按识别结果重命名。这三件事用命令行也能做用Python脚本也能做为什么我选了WPF一个很重要的原因是“可视化反馈”。批量改名的场景里图片内容、识别结果、重命名目标这三者是要对照着看的尤其是识别出错的时候你得一眼看出是哪张图出了问题。WPF的界面天然适合做这种“人机交互加预览确认”的工具比一行行黑乎乎的日志直观太多。另外Windows环境下WPF生成的是原生桌面程序双击就能跑不需要解释器也不需要在目标机器上配Python环境——这对一个给同事、给其他部门用的“小工具”来说非常重要。至于OCR选型这个纠结过一段时间。第一反应是本地OCR比如Tesseract搜了相关实现也试过但我这批图是带不同字号的印刷体编号Tesseract对低分辨率图的效果并不稳定而且需要额外处理语言包、图片预处理工程量不小。后来看到很多人在讨论腾讯云OCR识别准度确实高通用印刷体接口能直接返回文字位置和置信度并且有一段时间的免费额度按量计费对于批量任务来说成本也可控。所以就定了WPF负责界面和文件处理腾讯云OCR负责文字识别功能边界非常清楚。提醒一句本文讨论的是通过腾讯云提供的官方产品接口完成文字识别功能这是国内常见的云服务产品身份验证与密钥管理请按官方文档来别把自己的密钥硬编码到界面里。1.2 整体工作流程拆解这个工具的核心循环其实不复杂但细节不少用户选择一个文件夹工具列出所有.png/.jpg/.jpeg/.bmp图片文件。用户设置“识别区域”也就是这批图片上统一出现的文字所在位置。这个区域可以手动拖拽框选也可以填坐标数值。点击“开始识别”WPF程序按顺序或按设定的并发数读取图片把指定区域裁剪出来转成Base64编码调用腾讯云API。拿到OCR返回结果后程序提取出置信度最高的文本行做非法字符过滤、长度截断、重名处理最终得到一个新的文件名。用户可以在界面中预览“原文件名 - 新文件名”的对应关系也可以直接勾选自动执行重命名。日志面板记录每一步的耗时、成功与失败原因。整体上就是一个标准的“批处理管道”枚举、预处理、请求、解析、写文件。后面的章节会按这个流程把每一步的具体实现讲透。1.3 为什么不建议直接找现成闭源软件你可能会想网上不是有很多“批量重命名软件”吗有的还自带OCR为什么还要自己做我个人的体会是现成软件的识别区域和规则很难自定义到“区域识别”这个粒度。大多数重命名工具的OCR功能是全图识别但全图识别的问题在于图片上可能有多个干扰文字比如水印、拍摄日期、品牌Logo识别出来的文本你会拿不准哪一段是真正可以用来命名的。而我这个项目从一开始就锁定了“固定区域”——因为同一个批次归档的图片它的关键文字大概率出现在固定位置这种情况下裁剪区域后识别准确率和结果稳定性会比整图识别高一大截。自己写方案就是图这个“精准控制”和后续扩展空间比如加正则规则加多区域拼接命名。2. 界面设计与交互体验2.1 主界面要解决什么问题WPF界面的设计原则我总结成一句话把额外学习成本压到最低核心状态一眼可见。所以主界面分了三个区块左侧是文件夹路径和参数设置中间是图片列表和识别结果预览底部是运行日志。参数设置区其实就几样东西文件夹路径输入框和“浏览”按钮、Region识别区域的坐标X、Y、宽度、高度、以及一个“从图片预览中框选区域”的按钮。做这个按钮的原因是手填坐标你根本不知道填完是在图片什么位置不如直接打开一张示例图用鼠标拖个框程序自动把坐标回填到设置项里。2.2 WPF中的画布坐标与图片裁剪的换算这里有个关键的WPF技术点画布坐标系统和图片原始像素坐标不是一回事。WPF的Image控件默认会做缩放图片在屏幕上显示的区域可能比原始图片小鼠标拖出来的矩形是相对屏幕显示的坐标而在调用腾讯云API之前我们要的是“原始图片上的像素坐标”。所以框选后必须做一个换算记录图片原始尺寸原始宽度、原始高度。记录图片在界面上的显示尺寸控件的ActualWidth、ActualHeight。代码中用一个缩放比例换算scaleX 原始宽度 / ActualWidthscaleY 原始高度 / ActualHeight。把鼠标框选的坐标乘上比例得到原始的裁剪矩形。如果忽略这一步你会遇到一种很诡异的问题框选的位置在界面上看着是正确的但识别出来文字对不上。实际上就是WPF的布局惯性让你忘了控件缩放这回事。2.3 展示层用DataGrid还是自定义ItemsControl文件列表和“原文件名 - 新文件名”这种对应关系我之前第一反应是用DataGrid也确实绑定了DataTable数据源跑起来没问题但后面做多选操作比如勾选要改哪些文件就不太顺手了。后来我改成了ListView配合GridView列或者更省事的是用ListBox ItemTemplate因为我们要展示的信息其实就三列“原文件名”“识别文本”“新文件名”。如果你也要做这个工具我给的建议是不要急着上DataGrid先用一个可视化的列表方案把核心逻辑跑通再回头优化展示层。下面这个XAML是一个简化版本的列表模板每条记录一个CheckBox加两行文本信息看起来反而比表格少了很多割裂感ListBox x:NameFileListBox ItemsSource{Binding FileItems} ListBox.ItemTemplate DataTemplate Border Margin4 Padding6 BorderBrush#EEEEEE BorderThickness1 CornerRadius4 StackPanel CheckBox IsChecked{Binding IsSelected} Content{Binding OldName} / TextBlock Text{Binding NewName} Foreground#666666 / /StackPanel /Border /DataTemplate /ListBox.ItemTemplate /ListBox2.4 进度展示与取消机制批量任务最怕的就是跑到一半卡住所以WPF客户端里必须有大进度条和取消按钮。这里我用了两个进度信息一个是“已处理文件数/总数”另一个是“当前正在识别的文件名”。注意WPF的合规做法是通过数据绑定来更新这些值不要直接在非UI线程里去改控件的Text属性。我会在后文“线程和异步”那一段专门说明。3. 核心功能实现与原理讲解3.1 文件遍历与筛选文件遍历没什么好说的但有两个细节值得写出来。第一不要只遍历顶层目录。我给工具加了一个复选框“包含子目录”默认是勾选的这也符合归档场景的实际需要。子目录遍历用Directory.EnumerateFiles配合SearchOption.AllDirectories就是一行代码但它会引入一个新的问题重命名后如果文件所在的目录名也要跟着变你的逻辑会变得复杂。所以我的处理是只重命名文件名不改文件所属目录。虽然偶尔会出现目录名和文件内容不一致的情况但至少文件不会因为在“遍历的同时改名”而被重复读取或漏读。第二过滤条件要基于扩展名。这个看起来是小事但是Windows上图片扩展名大小写不一千万别用String.EndsWith(.JPG)这种写死大小写的方式统一用可枚举的方式去判断。下面是一段我实际用的过滤器private static readonly string[] ImageExtensions { .jpg, .jpeg, .png, .bmp }; IEnumerablestring GetImageFiles(string folderPath, bool searchChildren) { var option searchChildren ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateFiles(folderPath, *.*, option) .Where(file ImageExtensions.Contains(Path.GetExtension(file).ToLowerInvariant())); }3.2 图片裁剪与编码裁剪这一块我用的是System.Drawing命名空间下的Bitmap类。虽然微软现在推荐跨平台图像库但对于一个Windows桌面工具来说System.Drawing够用且接入成本最低。裁剪逻辑分三步用Bitmap.FromFile加载图片。用Bitmap.Clone(cropRect, PixelFormat.Format24bppRgb)裁出指定区域。用Image.Save保存到一张临时图片或者直接存成MemoryStream。裁剪后为什么要转成MemoryStream而不是直接保存临时文件因为这个工具的临时文件如果没清理干净会在文件夹里留一堆垃圾。所以我的做法是把裁剪出的Bitmap写进MemoryStream然后直接Convert.ToBase64String(ms.ToArray())这样全程在内存里完成不落地临时文件。但要注意如果一次批量处理几千张图内存里同时跑这么多数据GC压力会上去我还会在每个图片处理完以后主动Dispose掉Bitmap和Stream。一个容易踩的坑Bitmap.Clone裁剪时如果裁剪矩形超出了图片边界会直接抛异常。做区域识别时用户拖拽框选的时候稍微越界几个像素非常常见所以代码里必须加一个约束逻辑int rectX Math.Max(0, Math.Min(bmp.Width, cropRect.X)); int rectY Math.Max(0, Math.Min(bmp.Height, cropRect.Y)); int rectRight Math.Max(rectX, Math.Min(bmp.Width, cropRect.Right)); int rectBottom Math.Max(rectY, Math.Min(bmp.Height, cropRect.Bottom)); var safeRect new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY);3.3 腾讯云OCR接入识别区域为什么“裁剪后再识别”腾讯云OCR的接入步骤很简单开通对应产品、在控制台拿到SecretId和SecretKey然后写代码调用。国内云服务商有很多选腾讯云主要是因为它在我当时的测试集上识别率最高通用印刷体的返回结果里有DetectedText、Confidence、Polygon我只需要拿DetectedText即可。调用方式可以直接用腾讯云官方提供的.NET SDK也可以用HTTP方式自己做签名细化到代码层面用SDK反而更容易踩版本坑。如果你手头的项目是.NET 6以上建议直接引用官方NuGet包TencentCloudSDK一个调用大概长这样using TencentCloud.Common; using TencentCloud.Ocr.V20181119; using TencentCloud.Ocr.V20181119.Models; Credential credential new Credential { SecretId _secretId, SecretKey _secretKey }; OcrClient client new OcrClient(credential, ap-guangzhou); GeneralBasicOCRRequest request new GeneralBasicOCRRequest { ImageBase64 Convert.ToBase64String(croppedImageBytes) }; GeneralBasicOCRResponse response client.GeneralBasicOCRSync(request); double maxConfidence 0; string bestText string.Empty; foreach (var item in response.TextDetections) { if (item.Confidence maxConfidence !string.IsNullOrWhiteSpace(item.DetectedText)) { maxConfidence item.Confidence; bestText item.DetectedText; } }这里有一个核心设计问题我已经给了“识别区域”为什么OCR代码里不直接用整张图而是裁剪后再识别答案是精度和成本。精度上裁剪后画面上的干扰文字大幅减少OCR引擎不用在海报标题、水印、背景纹理之间做选择输出的文本基本就是区域内的那一行字。成本上腾讯云OCR是按图片大小和清晰度计费裁剪掉无关区域可以降低图片体积间接控制费用。所以设计上区域识别的“区域”一定要在调用OCR之前就生效而不是靠OCR结果里自己过滤。另一个容易被忽略的细节是腾讯云通用印刷体接口对图片大小的上限有限制图片Base64编码后不能太大。如果你在识别某些高清大图时出现“图片过大”之类的错误裁剪区域之外可以先做一次缩放/压缩处理再把图片传给接口。3.4 异步、并发与UI线程WPF中严禁在UI线程做耗时操作否则窗口会卡到“未响应”。我使用async/await把多张图片的批量识别做成可等待的任务。但要注意不建议几秒钟内发起几十个并发请求这很容易触发API频率限制。我实际试下来并发数控制在2到4之间最稳定超过4可能出现偶尔的网络超时或被限流。这样写的一个好处是让CPU密集型的图片裁剪和I/O密集型的网络请求交替执行既不会太慢也不会把接口打爆。private async Task ProcessOneFileAsync(string filePath, Rectangle region) { try { await Task.Run(() { using var bmp new Bitmap(filePath); using var cropped bmp.Clone(ClipRect(bmp.Width, bmp.Height, region), PixelFormat.Format24bppRgb); using var ms new MemoryStream(); cropped.Save(ms, ImageFormat.Jpeg); _imageBytes ms.ToArray(); }); string recognized await CallOcrAsync(_imageBytes); string newName BuildNewFileName(recognized); await Dispatcher.InvokeAsync(() UpdateUIItem(filePath, newName)); } catch (Exception ex) { await Dispatcher.InvokeAsync(() LogError(filePath, ex.Message)); } }注意上面代码里的Dispatcher.InvokeAsync它负责把UI更新的逻辑切回主线程执行。千万别试图在后台线程里直接改ObservableCollectionWPF会直接抛异常。3.5 重命名的安全性与边界处理识别出的文本不能直接当作文件名来用Windows文件名里有一批非法字符\/:*?|另外开头结尾不能有空格和点中间不能有换行。所以我在BuildNewFileName方法里做了这几个处理用正则把非法字符替换成_或直接移除。截断长度文件名最长255个字符实际建议控制在80以内否则在部分压缩软件里会导致问题。如果识别结果为空保留原文件名并在日志里标红方便人工查看。如果目标文件名已经存在在后面追加序号比如example_1.jpg避免覆盖原文件。这里还有一个小优化如果需要批量改名的文件都在同一个目录重命名前最好先构造一个“新旧文件名映射”先检查一遍会不会有重复目标而不是改完一个才发现两个文件映射到了同一个名字。我在工具里是双重保障先预检一遍如果发现重复就在新文件名后加时间戳或序号。4. 实操过程与核心环节实现4.1 编译运行前的准备要在自己的电脑上把这个项目跑起来你需要准备这几样东西Visual Studio 2022或者任意支持.NET 6/8的IDE加装“.NET桌面开发”工作负载。一个腾讯云的账号并开通对应OCR服务在控制台创建一个API密钥。一个测试图片目录里面至少放几张带统一区域文字的JPG图片。新建WPF工程后我推荐用.NET 6或.NET 8不用.NET Framework 4.8也不是不行只是新版SDK对异步、内存管理的支持更好而且官方SDK也是基于新版本做的适配。项目里需要添加的NuGet包就这么几个TencentCloudSDK、Newtonsoft.JsonSDK内部有依赖不过如果你日志直接用了还要另加。如果你的界面要用到一个简易的宿主窗口可以直接用默认的MainWindow不需要额外引入MVVM框架——这个工具的核心逻辑在“批处理”而非“架构优雅”所以我个人不建议非要在这种小工具上引Caliburn.Micro或Prism那会把简单问题复杂化。4.2 密钥配置与用户交互设计API密钥这种东西写死在代码里开源等于泄露。我做了一个简单的配置界面第一次运行程序时会引导用户填入SecretId和SecretKey并选择保存到本地配置文件。注意这个文件不要提交到代码仓库同时如果工具要分发给别人你要提醒他填自己的密钥而不是在程序里内置或者硬编码一个公共密钥。这里有一个非常容易困惑的问题腾讯云OCR有免费额度按量计费但调用密钥是个人账号下的权限凭据。如果你做的工具只是自己用直接在配置里填自己的即可。如果你的同事也想用一定要让他申请自己的账号密钥而不是大家公用一个因为一旦把密钥分享到公司的内部群、文档或者聊天记录里那些笔记、日志、导出文件就可能成为安全隐患。这一点在真实项目里必须认真对待。4.3 第一次完整运行的效果我第一次用这个工具跑了一批大约200张JPG图片每张图片尺寸大概1920x1080识别区域是右下角一个200x60的区域。批处理的结果是总耗时约4分钟平均每张1.2秒左右主要是网络请求的耗时。其中有184张识别出了和预期完全一致的编号文字识别成功率达92%。12张因为图片本身旋转、倾斜或文字被遮挡导致识别失败被保留原名并标红。4张识别到的文字中带有中英文混合情况这些走了额外的清理正则最后变成合法文件名。没有出现文件覆盖、路径不存在等问题。这个结果已经足够我用它做日常归档工具了毕竟全自动场景下能做到九成以上成功率剩下的人工看一眼日志非常轻松。偶尔有几张翘角、歪斜的图我会在原图上用画图工具旋转几度再重新归档。4.4 关于运行环境的注意事项这个工具依赖的是.NET桌面运行时本机装了.NET 6/8 SDK当然能直接跑。如果是分发给没有安装环境的人建议用Visual Studio的发布功能做自包含发布输出一个包含运行时的exe文件体积会在60MB以上但胜在对方机器上不需要装任何东西。另一个选项是框架依赖发布体积小很多但目标机器必须安装对应的.NET Desktop Runtime。我自己用的是自包含发布因为这类工具的典型使用场景是用户的办公电脑不一定有开发环境。5. 常见问题与排查技巧实录我把自己在开发和使用中遇到的典型问题做了一个速查表这些问题你在复现时大概率也会碰到问题现象可能原因解决办法拖动选择识别区域后识别结果完全不对没有做界面坐标到图片原始坐标的换算按原始缩放比例换算坐标裁剪前再次校验矩形是否越界调用API报“RequestLimitExceeded”并发数过高触发了腾讯云限流把并发控制在2~4或者加一个简易信号量限制最大并发数图片Base64太大导致识别失败图片尺寸过大或裁剪区域依然保持很高分辨率识别前对裁剪图做压缩比如把长边控制在1000px以内识别出的文本带中文标点和引号OCR返回的文本包含全角字符用正则统一替换为下划线并做Trim清理WPF界面在批量处理时“假死”在UI线程直接调用同步OCR方法改用async/await并确保所有UI更新都切回Dispatcher线程目标文件名带百分号或等号导致无法打开文件系统对特殊字符有额外限制改名时统一替换掉%、#这类字符程序抛出“GDI中发生一般性错误”Bitmap未正确Dispose资源被占用用using包裹所有Bitmap/Graphics对象避免句柄泄漏5.1 一些容易被忽略的边界场景批量归档场景里文件不只是都存在同一个目录下很可能分散在多个子目录中。处理这种场景时如果你在重命名时把“原文件名”当成唯一的身份标识那必须注意两张不同目录下的图片可能拥有相同的原名但重命名后可能会重名。所以我在内部管理时用的是“文件完整路径”作为唯一键显示时才拆成“路径 文件名”。还有一个很多人会犯的错误是遍历文件时使用Directory.GetFiles处理过程中又调用了File.Move来重命名这会导致遍历集合和实际文件系统发生不同步。我在实现中先把所有文件路径加载进一个List然后逐个处理处理过程中使用的都是预先加载的列表数据绝对不在遍历枚举器内部动原文件系统。5.2 提高识别率的几个土办法虽然腾讯云OCR的准度本身已经很不错但批量任务里总有那么几张歪斜、带干扰的图片。我摸索出了几个有效方案一是膨胀识别区域。如果你的文字在区域边缘、或者有轻微的旋转那框选区域时可以让范围稍微大一圈比如四周多留出5到10像素不要裁得刚刚好。二是开二值化预处理。在发给OCR之前将裁剪图转成灰度图再做一次简单的阈值二值化能在很多“白底灰字”的图片上显著提升识别效果。灰度化和二值化用System.Drawing就能实现不需要引入OpenCV。但注意如果原图是彩色文字、带渐变花纹二值化反而会降低效果所以最好在界面里做一个“是否做预处理”的开关或者只对识别失败的图片做第二次重试时启用。三是序列化重试。识别失败时不要马上放弃在原图上缩小识别区域重新调用一次往往能纠正因为坐标偏移导致的裁剪不完整问题。我在工具里加了一个“自动重试”选项重试次数设为1次不会显著增加调用成本。5.3 已经在实际使用中验证的性能数据为了给大家一个量化参考我在几台机器上分别跑过测试。开发机上CPU是i5-1240P16GB内存SSD单线程处理100张图耗时约130秒把并发设为3后同样的100张图耗时降到约50秒。识别速度的瓶颈主要在API网络延迟上本地图片裁剪耗时几乎可以忽略不计。这个数据说明不要把批量处理的单位控制得太小并发3到4的收益最大再多反而会触发限流。另外我还处理过一批.bmp格式的扫描图每个文件接近20MB裁剪后打码转成JPG再传给API识别速度和.JPG源文件基本没有差别。所以如果你的图片是BMP、PNG这类体积大的格式建议都在裁剪之后统一转成JPG能明显减少网络上传时间。5.4 这个方案还能怎么扩展如果你做完这个核心流程后还有余力我建议往这几个方向扩展把“识别区域”配置保存成预设文件以后切换不同批次的图片时可以直接加载预设。支持多个识别区域拼接命名比如“区域A识别出的编号 区域B识别出的日期”一起组合成文件名。增加“重命名预览确认”页面在真正执行File.Move之前让用户勾选哪些文件要改、哪些保留原名。把后缀名的自动补充逻辑做进去如果识别结果里带了原来的扩展名处理时直接截断如果没有就自动补上原文件的扩展名。其中“多区域拼接”我后来在一批带日期和编号的图片上试过组合命名的效果很好比如把“20250411”和“订单号A1023”拼成“20250411_订单号A1023.jpg”。这个功能相比单区域识别只多了一个Region列表的维护和一个字符串拼接的步骤代码量不大但实用性提升非常明显。最后再分享一个经验做这类工具的时候一定要先拿真实数据测试再写文档和界面。因为区域OCR识别的痛点和效果只有在你自己的图片上跑过一遍之后才能有体感。我自己第一版工具做得很粗糙界面上连识别区域预览都没有全靠手动填坐标结果几张图坐标偏了几个像素就识别错了。后来花了半天把区域框选和坐标换算加上体验才真正正常。这个工具到现在还留在我的工具箱里每当要整理一批带编号的图片时双击打开、框个区域、点个开始等着日志慢慢跳完就行那种感觉确实轻松很多。
返回列表