WPF+腾讯云OCR实现批量图片区域文字识别与自动重命名
2026/9/24 22:03:19 网站建设 项目流程

先交代一下背景。上个月帮朋友整理一批产品标签扫描件,文件夹里几百张 JPG,文件名全是“IMG_20240312_113045.jpg”这种,肉眼根本分不清哪张是哪个型号。当时脑子里第一个念头是:有没有现成工具,能自动识别图片指定区域里的文字,然后直接用识别结果改文件名?搜了一圈,要么只支持整张图识别,要么改名规则太死板,要么是付费在线工具还限制张数。那干脆自己做一个小工具,技术栈选 WPF 加腾讯云 OCR API,整个过程就是批量遍历图片、手动框选要识别的区域、调用接口拿文字、按规则自动改名。做完之后我把这套方案的完整思路和踩坑过程整理出来,供有同样需求的朋友参考。

1. 需求拆解与方案选型

1.1 三个核心诉求背后的真实场景

先搞清楚标题里“批量图片区域识别改名”这句话到底在说什么。拆开看就是三个动作:批量、区域识别、改名。

批量:不是处理一两张图,而是几十上百张。这意味着程序必须支持文件夹遍历、自动过滤图片格式、连续处理,同时要有进度提示,否则跑起来像死机一样。

区域识别:不是整张图全部识别,而是只识别图片中某一块指定区域。比如一张产品标签上,有 logo、有型号、有二维码、有生产日期,我们需要的可能只是型号那一行。如果整张图都识别,返回几十条文字结果,还要额外做过滤逻辑,而且容易把干扰文字混进来。更关键的是,同一批图片通常结构是固定的,型号都出现在同一位置,所以“区域”应该可以框选一次,应用到整批图片。

改名:用识别出来的文字重命名文件。这里有个容易被忽略的坑——识别出来的文字不一定合法,可能包含/ \ : * ? " < > |这些 Windows 文件名非法字符,也可能超长、为空、包含多余空格。命名规则还得考虑重名冲突,否则直接 File.Move 会抛异常。

这三个点组合在一起,决定了整个工具的形态:一个带图形界面(用来框选区域)、能跑批量任务、内置命名规则的桌面程序。

1.2 为什么选 WPF 加腾讯云 OCR,而不是其他搭配

先说 UI 框架的选型。当时列了几个候选:Python + PyQt,Electron,WinForms,WPF。Python + PyQt 其实也能做,但对最终用户不友好,要装 Python 环境,打包成 exe 体积大还容易被杀毒误报。Electron 更重,一个 Hello World 都要一两百 MB,为了一个改文件名的小工具不值当。WinForms 虽然轻,但界面做框选交互不如 WPF 灵活,画布、绑定、样式这些能力 WPF 明显更强。WPF 是 Windows 原生框架,C# 写逻辑顺手,发布单文件 exe 也很简单,最终选了 WPF。

再说 OCR 引擎。本来想用 Tesseract,毕竟开源免费,但实际测下来中文精度一般,尤其在复杂背景下效果很惨。又不想自己训练模型,所以转向云 API。对比了腾讯、百度、阿里三家,最终选腾讯云,原因有三个:新用户有免费额度,够把工具调通;官方提供 .NET SDK,直接 NuGet 装包,不用自己实现签名;接口返回结构清晰,识别结果里每行文字带置信度和坐标,方便做二次过滤。

2. 环境准备与腾讯云 OCR 接入

2.1 开发环境与 NuGet 依赖

基础环境是 Visual Studio 2022,目标框架 .NET 6(Windows 桌面应用,选 .NET Core 版本方便后续跨平台思路,不过 WPF 本身只在 Windows 上跑)。创建一个 WPF 应用程序项目,然后通过 NuGet 装三个包:

  • TencentCloudSDK:腾讯云官方 SDK 主包,里面包含 OCR 的接口定义。
  • System.Drawing.Common:用于处理图片裁剪、旋转等操作。注意 .NET 6 里 Windows 平台要用这个包,得额外安装。
  • Microsoft.Toolkit.Mvvm(可选):如果不想写一堆事件处理器,用 MVVM 模式会舒服很多。但为了降低理解门槛,下面的示例代码直接用 code-behind 写逻辑,不引入额外框架。

