在 .NET MAUI 里接入 PP-OCRv6 文本检测实现查找并点击 目录1. 背景2. 接入过程2.1 Python先行快速验证2.2 迁移到 MAUI2.3 画框的学问2.4 坑四点排序3. 查找并点击4. 最后这篇文章介绍了如何在 .NET MAUI 中接入 PP-OCRv6 完整的 OCR 能力实现文本检测与识别。解决 FlowBot 开发中更实际的问题怎么先找到文字按钮在哪再把它点下去。1. 背景在之前的文章 在 .NET MAUI 里接入 PP-OCRv6 离线文字识别我们已经把 PP-OCRv6 的识别模型接进了 FlowBot已经可以对一块截图区域做离线识别。这个能力本身是成立的用来做“文字条件判断”也没问题。但真到自动化场景里很快会遇到一个更麻烦的问题很多时候我们并不知道文字的精确位置只知道屏幕上大概会出现“下载”“跳过”“立即领取”这类词希望程序自己找到它然后再去点击。这时候如果还只靠 rec 模型就行不通了。因为识别模型本质上是拿一张已经裁好的单行文字图去读内容它不是拿来在整张手机截图里“找字”的。直接把整屏或者一个大区域喂给 rec那必然是不靠谱的。所以后面的能力必须补上文本检测也就是 det。让模型先回答“字在哪”再继续交给识别模型回答“字是什么”最后才能把“查找并点击”这件事真正做成。完整接入OCR当然最完整的还需要 cls 模型来判断文字方向但在 FlowBot 里我暂时没有接 cls。原因很简单手机界面上的文字基本都是横排旋转的情况极少所以先不接 cls 也不会影响大部分场景。2. 接入过程从业务上看这次接入的目标其实很明确对当前屏幕截图或者一个可选搜索区域截图运行 det 模型。得到一批文本候选四点框。对每个四点框做透视裁剪裁出更接近单行文字的图块。把裁剪结果交给 rec 模型识别。用目标关键词去匹配识别结果。匹配成功后点击这个四点框的几何中心点。对应到 FlowBot 里的最终能力就是“查找并点击”步骤在 OCR 模式下的执行流程。这里有个关键认识det 和 rec 不是两个平行功能而是一条串起来的流水线。det 负责定位rec 负责理解少一个都不太行。2.1 Python先行快速验证我这次还是先走了一遍 Python 验证。原因和上一篇一样先把模型和后处理链路单独验证清楚再往 MAUI 里搬排查起来会轻松很多。当然这次做的收益也很明显直接发现了处理上的问题刚开始的第一版测试发现整体的识别率不高位置虽然对但是识别内容乱码置信度也很低。经过排查发现是裁剪出来的图片被旋转了180度喂给 rec导致识别结果完全不对。最终经过调试和排错最终做成了一个完整的run_ppocr.py默认走small模型组也可以切到tiny。模型文件是这样组织的script/PP-OCRv6_small_det_onnx.onnx script/pp_ocrv6_small_rec.onnx script/PP-OCRv6_small_rec_inference.yml命令行用法也尽量保持简单python script/run_ppocr.py script/test.png python script/run_ppocr.py script/test.png --compare-output script/run_ppocr_compare.png python script/run_ppocr.py script/test.png --json-output script/run_ppocr_result.json这个脚本里det 输入和 rec 输入是两套预处理det 会先按最长边限制缩放再补齐到 32 的倍数。rec 则是固定高度48宽度按比例自适应。det 预处理大致如下def preprocess_det(image: Image.Image, limit_side_len: int) - tuple[np.ndarray, tuple[int, int]]: rgb_image image.convert(RGB) original_size rgb_image.size resized_width, resized_height resize_to_multiple_of_32(rgb_image.width, rgb_image.height, limit_side_len) resized rgb_image.resize((resized_width, resized_height), Image.Resampling.BICUBIC) array np.asarray(resized).astype(float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypefloat32) std np.array([0.229, 0.224, 0.225], dtypefloat32) array (array - mean) / std array np.transpose(array, (2, 0, 1)) return np.expand_dims(array, axis0).astype(float32), original_size这个均值和方差跟上一篇 rec 的0.5 / 0.5归一化不一样。也就是说det 和 rec 虽然都属于 PP-OCRv6但前处理不能偷懒混用。Python测试2.2 迁移到 MAUI模型文件本身接进 MAUI 其实不难和上一篇类似仍然是放到Resources/Raw第一次使用时复制到 cache 目录再用InferenceSession加载。这次新增的是检测模型src/FlowBot.App/Resources/Raw/det_inference.onnxAndroid 运行时里我额外加了一个PpOcrDetector和PpOcrRecognizer分开管理private const string PpOcrDetModelAssetName det_inference.onnx; private const int PpOcrDetLimitSideLen 960; private const float PpOcrDetThreshold 0.2f; private const int PpOcrDetMinArea 16; private const float PpOcrDetUnclipRatio 1.4f; private static readonly LazyPpOcrDetector PpOcrDetectorInstance new(CreatePpOcrDetector); private static readonly LazyPpOcrRecognizer PpOcrRecognizerInstance new(CreatePpOcrRecognizer);真正麻烦的部分不在session.Run而在 det 输出以后怎么把分数图变成可用的文字框。我这里没有直接照搬 PaddleOCR 的全部 C/Python 后处理而是做了一套更适合当前项目、但行为足够接近的版本对输出分数图按阈值二值化。提取连通域。过滤太小的区域。提取边界点。做凸包。按unclip思路外扩多边形。求最小外接四边形。二值化这一步很直接for (var y 0; y height; y) { for (var x 0; x width; x) { mask[y, x] output[0, 0, y, x] PpOcrDetThreshold; } }后面我没有直接输出轴对齐矩形而是尽量保留为四点框。原因也很简单手机界面上的文字不总是完全水平哪怕只是轻微倾斜四点框也比普通矩形更接近真实文字区域。2.3 画框的学问如果只是想“画个框看起来差不多”普通矩形当然更省事。但一旦后面要把这个框拿去做 rec矩形的问题就会马上暴露出来框太大会把背景、图标、边缘噪声一起裁进去。框方向不对会让本来横着的字被切歪。多行或斜排文本会让识别模型输入分布明显变差。FlowBot 这次接 det不是为了做一个“能看到框”的展示功能而是为了支撑后续点击和判断。所以框的几何质量直接影响识别质量。也正因为这样后处理最后落成了四点框而不是简单left/top/right/bottom。2.4 坑四点排序这次调试里最容易让人误判的一点是有时候 det 看起来已经框住了目标文字但 rec 结果还是一塌糊涂。一开始很容易怀疑是这些问题det-thresh太低或太高。unclip-ratio不合适。检测框不够贴边。模型本身对手机截图不稳定。但我后面把中间裁剪图导出来一看才发现真正的问题根本不在阈值而在透视裁剪的点顺序。如果四点框的顺序错了透视变换出来的文字图会被镜像、翻折或者局部扭曲。rec 模型拿到这种输入当然不可能正常识别。最终稳定下来的排序逻辑是private static PointF[] OrderQuad(IReadOnlyListPointF points) { if (points.Count ! 4) { throw new InvalidOperationException($期望 4 个点实际为 {points.Count} 个。); } var topLeft points.OrderBy(point point.X point.Y).First(); var bottomRight points.OrderByDescending(point point.X point.Y).First(); var topRight points.OrderByDescending(point point.X - point.Y).First(); var bottomLeft points.OrderBy(point point.X - point.Y).First(); return [topLeft, topRight, bottomRight, bottomLeft]; }这个顺序看起来只是个小细节但它实际上决定了后面整条 OCR 流水线是不是成立。在这个开发过程中让我深有体会的是做 det rec 集成千万不要只盯着最终识别文本看。把中间裁剪图导出来是定位问题最快的方法之一。因为它能很快回答一个更基础的问题到底是 det 框错了还是 crop 错了还是 rec 真不行。3. 查找并点击光把 det 模型跑起来其实还不算完成。因为对于 FlowBot 这种自动化工具模型能力最终还是要落到一个具体动作上。我这次把它落成了一个新的步骤类型public enum AutomationStepKind { TextCondition, RepeatUntilText, Swipe, Tap, Delay, Tone, Vibrate, FindAndTap }同时保留两种文字来源public enum TextRecognitionMode { AccessibilityText, PpOcrV6Small }这样FindAndTap在执行时就可以按识别方式分流public TaskFindAndTapResult FindAndTapAsync( ScreenRegion region, TextRecognitionMode recognitionMode, string? className, string? text, int timeoutMilliseconds, CancellationToken cancellationToken) { return recognitionMode switch { TextRecognitionMode.PpOcrV6Small FindAndTapWithOcrAsync(region, text, timeoutMilliseconds, cancellationToken), _ FindAndTapWithAccessibilityAsync(region, className, text, timeoutMilliseconds, cancellationToken) }; }OCR 模式下的核心循环其实很简单截图。先 det。每个框做 crop。每个 crop 做 rec。文本命中就点击。foreach (var quad in detector.Detect(searchBitmap)) { usingvar quadBitmap CropQuadBitmap(searchBitmap, quad); var actualText recognizer.Recognize(quadBitmap); if (!ContainsText(actualText, expectedText)) { continue; } var globalQuad quad.Select(point new PointF(point.X offsetX, point.Y offsetY)).ToArray(); var centerX (int)Math.Round(globalQuad.Average(point point.X)); var centerY (int)Math.Round(globalQuad.Average(point point.Y)); await service.TapAsync(centerX, centerY, cancellationToken); returnnew FindAndTapResult(true, $OCR 命中“{actualText}” 点击 ({centerX},{centerY})); }这里点击的不是普通矩形中心而是四点框几何中心。这样做的好处是如果检测框本身有一点倾斜或者旋转点击点仍然会更贴近真实文字区域的中心。验证测试4. 最后通过这两篇文章我分享了在 .NET MAUI 里接入 PP-OCRv6 的完整过程从单纯的识别到检测 识别再到查找并点击。希望能对大家在移动端自动化和 OCR 集成上有所帮助。这两篇文章的代码和思路都是在 FlowBot 项目里实践过的已经在实际场景中验证了可行性。关于 FlowBot 后续整理好后会开源届时大家可以直接参考和使用。引入地址