尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

使用WPF与腾讯云OCR实现批量图片固定区域文字识别及自动重命名

发布时间:2026/9/24 22:16:23

资讯中心
01
ARTICLE

使用WPF与腾讯云OCR实现批量图片固定区域文字识别及自动重命名

使用WPF与腾讯云OCR实现批量图片固定区域文字识别及自动重命名
一直都有朋友问我有一堆JPG图片每张图片上固定位置有一段文字比如发票号、设备编码、快递单号能不能自动把这些文字识别出来直接改成文件名市面上的OCR工具很多能识别文字的都一大把但“按固定区域批量识别并自动改名”这个需求还真没几个现成软件能干净利落搞定。要么是只能一张张手动框选要么是只能整图识别导致文件名里混进一堆无关文字要么干脆收费还不便宜。这事的本质其实不复杂区域OCR识别 批量重命名。既然找不到趁手的工具那就自己做一个。我用了WPF做桌面界面接腾讯云的通用印刷体OCR API做了个小工具整个过程跑下来也就一个周末的功夫。这篇文章把这套方案的完整思路、关键代码和踩坑记录都整理出来有同样需求的人可以直接照着抄。1. 先弄清楚为什么没有“完美”现成工具在动手写代码之前我花了点时间把市面上的现成方案过了一遍也建议你先做这一步。因为盲目的“自己造轮子”并不总值得但如果现成方案确实满足不了需求那你就会很清楚自己造这个轮子到底要解决什么。1.1 现成小工具的三个典型痛点我实际用过几类工具问题基本集中在三个方面。第一类是整图OCR工具比如某些截图识别软件、PDF识别工具。它们能识别整张图片里的所有文字但改文件名的时候你根本不知道哪段文字才是你想要的。举个例子一张快递面单上既有收件人又有地址还有单号整图识别后会把“张三”“北京市朝阳区某某路”全拼在一起拿去当文件名既长又乱。如果图片是扫描件背景里还有水印、印章文字那文件名就更没法看了。第二类是需要手动单张框选的工具。这类工具允许你手动在图片上拉一个框告诉软件“识别这个区域”但每张图都要拉一次框。几十张图还能忍上千张图就完全是体力活了。而且很多这类工具一次只能处理一张没有“记住区域、批量处理”的能力。第三类是专业文档采集软件功能确实全但价格高、配置复杂还要培训学习成本。为一个改文件名的需求去上这种系统属于杀鸡用牛刀。所以核心矛盾就一句话需求是固定的、重复的、批量化的——图片区域是固定的识别内容是固定的唯独没有工具能把“固定区域”这个设定记住并套用到所有图片上。这正是自己写一个工具的价值所在。1.2 为什么选WPF和腾讯云OCR先说WPF。做Windows桌面小工具可选的技术栈其实挺多WinForms、WPF、Electron、Avalonia、Qt。我选WPF的原因很直接界面能力够强。WPF的Canvas、Border、Rectangle这些控件做“图片显示 矩形框选”交互非常顺手矢量化的渲染让缩放和坐标换算很直观。生态成熟。.NET开发资料多、坑少打包发布也方便xcopy发布就能跑目标机器不需要装运行环境如果发布成自包含。跨平台不是刚需。这种工具基本是在自己的Windows电脑上用没必要为“万一以后要用在Mac上”付出额外的复杂度。再说OCR引擎。本地OCR方案我试过Tesseract开源性好、离线可用但识别中文印刷体尤其是图片里的那种小字号、有背景干扰的文字准确率明显不如云服务。考虑到这工具处理的是“固定区域的批次图片”识别的稳定性比离线能力重要得多所以我选了腾讯云OCR的通用印刷体识别接口。腾讯云OCR的选择理由有三点识别准确率高对中文场景的优化比较好尤其是印刷体、票据、单据这类内容。有官方.NET SDK接入成本低不用自己拼HTTP请求和签名。有免费额度个人工具、小批量使用基本花不了几个钱。后面我会详细讲怎么接入。这里先强调一个理念工具选型不要追求“最强大”要追求“最合适”。本地OCR虽然免费且隐私性好但准确率的稳定性在这个场景里更关键。2. 界面先搭起来WPF主窗口与区域框选交互这个工具的使用流程其实很清晰选择文件夹 → 加载预览图 → 框选要识别的区域可以框多个 → 开始批量识别 → 自动重命名。界面上最重要的不是你用什么花哨的样式而是让“框选区域”这件事直观、顺手。2.1 主窗口布局设计我用的布局是三段式顶部是操作栏中间是图片画布区底部是识别结果列表。Grid Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition Height*/ RowDefinition Height200/ /Grid.RowDefinitions StackPanel OrientationHorizontal Margin10 Grid.Row0 Button Content选择文件夹 ClickSelectFolder_Click Width100 Margin0,0,8,0/ Button Content框选区域 x:NameSelectRegionBtn ClickSelectRegion_Click Width100 Margin0,0,8,0/ Button Content开始识别 x:NameStartOcrBtn ClickStartOcr_Click Width100 Margin0,0,8,0/ TextBlock x:NameStatusText VerticalAlignmentCenter Margin12,0,0,0/ /StackPanel Border Grid.Row1 Background#1E1E1E BorderBrush#333 BorderThickness1 Canvas x:NameImageCanvas MouseLeftButtonDownCanvas_MouseDown MouseMoveCanvas_MouseMove MouseLeftButtonUpCanvas_MouseUp Image x:NameDisplayImage StretchUniform/ Rectangle x:NameSelectionRectangle Stroke#FF4D4D StrokeThickness2 StrokeDashArray4 2 VisibilityCollapsed/ /Canvas /Border ListBox Grid.Row2 x:NameResultList Margin10,10,10,10 ListBox.ItemTemplate DataTemplate StackPanel OrientationHorizontal TextBlock Text{Binding FileName} Width260 TextTrimmingCharacterEllipsis/ TextBlock Text{Binding RecognizedText} Foreground#666/ /StackPanel /DataTemplate /ListBox.ItemTemplate /ListBox /Grid中间画布用的是Canvas里面放一个StretchUniform的Image。这里有个关键点为什么用Uniform而不是Fill因为Fill会拉伸图片导致比例失真框选区域的位置也会跟着错位。Uniform保持图片原始宽高比图片居中显示这才是“所见即所得”的基础。2.2 框选区域的鼠标交互与坐标换算框选交互的代码不复杂鼠标按下记录起点移动时更新矩形抬起时记录终点并生成一个Region对象。private Point _startPoint; private bool _isSelecting; private readonly ObservableCollectionOcrRegion _regions new(); private void Canvas_MouseDown(object sender, MouseButtonEventArgs e) { if (_currentMode ! EditMode.SelectRegion) return; _startPoint e.GetPosition(ImageCanvas); _isSelecting true; SelectionRectangle.Visibility Visibility.Visible; } private void Canvas_MouseMove(object sender, MouseEventArgs e) { if (!_isSelecting) return; var current e.GetPosition(ImageCanvas); var rect new Rect( Math.Min(_startPoint.X, current.X), Math.Min(_startPoint.Y, current.Y), Math.Abs(current.X - _startPoint.X), Math.Abs(current.Y - _startPoint.Y)); SelectionRectangle.Rect rect; } private void Canvas_MouseUp(object sender, MouseButtonEventArgs e) { if (!_isSelecting) return; _isSelecting false; SelectionRectangle.Visibility Visibility.Collapsed; var endPoint e.GetPosition(ImageCanvas); var rect new Rect(_startPoint, endPoint); if (rect.Width 5 || rect.Height 5) { return; // 太小不算有效区域 } // 换算为原始图片像素坐标 var pixelRect ConvertToPixelRect(rect); _regions.Add(new OcrRegion { X pixelRect.X, Y pixelRect.Y, Width pixelRect.Width, Height pixelRect.Height }); }关于ConvertToPixelRect这个坐标换算是整个框选交互里最容易出错的地方。WPF里鼠标得到的是逻辑像素坐标也就是和Canvas内部坐标一致的坐标。但Image控件使用Uniform模式后图片在Canvas里实际显示的区域可能不是整个Canvas而是居中后留有上下或左右的黑边。所以换算不能直接用Canvas宽度对比图片宽度。正确做法是先获取图片控件的ActualWidth和ActualHeight再根据图片自身的原始尺寸计算图片实际绘制区域。private Rect ConvertToPixelRect(Rect displayRect) { var img DisplayImage; if (img.Source null) return Rect.Empty; var srcWidth img.Source.Width; var srcHeight img.Source.Height; double scale Math.Min(img.ActualWidth / srcWidth, img.ActualHeight / srcHeight); double drawWidth srcWidth * scale; double drawHeight srcHeight * scale; double offsetX (img.ActualWidth - drawWidth) / 2; double offsetY (img.ActualHeight - drawHeight) / 2; // 减去偏移再除以缩放比得到原图像素坐标 double pixelX (displayRect.X - offsetX) / scale; double pixelY (displayRect.Y - offsetY) / scale; double pixelW displayRect.Width / scale; double pixelH displayRect.Height / scale; return new Rect(pixelX, pixelY, pixelW, pixelH); }这个换算我在第一次做的时候偷懒直接按Canvas宽高比例算结果拉伸模式下区域全偏了。UI上的坐标和图像的像素坐标完全是两套系统必须做换算而且换算时要把Uniform带来的留边考虑进去。这是第一个值得记下来的细节。2.3 区域的持久化与可视化如果每次打开工具都要重新框一次区域那批量处理的意义就少了一半。所以我把区域配置序列化成了JSON文件放在图片文件夹下。public class OcrRegion { public double X { get; set; } public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } public string Name { get; set; } 区域1; } public class ProjectConfig { public string ImageFolder { get; set; } public ListOcrRegion Regions { get; set; } new(); }保存和加载用System.Text.Json就够不需要引入额外依赖。每次框选完一个区域除了在列表里加一项我还把已确定的区域画在画布上用半透明的不同颜色区分方便用户确认区域位置是不是对的。private void RedrawRegions() { // 移除旧的区域矩形只保留交互用的SelectionRectangle foreach (var child in ImageCanvas.Children.OfTypeSystem.Windows.Shapes.Rectangle().ToList()) { if (child ! SelectionRectangle) ImageCanvas.Children.Remove(child); } foreach (var region in _regions) { var r new System.Windows.Shapes.Rectangle { Stroke Brushes.LimeGreen, StrokeThickness 1.5, Fill new SolidColorBrush(Color.FromArgb(40, 0, 255, 0)) }; var pixelRect ConvertFromPixelRect(new Rect(region.X, region.Y, region.Width, region.Height)); Canvas.SetLeft(r, pixelRect.X); Canvas.SetTop(r, pixelRect.Y); r.Width pixelRect.Width; r.Height pixelRect.Height; ImageCanvas.Children.Add(r); } }这里有个细节区域持久化保存的是“原始图像素坐标”而不是显示坐标。这样即使下次打开工具时窗口大小变了、图片缩放比例变了区域依然能准确定位到图片上的同一块位置。如果保存的是显示坐标换个窗口尺寸就全乱了。3. 打通OCR能力腾讯云API接入全流程界面搭好之后接下来是核心的识别能力接入。这一步涉及账号准备、SDK引入、代码调用和参数调优四个环节按顺序走完就能在自己代码里调通OCR了。3.1 账号准备与密钥获取在写代码之前先得搞定腾讯云账号和密钥。注册腾讯云账号完成实名认证。在控制台搜索“文字识别”开通“通用印刷体识别”服务也就是GeneralBasicOCR接口对应的产品。在“访问管理 → API密钥管理”里创建SecretId和SecretKey。这里要特别提醒三个点第一密钥是敏感信息。不要在代码里硬编码后把源码随便发到GitHub上更不要在博文里贴真实的密钥。我自己开发时是把密钥放在appsettings.json里并加入.gitignore。第二开通后确认免费额度。腾讯云OCR每种接口每月都有免费调用额度个人批量改名的使用量完全够用。万一超过额度价格也很便宜。第三区域选择要注意。腾讯云的OCR接口虽然很多地域都有接入但不同地域的调用延迟略有差异。国内使用的话选ap-guangzhou或者ap-beijing都比较稳。3.2 引入官方SDK与核心调用代码腾讯云提供了官方的.NET SDKNuGet搜索TencentCloudSDK把TencentCloudSDK.Ocr或其他对应包装进来然后写一个封装类using TencentCloud.Common; using TencentCloud.Common.Profile; 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) { var credential new Credential { SecretId secretId, SecretKey secretKey }; var clientProfile new ClientProfile(); _client new OcrClient(credential, region, clientProfile); } public async Taskstring RecognizeRegionAsync(byte[] imageBytes) { var req new GeneralBasicOCRRequest { ImageBase64 Convert.ToBase64String(imageBytes) }; var resp await _client.GeneralBasicOCR(req); // 把所有识别出来的文本行拼起来 if (resp.TextDetections null || resp.TextDetections.Length 0) return string.Empty; var texts resp.TextDetections .OrderBy(t t.Polygon ! null t.Polygon.Length 0 ? t.Polygon.Min(p p.Y) : 0) .Select(t t.DetectedText); return string.Join(, texts); } }调用接口传图片有两种方式ImageBase64和ImageUrl。对于本地批量处理的场景直接用ImageBase64最方便不需要先把图片传到一个公网URL再让腾讯云去拉取。但要注意Base64编码后体积会增加约33%腾讯云对单张图片大小有限制所以图片过大的时候要先压缩再传。那段OrderBy排序代码我要解释一下。腾讯云返回的TextDetections数组不保证按阅读顺序排列如果不排序直接拼接识别到的两段文字可能顺序是颠倒的。通用印刷体识别的返回结果里带Polygon多边形坐标取每一行文字的最小Y坐标来排序基本能恢复“从上到下”的阅读顺序。如果你的图片区域是水平排版这一步很关键。3.3 识别参数的取舍与精度优化腾讯云通用印刷体识别接口有参数可以调整但最常用的其实就几个。我实际用下来觉得值得关注的参数参数作用我的建议ImageBase64待识别图片区域裁剪后的小图直接传DetectText是否检测后返回文本框信息默认开不用改EnableAdventCheck广告文字检测对这个场景没用关掉省时EnablePdfRecognizePDF识别不需要LanguageType语言类型默认会自动识别中英文够用真正的精度优化不在这几个参数上而是在图片预处理上。我做了三件事转灰度图。区域裁剪出的小图如果是彩色的先转成灰度。文字识别主要靠边缘和纹理颜色信息帮助不大但灰度化可以减少无关颜色的干扰。适当放大图片。如果裁剪出的区域分辨率太低比如文字只有十几个像素高OCR识别率会明显下降。我在裁剪后会把小图按比例放大到字高30像素以上再传给接口。裁剪时加上边距。如果区域框得比较紧文字紧贴边界识别时容易丢字。我实际裁剪时会把区域向外扩展510像素作为安全边距。下面这段是区域裁剪加预处理的实现用的System.Drawing.Commonprivate byte[] CropAndPreprocess(string filePath, OcrRegion region, int padding 8) { using var src System.Drawing.Image.FromFile(filePath); double scaleX src.Width / (double)DisplayImage.Source.Width; // 注意这里用的是换算后的像素坐标不用再乘缩放 // 区域已经是原始像素坐标直接画就行 int x Math.Max(0, (int)region.X - padding); int y Math.Max(0, (int)region.Y - padding); int w Math.Min(src.Width - x, (int)region.Width padding * 2); int h Math.Min(src.Height - y, (int)region.Height padding * 2); using var bmp new Bitmap(w, h); using (var g Graphics.FromImage(bmp)) { g.DrawImage(src, new Rectangle(0, 0, w, h), new Rectangle(x, y, w, h), GraphicsUnit.Pixel); } using var gray new Bitmap(bmp.Width, bmp.Height); using (var g Graphics.FromImage(gray)) { var colorMatrix new System.Drawing.Imaging.ColorMatrix( new float[][] { new float[] { 0.299f, 0.299f, 0.299f, 0, 0 }, new float[] { 0.587f, 0.587f, 0.587f, 0, 0 }, new float[] { 0.114f, 0.114f, 0.114f, 0, 0 }, new float[] { 0, 0, 0, 1, 0 }, new float[] { 0, 0, 0, 0, 1 } }); var attributes new System.Drawing.Imaging.ImageAttributes(); attributes.SetColorMatrix(colorMatrix); g.DrawImage(bmp, new Rectangle(0, 0, gray.Width, gray.Height), 0, 0, bmp.Width, bmp.Height, GraphicsUnit.Pixel, attributes); } using var ms new MemoryStream(); gray.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); }灰度化处理是这部分代码里最有性价比的一个操作。我没有做二值化因为有些扫描件背景不均匀固定阈值的二值化反而会把文字“化掉”。灰度化加原图识别就够稳了。4. 批量流程的核心逻辑从一组图到一组文件名界面和OCR能力都就绪了开始把它们串成一条完整的批量处理流水线。这是整个工具从“能用”走向“好用”的关键。4.1 批量处理的流程设计批量处理的流程我拆成了四步每一步都有明确的产出和校验点private async void StartOcr_Click(object sender, RoutedEventArgs e) { if (_regions.Count 0) { MessageBox.Show(请先框选至少一个识别区域); return; } StartOcrBtn.IsEnabled false; SelectRegionBtn.IsEnabled false; ProgressBar.Visibility Visibility.Visible; try { var files Directory.GetFiles(_currentFolder, *.jpg) .Concat(Directory.GetFiles(_currentFolder, *.jpeg)) .Concat(Directory.GetFiles(_currentFolder, *.png)) .OrderBy(f f) .ToList(); var renameTasks new List(string oldPath, string newPath, string fileName, string text)(); var semaphore new SemaphoreSlim(2); // 控制并发数 for (int i 0; i files.Count; i) { ProgressBar.Value i * 100.0 / files.Count; var file files[i]; await semaphore.WaitAsync(); try { var regionTexts new Liststring(); foreach (var region in _regions) { var cropped CropAndPreprocess(file, region); var text await _ocrService.RecognizeRegionAsync(cropped); regionTexts.Add(text); } var newName BuildNewFileName(file, regionTexts); renameTasks.Add((file, newName, Path.GetFileName(file), string.Join(|, regionTexts))); ResultList.Dispatcher.Invoke(() { ResultList.Items.Add(new { FileName Path.GetFileName(file), RecognizedText string.Join( | , regionTexts) }); }); } finally { semaphore.Release(); } } // 预览确认后真正执行重命名 ApplyRenames(renameTasks); } finally { ProgressBar.Visibility Visibility.Collapsed; StartOcrBtn.IsEnabled true; SelectRegionBtn.IsEnabled true; } }这个逻辑里有两个我当时纠结过的点这里展开说。第一个是先识别预览再重命名。识别和改名是两件事把它们分开用户可以在窗口里先看一遍识别出的文本有没有明显错误比如把O识别成0确认没问题再真正修改文件名。如果直接识别完就改名几百张图一旦出错改回去非常痛苦。哪怕多花一次点击的功夫这个确认步骤也是值得的。第二个是控制并发数。从异步代码上看我可以同时发起几十个请求API也能接受但腾讯云OCR接口有QPS限制一般默认每秒几次到十几次超过了会被限流返回错误。我直接用SemaphoreSlim(2)把并发压到2稳妥且基本不影响使用体验。如果你要处理几千张图可以把这个值微调到34但别贪多触发限流后反而更慢。4.2 文件名清洗与冲突处理从OCR拿回来的文本不能直接用。试想一下一个识别结果里带着换行、空格、斜杠、问号直接当文件名会怎样要么创建失败要么生成了一个丑得要命的名字。所以有一套固定的清洗规则private string SanitizeFileName(string text) { if (string.IsNullOrWhiteSpace(text)) return 未识别; // 替换掉Windows文件名的非法字符 var invalidChars Path.GetInvalidFileNameChars(); var sb new StringBuilder(text.Length); foreach (var ch in text) { if (invalidChars.Contains(ch) || char.IsControl(ch)) sb.Append(_); else if (ch ) sb.Append(_); // 空格也替换避免名字里出现空格的尴尬 else sb.Append(ch); } var result sb.ToString().Trim(_, ., ); return string.IsNullOrEmpty(result) ? 未识别 : result; }如果Import这个单词识别时中间混进了制表符或换行符上面的char.IsControl(ch)会把它们全部换成下划线。这一步能避免最脏的那类问题。文件名冲突处理也很重要。假设一张图识别出来的文本和上一张一样直接重命名后面的图会覆盖前面的图。我的策略是如果目标文件名已存在自动追加序号。private void ApplyRenames(List(string oldPath, string newPath, string fileName, string text) renameTasks) { var usedNames new HashSetstring(StringComparer.OrdinalIgnoreCase); foreach (var task in renameTasks) { var dir Path.GetDirectoryName(task.oldPath); var targetName task.newPath; var nameWithoutExt Path.GetFileNameWithoutExtension(targetName); var ext Path.GetExtension(targetName); if (usedNames.Contains(targetName)) { int counter 1; do { targetName ${nameWithoutExt}_{counter}{ext}; counter; } while (usedNames.Contains(targetName)); } usedNames.Add(targetName); var targetPath Path.Combine(dir, targetName); File.Move(task.oldPath, targetPath); } }有一个容易被忽略的坑Windows文件名不区分大小写但HashSet默认区分。所以这里传入StringComparer.OrdinalIgnoreCase否则Invoice.jpg和INVOICE.jpg会被当成两个名字实际写文件时窗口还是可能弹冲突提示。4.3 多区域的拼接顺序与命名规则如果一张图片上框了多个区域比如一张商品标签图同时框了“商品编码”和“生产日期”两个区域最终文件名怎么拼我一开始是简单地用下划线连接private string BuildNewFileName(string filePath, Liststring regionTexts) { var cleanParts regionTexts.Select(SanitizeFileName) .Where(t !string.IsNullOrEmpty(t) t ! 未识别); var joined string.Join(_, cleanParts); if (string.IsNullOrEmpty(joined)) joined 未识别; return joined Path.GetExtension(filePath); }但这个方案有个问题只看结果你分不清哪段是商品编码、哪段是生产日期。于是在区域配置类里加了个Name字段文件名拼接时带上标签前缀var parts new Liststring(); for (int i 0; i _regions.Count; i) { var clean SanitizeFileName(regionTexts[i]); if (string.IsNullOrEmpty(clean) || clean 未识别) continue; parts.Add(${_regions[i].Name}_{clean}); } return string.Join(_, parts) Path.GetExtension(filePath);合并后的效果类似商品编码_6923456789012_生产日期_20231228.jpg。这个带标签前缀的细节实际用起来比想象中重要得多。因为识别出来的文字本身可能没有自解释性一串数字谁也不知道它是什么加上前缀后文件名才真正有可读性。5. 真实运行效果与踩坑复盘工具的代码写完后我拿自己手头的一批设备标签图测了一轮效果比预想的好但过程中也踩了几个值得记录下来的坑。5.1 一组真实图片的实测数据测试图片是朋友给的500多张设备铭牌照片每张图片上我们需要提取“设备型号”和“出厂编号”两个字段都在固定的右上角区域。我框好两个区域后一键跑完。结果统计如下指标数值说明总图片数536全部为JPG识别成功率521有15张未识别出完整文本一次通过率97.2%识别出的文本可直接当文件名单张平均耗时约0.8秒并行2个请求时总耗时约7分钟536张图失败案例有三类一类是铭牌表面有严重反光字被高光遮盖这类其实换个人眼也看不清一类是字体特别怪异的艺术字还有一类是区域框选偏了因为个别图片的铭牌位置跟标准位置有轻微偏移。这次实测让我意识到一个设计上的局限“固定区域”的假设在大多数时候成立但不是永远成立。如果图片里的物体位置有较大偏移严格固定区域会识别失败。当时为了不把工具复杂度推高我选择了“区域容差扩展”的方式把设定的识别区域向外各放大10%在这个扩展范围内识别然后从返回的文本结果里做过滤——只保留与预期长度相似的文本。这样能覆盖一部分偏移场景。5.2 四个典型的坑与对应解法第一个坑是System.Drawing在.NET Core下的兼容问题。我用的是System.Drawing.Common包来裁剪图片在Windows上没问题但如果以后要把工具发布到Linux环境就需要注意。当时我直接锁定了Windows运行所以问题不大。如果你在多平台部署可以换用SkiaSharp或ImageSharp。第二个坑是Base64编码导致的请求体积超限。我一开始没做图片压缩直接传原图的裁剪区域结果有些大图裁剪出来的区域也有几百KBBase64编码后超过接口限制报错。后来在CropAndPreprocess里加了一步简单的质量压缩把JPG的编码质量从默认的75降到60体积几乎减半识别率没有明显下降。第三个坑是Coordinate坐标换算在DPI缩放下的偏差。现在很多Windows笔记本默认缩放是125%或150%WPF的坐标和物理像素不是1:1。在Canvas_MouseDown里用e.GetPosition拿到的坐标是WPF的逻辑坐标而后面裁剪用的System.Drawing.Bitmap处理的是物理像素。如果不处理DPI缩放在150%缩放下框选的区域会整体偏上偏左。解决方法是程序启动时加载SetProcessDpiAwareness让WPF按物理像素绘制或者把缩放因子换算回来[DllImport(user32.dll)] private static extern bool SetProcessDPIAware(); public MainWindow() { InitializeComponent(); SetProcessDPIAware(); // 在窗口初始化时调用 }第四个坑是连续调用OCR导致本地端口耗尽。这个问题比较隐蔽。腾讯云SDK底层用的是HTTPClient如果不复用HttpClient实例每个请求都会新建TCP连接大量请求后会出现临时端口耗尽程序突然报错“Failed to establish connection”。解决方式就是我把TencentOcrService设计成单例整个程序只创建一个OcrClient实例底层TCP连接池复用问题就消失了。5.3 后续还能怎么扩展这个工具做完之后我顺便想了几个扩展方向都还没做但架构上是预留了位置了。一是接入更多OCR引擎。目前是直接依赖腾讯云OCR但接口封装是独立的ITextRecognizer接口以后想换成百度OCR、阿里云OCR、PaddleOCR本地部署只需要实现这个接口不需要动UI和批量处理逻辑。二是支持更多图片格式。现在只做了JPG、PNG、JPEG三种其实加BMP、TIFF也不难主要是在文件过滤时多列几个后缀。三是模板保存功能。现在区域配置保存为项目JSON文件可以进一步做成“模板库”比如保存“快递面单模板”“发票模板”“铭牌模板”下次处理同类型图片时直接选模板连框选区域都省了。四是识别结果的二次校验。OCR对0和O、1和I这类字符容易混淆如果识别的字段有明确格式比如固定位数可以加正则校验不符合格式的标记出来人工处理。这对生产环境下的批量场景特别有用。最后再分享一点实际体会把整个项目从头到尾做下来我最深的感受是这种“小工具”最麻烦的不是代码本身而是把需求想清楚的过程。最开始我以为核心难点在OCR调用写完之后才发现真正的工程量在区域框选的坐标换算、批量处理的流程设计、文件名的清洗与冲突处理这些看起来很不起眼的环节。实测中我也发现用这种方式处理批量图片单纯从时间成本看500张图大概需要7分钟左右加上人工抽检的时间一共不超过15分钟。如果全手动处理每张图打开、框选、识别、复制、改名平均一分钟一张500张图就是8个多小时。这个对比是巨大的。这个工具的价值不在于技术有多炫而在于它把“重复劳动”压缩成了“确认一次机器跑完”。我现在的习惯是处理这类批量改名的活时先花5分钟想清楚区域和规则再用工具跑完留出更多时间处理那些真正需要人判断的事情。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。