装包时有个小提示:TencentCloudSDK 这个包版本更新比较频繁,建议装最新的稳定版。我一开始装了老版本,结果 OCR 接口里缺少部分字段,排查了半天才发现是 SDK 版本太旧。

2.2 开通腾讯云 OCR 服务与获取密钥

打开腾讯云控制台,在“访问管理 > 访问密钥 > API 密钥管理”里创建一对 SecretId 和 SecretKey。这两串密钥要保存好,代码里不要硬编码,我习惯放在一个 local.settings.json 文件里并加入 .gitignore,避免误传仓库。

然后在“产品与服务 > 人工智能 > 文字识别”里开通通用印刷体识别服务。控制台会显示免费调用额度,一般新用户赠送一个月免费额度,足够测试使用。如果只是个人小批量整理文件,免费额度基本上够用。

2.3 在 WPF 中封装一个 OCR 调用类

直接在主窗口里写调用逻辑会让代码越来越乱,最好单独封装一个OcrHelper类。核心代码大致如下:

using TencentCloud.Common; using TencentCloud.Common.Profile; using TencentCloud.Ocr.V20181119; using TencentCloud.Ocr.V20181119.Models; public class OcrHelper { private static readonly string SecretId = "your-secret-id"; private static readonly string SecretKey = "your-secret-key"; public static async Task<string> RecognizeTextFromBytes(byte[] imageBytes) { Credential cred = new Credential { SecretId = SecretId, SecretKey = SecretKey }; HttpProfile httpProfile = new HttpProfile { Endpoint = "ocr.tencentcloudapi.com" }; ClientProfile clientProfile = new ClientProfile { HttpProfile = httpProfile }; OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile); GeneralBasicOCRRequest req = new GeneralBasicOCRRequest { ImageBase64 = Convert.ToBase64String(imageBytes) }; GeneralBasicOCRResponse resp = await client.GeneralBasicOCR(req); if (resp.TextDetections != null && resp.TextDetections.Length > 0) { // 默认取第一行识别文本,也可以根据坐标、置信度做过滤 return resp.TextDetections[0].DetectedText?.Trim(); } return string.Empty; } }

这里有个细节:构造OcrClient时第二个参数是地域,OCR 服务不需要区分地域,填ap-guangzhou即可。接口选择上,基础需求用GeneralBasicOCR就够了,如果图片清晰度低或者文字小,可以换GeneralAccurateOCR,不过单价会高一些。返回结果里TextDetections是一个数组,每个元素是识别出的一块文字,包含DetectedTextConfidence两个常用字段。我建议不只是取第一行,而是把整个数组都返回出来,在 UI 上展示,让用户决定用哪一行作为文件名,后面会讲这个交互设计。

3. 核心功能实现:图片加载、区域框选与裁剪

3.1 批量加载图片文件

在窗口左侧放一个 ListBox,通过 FolderBrowserDialog 选择一个文件夹,然后用Directory.EnumerateFiles过滤出图片扩展名:

private void BtnLoadFolder_Click(object sender, RoutedEventArgs e) { using var dialog = new System.Windows.Forms.FolderBrowserDialog(); if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { string folder = dialog.SelectedPath; var extensions = new HashSet<string> { ".jpg", ".jpeg", ".png", ".bmp" }; _fileList = Directory.EnumerateFiles(folder) .Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant())) .OrderBy(f => f) .ToList(); ListBoxFiles.ItemsSource = _fileList.Select(f => Path.GetFileNameWithoutExtension(f)); if (_fileList.Count > 0) { ShowImage(0); } } }

注意这里用EnumerateFiles而不是GetFiles,前者是流式返回,处理几千个文件时内存占用更小。用户选中的文件夹里可能混着子目录,我暂时没有递归遍历,有需要可以加SearchOption.AllDirectories。另外图片格式虽然标题写的是 JPG,但实际场景里往往还有 PNG、BMP,所以扩展名过滤尽量放宽。

3.2 图片预览与区域框选交互

中间部分是一块大区域,用来显示当前图片和画矩形选区。我用一个Grid,底层放Image,上层放一个透明的Canvas,鼠标事件都挂在 Canvas 上:

<Grid> <Image x:Name="ImgPreview" Stretch="Uniform" /> <Canvas x:Name="CanvasOverlay" Background="Transparent" MouseLeftButtonDown="CanvasOverlay_MouseLeftButtonDown" MouseMove="CanvasOverlay_MouseMove" MouseLeftButtonUp="CanvasOverlay_MouseLeftButtonUp" /> </Grid>

框选逻辑按下、移动、抬起三步。鼠标按下时记录起点,移动时动态更新矩形位置和大小,抬起时确定选区并保存为Rect对象。为了直观,我在 Canvas 上画一个紫色半透明的Rectangle

private Point _startPoint; private Rectangle? _currentRect; private void CanvasOverlay_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { _startPoint = e.GetPosition(CanvasOverlay); _currentRect = new Rectangle { Fill = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)), Stroke = Brushes.Red, StrokeThickness = 1.5 }; Canvas.SetLeft(_currentRect, _startPoint.X); Canvas.SetTop(_currentRect, _startPoint.Y); CanvasOverlay.Children.Add(_currentRect); } private void CanvasOverlay_MouseMove(object sender, MouseEventArgs e) { if (_currentRect == null) return; var pos = e.GetPosition(CanvasOverlay); double x = Math.Min(_startPoint.X, pos.X); double y = Math.Min(_startPoint.Y, pos.Y); double w = Math.Abs(pos.X - _startPoint.X); double h = Math.Abs(pos.Y - _startPoint.Y); _currentRect.Width = w; _currentRect.Height = h; Canvas.SetLeft(_currentRect, x); Canvas.SetTop(_currentRect, y); }

这里有一个常见误区:Image控件的显示区域和实际位图尺寸通常不一致。当Stretch="Uniform"时,图片会在控件里等比例缩放,四周可能留白,鼠标在Canvas上的坐标不能直接当原图像素坐标用,必须换算。

换算公式是:

原图X = 选区左边距 - 图片显示区域左边距) × (原图宽度 / 图片显示区域宽度) 原图Y = 选区上边距 - 图片显示区域上边距) × (原图高度 / 图片显示区域高度)

我在代码里封装了一个转换方法,避免每次框选都用手算:

private Rect ConvertDisplayRectToOriginalRect(Rect displayRect) { double displayWidth = ImgPreview.ActualWidth; double displayHeight = ImgPreview.ActualHeight; double originalWidth = _currentBitmap.PixelWidth; double originalHeight = _currentBitmap.PixelHeight; // 计算图片在控件内实际显示的位置(Uniform 模式下可能留有空白) double ratio = Math.Min(displayWidth / originalWidth, displayHeight / originalHeight); double shownWidth = originalWidth * ratio; double shownHeight = originalHeight * ratio; double offsetX = (displayWidth - shownWidth) / 2; double offsetY = (displayHeight - shownHeight) / 2; double x = (displayRect.X - offsetX) / shownWidth * originalWidth; double y = (displayRect.Y - offsetY) / shownHeight * originalHeight; double width = displayRect.Width / shownWidth * originalWidth; double height = displayRect.Height / shownHeight * originalHeight; // 边界裁剪,防止越界 x = Math.Max(0, x); y = Math.Max(0, y); width = Math.Min(originalWidth - x, width); height = Math.Min(originalHeight - y, height); return new Rect(x, y, width, height); }

如果没用Uniform而是直接拉伸填满控件,上述换算公式就变成(displayRect.X / displayWidth) * originalWidth,简单一些。但实际图片比例未必和控件一致,所以Uniform更合理。

3.3 裁剪选区并生成 OCR 图片字节

拿到原图坐标后,使用CroppedBitmap裁剪并转成字节数组。这里也要小心一点:CroppedBitmap需要源是BitmapSource,大多数情况下我们用BitmapImage加载图片,可以直接传给CroppedBitmap。但为了后续能处理 EXIF 旋转,我统一先转成Bitmap,再转成BitmapSource展示。

裁剪逻辑:

private byte[] CropRegionToBytes(BitmapSource source, Int32Rect region) { var cropped = new CroppedBitmap(source, region); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(cropped)); using var ms = new MemoryStream(); encoder.Save(ms); return ms.ToArray(); }

注意输出格式我用的是 PNG 而不是 JPG,因为 PNG 无损,OCR 识别精度会好一点。虽然文件体积大一些,但图片区域通常很小,影响不大。

4. 批量识别、命名规则与冲突处理

4.1 批量识别区域文字并预览结果

区域框选确定后,点击“开始识别”按钮,程序就遍历_fileList里所有图片,对每一张裁剪对应区域,调用OcrHelper.RecognizeTextFromBytes,把识别出来的文字和原文件名一起显示在右侧列表里。

具体执行时我加了两个小设计:一个是用进度条展示当前进度,另一个是识别结果先不直接写回文件名,而是全部展示出来,等用户确认后再执行重命名。原因是 OCR 识别总有可能出错,如果直接写回,可能把好文件改成坏名字,等发现时原文件名已经丢了。这个“二次确认”步骤虽然多了一步操作,但安全系数高很多。

执行代码大致如下:

private async void BtnRecognizeAll_Click(object sender, RoutedEventArgs e) { _recognizeResults = new List<(string OriginalFile, string RecognizedText)>(); ProgressBar.Visibility = Visibility.Visible; ProgressBar.Maximum = _fileList.Count; for (int i = 0; i < _fileList.Count; i++) { string file = _fileList[i]; string fileName = Path.GetFileName(file); string recognizedText = string.Empty; try { var bitmap = new BitmapImage(new Uri(file)); // 这里需要处理 EXIF 旋转,后面讲 var region = ConvertDisplayRectToOriginalRect(_selectedRegion); byte[] cropBytes = CropRegionToBytes(bitmap, new Int32Rect( (int)region.X, (int)region.Y, (int)region.Width, (int)region.Height)); recognizedText = await OcrHelper.RecognizeTextFromBytes(cropBytes); } catch (Exception ex) { MessageBox.Show($"图片 {fileName} 识别失败:{ex.Message}"); } _recognizeResults.Add((file, recognizedText)); ListBoxResults.Items.Add($"{fileName} -> {recognizedText}"); ProgressBar.Value = i + 1; } ProgressBar.Visibility = Visibility.Collapsed; }

这里最大的问题是没有异常重试机制。实际调用云接口时,偶尔网络抖动或者 QPS 超限就会失败,一旦失败当前图片直接返回空字符串,整个流程继续跑。我建议加上一个for重试次数,比如每张图最多重试 3 次,每次间隔 300 毫秒。如果重试 3 次还是失败,再记录日志并继续下一张。

4.2 组装合法文件名:过滤非法字符与长度限制

识别结果拿来直接用会出事。Windows 文件名不允许包含\ / : * ? " < > |这 9 个字符,识别出来的文本里如果带了反斜杠或者冒号(比如货号“AB:01”),重命名时File.Move会直接抛异常。所以要先清洗:

private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); private static string SanitizeFileName(string input, int maxLength = 80) { if (string.IsNullOrWhiteSpace(input)) return string.Empty; string cleaned = new string(input.Where(ch => !InvalidFileNameChars.Contains(ch)).ToArray()); cleaned = cleaned.Trim(); cleaned = cleaned.Replace(" ", "_"); if (cleaned.Length > maxLength) cleaned = cleaned.Substring(0, maxLength); return cleaned; }

Path.GetInvalidFileNameChars()已经包含了 Windows 和 .NET 层面的所有非法字符,直接用最省事。识别文字里的换行符也要处理掉,我通常在前面替换空格时把\r\n一起替换成空字符串。

为了保留原始文件信息,命名规则不要只放识别文字,我使用{识别文字}_{原文件名前若干位}这种组合方式。比如原文件名IMG_20240312_113045.jpg,识别文字是型号A100,新文件名叫型号A100_IMG_20240312_1130.jpg。这样既能快速辨识,又不容易因为重名冲突。

4.3 重名冲突与目标文件存在检测

批量重命名时最容易踩的坑是重名。两张图可能识别出同一个文字,比如两张发票都写着“快递单号”,如果都改成快递单号_xxx.jpg,第二张就会因为目标文件已存在而失败。

所以重命名逻辑必须检查目标文件是否存在,存在的话自动追加序号:

private static string GetUniqueFileName(string directory, string baseName, string extension) { string candidate = Path.Combine(directory, baseName + extension); int index = 1; while (File.Exists(candidate)) { candidate = Path.Combine(directory, $"{baseName}_{index}{extension}"); index++; } return candidate; }

还有一个更深的坑:如果原文件在D:\tags目录,目标文件名也在同一个目录,那么D:\tags\A.jpg改成D:\tags\A_1.jpg时,必须确保新名字不等于当前正在处理的某个其他文件的原始名字,否则可能出现 A.jpg 先改名,然后处理 B.jpg 时新名字又叫 A.jpg,导致 A 和 B 的文件内容混合。规避方案是先把所有目标文件名全部计算出来,再统一在内存里检查是否有重复目标名,如果有,在批处理开始前就提示用户哪些文件会冲突。

我实测了一轮,最常见的冲突是识别文字全部为空。这时候如果还按_原文件名的规则生成,那所有空结果的文件名都会被改成_IMG_xxx.jpg,出现大量重复。我的方案是:识别文本为空时,保持原名不动,并给该行标记一个颜色,让用户手动处理。

4.4 进度展示与取消机制

批量处理如果卡在一张超大图上,整个程序就像死机。我加了一个CancellationTokenSource,配合按钮“取消”使用。每次调用RecognizeTextFromBytes时传入CancellationToken,SDK 内部支持取消请求。界面上的进度条和 TextBlock 显示“3/78”,让用户心里有数。

这里说明一下,为什么这里要专门讲进度和取消。做批量工具,最烦人的就是任务跑到一半想停停不下来,以及进度条不动让人心慌。我一开始没加取消,跑 100 张图时误点了缩小窗口,程序直接未响应,体验非常糟糕。后来加了async+CancellationToken,整个流程顺畅多了。

5. 处理图片方向与 OCR 精度优化

5.1 EXIF 旋转问题

手机或相机拍出来的 JPG 图片,可能带有一个 EXIF 方向信息(Orientation),但图片本身的像素数据并没有被旋转。Windows 照片查看器和 WPF 会读取 EXIF 自动旋转,但底层 OCR 接口拿到的字节流没有这个信息,所以可能出现“预览图是正的,识别结果却是竖着排”的情况。

解决办法是在裁剪前手动旋转:

public static BitmapFrame NormalizeOrientation(BitmapSource source, uint orientation) { BitmapFrame result; switch (orientation) { case 2: // 水平翻转 result = new TransformedBitmap(source, new ScaleTransform(-1, 1)); break; case 3: // 旋转180 result = new TransformedBitmap(source, new ScaleTransform(-1, -1)); break; case 4: // 垂直翻转 result = new TransformedBitmap(source, new ScaleTransform(1, -1)); break; case 5: // 水平翻转后旋转90(具体取决于实现) result = new TransformedBitmap(source, new TransformGroup()); // 上面例子只是示意,实际请根据 EXIF 标准实现 break; case 6: // 旋转90 result = new TransformedBitmap(source, new RotateTransform(90)); break; case 7: result = new TransformedBitmap(source, new TransformGroup()); break; case 8: // 旋转270 result = new TransformedBitmap(source, new RotateTransform(270)); break; default: result = (BitmapFrame)source; break; } return BitmapFrame.Create(result); }

注意 EXIF 方向字段的值与旋转角度对应关系容易记混。我建议用现成库,比如MetadataExtractor读取Orientation字段,然后调用上面的转换方法。这个处理放在加载图片展示阶段,保证用户框选区域时看到的是正图,裁剪出来发送给 OCR 的也是正图。

5.2 框选区域的适度放大

有些图片上的文字区域像素不够大,直接裁剪识别率不高。一个有效技巧是裁剪时在外围扩充一圈边距,把部分背景也带进去,比如:

int padding = 10; int x = Math.Max(0, (int)region.X - padding); int y = Math.Max(0, (int)region.Y - padding); int width = Math.Min(source.PixelWidth - x, (int)region.Width + padding * 2); int height = Math.Min(source.PixelHeight - y, (int)region.Height + padding * 2); var cropRect = new Int32Rect(x, y, width, height);

这样做的原理是 OCR 引擎对周围有少量背景的文字识别效果通常比纯文字区域更好,因为上下文信息更多。但 padding 不能太大,否则会混入无关文字,导致返回结果变多。实测 10 到 20 像素比较合适。

5.3 置信度过滤

OCR 接口返回的TextDetections每条记录都有Confidence字段,取值范围 0 到 100。如果某条文字识别置信度很低,比如 60 以下,说明结果不可靠,应该过滤掉。我的策略是只取置信度最高的那条作为最终命名文本,并在 UI 上把置信度显示出来:

resp.TextDetections .OrderByDescending(t => t.Confidence) .FirstOrDefault(t => !string.IsNullOrWhiteSpace(t.DetectedText))

这个方法在实践中有个附带好处:如果一张图里同时有型号和二维码识别内容,置信度最高的往往是人类视觉上最显眼的那行,正好符合“提取这个区域最核心文字”的需求。

6. 常见问题与排查技巧实录

6.1 API 签名错误或请求超时

新手最容易犯的错是把 SecretId 填到 SecretKey 位置,或者 SecretKey 末尾多了空格。腾讯云的签名算法校验很严格,多一个空格都会抛 AuthFailure.SignatureFailure 错误。排查思路:先检查密钥复制是否完整;再确认代码里没有对密钥做 Trim;最后确认地域参数是否正确,OCR 服务固定用ap-guangzhou就行。

请求超时方面,如果图片体积太大,Base64 字符串很长,会拖慢接口响应。解决办法是在调用 SDK 时设置更长的超时时间,比如把HttpProfileTimeout设为 30 秒。另外,如果单张图片超过 7MB,需要先用压缩算法降采样再调用接口。我一般把裁剪出来的区域图片做一个等比缩放,让最长边不超过 2000 像素,识别精度基本不受影响。

6.2 批量任务中途失败,如何排查

大批量处理时,如果第 30 张图突然失败,程序应该在日志文件中记录是哪个文件名、失败原因。我在代码里加了一个简单的日志类,把每条识别记录写入 txt:原文件名、识别文本、耗时、是否成功。这样跑完一轮后可以快速定位失败图片,不需要在界面上翻找。

排错日志的格式类似:

2024-03-12 10:15:33 | IMG_2045.jpg | 识别成功 | 型号A100 | 耗时 1.2s 2024-03-12 10:15:35 | IMG_2046.jpg | 识别失败 | 请求超时 | 耗时 30s

如果大量图片连续失败,先检查网络和 API 配额;如果只有特定图片失败,大概率是图片本身有问题,比如损坏或者区域太小。

6.3 区域坐标在缩放图片时的万一致命问题

前面讲坐标换算时,我默认图片在控件里是Uniform缩放。但如果用户手动拖动窗口大小,ImgPreview.ActualWidth会变,换算关系也变了。所以框选结束后,不要再改变窗口大小,或者把选区保存成相对比例(0 到 1 的浮点数),批量识别时再根据原始图片尺寸换算回绝对坐标。相对比例是更稳妥的方式:

public record RegionNormalized(double X, double Y, double Width, double Height); private RegionNormalized DisplayRectToNormalized(Rect displayRect) { double displayWidth = ImgPreview.ActualWidth; double displayHeight = ImgPreview.ActualHeight; return new RegionNormalized( displayRect.X / displayWidth, displayRect.Y / displayHeight, displayRect.Width / displayWidth, displayRect.Height / displayHeight); }

批量处理时再乘回原始宽高。这样即使换了不同分辨率的图片,框选区域也能保持相对位置。

6.4 误把二维码识别结果当文件名

有些标签上二维码紧挨着型号文字,OCR 可能把二维码旁边的小字也识别出来并排在第一位。遇到这种情况,可以在框选区域时尽量缩小范围,只框型号那段文字。如果已经跑完一轮发现结果不对,就在右侧“识别结果”列表里手动编辑文字,再执行重命名,我的工具里加入了 ListBox 项双击编辑功能,不重新调接口,直接改文本。

6.5 免费额度到底够不够

个人整理几百张图,用GeneralBasicOCR免费额度是够的。但如果照片结构复杂,需要调高精度版本,费用会上升。我实测高精度版本对模糊图片有帮助,但对清晰的打印体文字,基础版和高精度版识别结果差别不大,不建议为了省事直接上高精度版,先基础版跑一轮,发现问题再单独重处理。

7. 让工具更顺手:后续可扩展的细节

分享一下我在这套工具上后续加的小功能,算是对“批量图片区域识别改名”这个需求的延伸理解。

首先是一个命名模板功能。不同用户的命名习惯差别很大,有人喜欢识别文字_原文件名,有人喜欢原文件名_识别文字,还有人希望加日期前缀。我加了一个简单的模板输入框,支持{text}{source}{date}占位符,运行时替换成实际内容。比如模板填{date}_{text}_{source},生成的文件名就是20240312_型号A100_IMG_1234.jpg

其次是支持多区域识别。有些标签需要同时提取两个区域,比如型号和批次号,然后拼成型号A100_批次B作为文件名。做法是维护一个List<Rect>区域列表,每个区域可单独命名标签,批量识别时对每个区域分别调用 OCR,最后合并字符串。代码逻辑上并不复杂,就是多一层循环。

最后是导出 CSV。整理完一批文件后,把原文件名、新文件名、识别文本、置信度、处理状态统一导出到 CSV,方便后期做台账。这个功能其实很简单,就是写一个StreamWriter循环输出,但对整理发票、合同扫描件这类场景很有价值。

如果你有大量图片需要定期整理,建议把识别结果缓存到本地数据库或 JSON 文件里,避免每次重复调用 OCR。同一张图片如果只改了命名规则,不需要重新识别,直接读缓存即可。我最初没意识到这点,重复跑了三轮,免费额度差点用完,后来改成缓存方案,速度立刻上来了。

写到最后

这个项目从头到尾做下来,我最大的感受是:“自动改文件名”这个需求本身不难,难的是处理各种边界情况——图片方向、坐标系换算、文件名非法字符、重名冲突、API 调用失败。每解决一个边界问题,工具就离“真正给人用”更近一步。如果你也在找类似的批量图片改名工具,照着这套方案做,基本能覆盖 90% 的场景。上线使用之前,千万别忘了在测试文件夹里放几张不同方向的图片,把最常见的坑提前踩一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询