
HarmonyOS 应用实例系列做到第六个主题从“认识图形”切入很多人第一眼觉得这不过是一个给儿童教育用的 Demo但把它拆开看你会发现这里其实藏着一整条“相机取流 → 图像分类 → AR 叠加标注”的端侧链路。“劳动节假期我重新翻出这期的源码把 AR 分类与识别的实现思路完整重跑了一遍踩了一些坑也把几处容易想当然的地方做了调整。这篇文章就按我的实操顺序来写适合已经能独立写 ArkTS 页面、想把手伸向相机和 AI 能力边界的人也适合那些在 HarmonyOS NEXT SDK 5.0.0(12)也就是 API 12 及以上版本上做端侧视觉应用的初学者。你不需要懂 AR 数学原理但最好能静下心把一个流程走通。”1. 项目拆解一个“认识图形”实例到底在练什么1.1 应用实例六在整个系列里的定位既然是第六个实例前面大概率已经覆盖了基础 UI、状态管理、网络请求这些常规能力。到这一步题目突然变成了“相机 AR 识别”很多人会觉得跳跃太大实际上这是刻意设计的进阶节奏。做 HarmonyOS 应用开发如果永远停留在“页面好看、数据能刷”的层面你会发现与竞品的差异很难拉开因为真正的端侧体验往往长在硬件能力和系统服务的交叉点上。“认识图形”选了三个最基本的几何对象圆形、三角形、四边形。它们特征鲜明、形状规则非常适合作为图像识别入门的训练样本。相比识别人脸、识别物体这类需要深度学习模型支撑的任务几何图形的分类逻辑要朴素得多你甚至不需要引入任何第三方推理框架用传统图像处理里的轮廓分析就能达到很高的准确率。这给初学者留出了理解“图像从哪来、特征怎么提、结果怎么画回去”的完整空间。1.2 分类与识别它不只是一次图像匹配大多数人对“识别”的第一反应是“这东西像不像某个模板”。但真实场景里的分类与识别要比模板匹配复杂得多同一个正方形放在不同光照下拍摄角度不同画面里的大小不同甚至纸张有褶皱投影到传感器上的像素分布就完全不一样。所谓“认识图形”真正要做的是从像素中提取出对平移、旋转、缩放不那么敏感的几何不变量——角点数量、边的长度比例、轮廓的凸性、面积与周长的关系等等。明白了这一点你就会理解为什么很多入门教程会让你把图像转灰度、滤波、二值化、找轮廓而不是直接拿原图去喂网络。这些预处理步骤本质上是在“洗掉”颜色、纹理、噪声这些与形状无关的干扰让分类器只关注结构信息。这也给后续升级埋了一个伏笔如果你某天想识别的不再是几何图形而是猫和狗、手写数字你只需要把特征提取这一层替换成神经网络前面图像获取和后面结果展示的代码几乎不用动。1.3 从手机屏幕到光波导 AR 眼镜的硬件演进很多人在手机上做完 AR 标注觉得不够“AR”因为没有三维姿态跟踪只是把标签贴在相机画面上的固定位置。这种想法可以理解但需要注意AR 的能力边界是由硬件形态决定的。当前这个实例跑在普通手机上体验范本就是“平面叠加”类似学习软件里的拍题、几何测量。而在光波导 AR 眼镜这类穿戴设备上显示模组在眼前半透明成像虚拟标签要“钉”在真实空间的坐标点上这就必须引入空间锚定、姿态估计、SLAM 等能力。做这个实例时我刻意把“识别逻辑”和“AR 渲染逻辑”分开写就是为了以后把识别结果从手机 Canvas 迁移到光波导眼镜的渲染管线时不至于重写核心算法。在 HarmonyOS 这一侧如果你的工程基于 API 12 的权限模型和相机服务来写拿到的是相对干净的图像流和可感知相机姿态的接口后续接 XR 能力会省掉很多重新适配的麻烦。这也是我建议不要在示例里把代码焊死在手机屏幕坐标系里的原因。2. 实现路线选型模型、SDK 与规则算法的取舍2.1 路线一直接用系统内置识别服务HarmonyOS 系统能力里自带了一批视觉相关的服务包括二维码识别、文本识别、人脸检测等。如果你把题目改成“认识二维码”“认识银行卡号”直接调系统服务是最快的方式不需要自己训练模型也不需要处理复杂的像素格式API 封装得相当干净。但这个方案放到“认识图形”上就很别扭。系统内置服务里没有“识别圆形和三角形”这个类别它们面向的是强语义目标比如文字有笔画结构、二维码有定位角点而几何图形是一类非常抽象的低层视觉特征。你无法通过配置参数让 OCR 模型“顺便”知道画面里有个三角形。所以这个路线很快被我排除了但它给我们的启发是动手选型前先翻一遍官方 Kit 列表如果系统已经给了现成能力就别重复造轮子如果没有再考虑自研。2.2 路线二端侧 AI 分类模型端侧跑一个图片分类模型是当前视觉应用的常见做法。比如在 MindSpore Lite 或 ONNX Runtime 上部署一个用 MNIST 风格训练过的模型把“三角形、圆形、正方形”作为三个类别输入约定尺寸的灰度图输出分类概率。这种方案的最大优势是泛化能力强同一个流程可以直接迁移到“数字识别”“手势识别”等其他题目上代码框架不变换的是训练数据和模型文件。代价也很明显你得解决数据集、标注、训练、量化、模型转换、端侧推理等一系列问题。单是做一个“认识图形”的 Demo 就引入模型训练链路我个人觉得是杀鸡用牛刀。尤其当模型在真机上出现预测不稳定的情况时调试复杂度会突然上升有时候分类错误你甚至分不清是因为光照、图片缩放、还是训练集里某种图形本身就太少。如果你的目标是快速验证一个端侧应用的闭环这条路并不友好。2.3 路线三基于轮廓特性的几何规则分类不依赖任何机器学习框架纯靠图像处理规则来完成分类是我最终选择的主路线。思路很直接先对灰度图做边缘检测找到画面里所有封闭连通域把这些连通域的轮廓提取出来然后统计轮廓上的凸包点数量。三角形的凸包点数是 3四边形是 4而圆形没有明显角点轮廓整体接近椭圆可以根据“角点数”和“边缘平滑度”很自然地区分。这条路线对“认识图形”这个题目来说准确率高、计算量小、不存在模型文件加载问题而且算法的每一步都可以在调试器里可视化观察——比如把轮廓点画出来看看是不是真的找对了。这在教学实例里是极有价值的因为你能明确知道分类失败的原因出在预处理还是出在特征判断而不是面对一个神经网络里说不清道不明的隐层。2.4 最终方案规则分类 预留模型接口的混合架构把三条路线的优缺点放在一起对比之后我的最终取舍是主流程使用规则分类器但在代码结构上预留下一个可以替换的“分类器接口”。也就是说页面里不直接调用某个封装好的函数去判断图形而是通过一个ShapeClassifier接口来调用规则算法作为默认实现。这样切除模型扩展方案的接口成本几乎是零。这里想多说一句选型的心法不要因为“AI 热门”就硬上模型。一个具体场景里到底用规则还是模型应该看输入变化是否可控。几何图形是人工绘制的、背景相对简单、类别明确固定规则分类是最合理的技术债选择。如果你未来识别手写数字、自然场景里的交通标志那再切换成模型方案也不迟而且在切换过程中你会发现前面做的图像预处理、ROI 提取、后处理逻辑大部分都能继续复用。3. 关键代码实现从相机帧到实时 AR 标注3.1 工程准备SDK 版本与权限声明项目基于 HarmonyOS NEXT SDK 5.0.0(12)也就是常说的 API 12 及以上版本创建工程。如果你从应用市场下载的示例源码版本较低需要留意 API 12 对权限模型做了收紧相机类的敏感权限不再允许在module.json5里只写一个名字就完事还需要补充reason申请理由和usedScene使用场景的描述。我用的是下面这份配置你直接抄过去时记得把 reason 改成自己应用的实际说明否则上架审核会卡{ module: { requestPermissions: [ { name: ohos.permission.CAMERA, reason: 用于拍摄图形画面以完成识别与标注, usedScene: { abilities: [EntryAbility], when: inuse } } ], requestPermissionsTest: [ { name: ohos.permission.INTERNET, reason: 用于后续模型文件下载与调试, usedScene: { abilities: [EntryAbility], when: always } } ] } }这里有一个容易忽略的细节在 API 12 的权限模型下ohos.permission.INTERNET如果只是调试用可以放到测试权限集里但如果你后续要从网络拉取模型就必须让它在正式权限集里声明否则运行时会出现网络请求直接被拦截的情况。一开始我就因为这个原因排查了很久因为正式包和 Debug 包表现不一致。工程建好后建议先在模拟器里跑通一个最简单页面再开始接相机。模拟器对 Camera 的支持目前仍然有限你会遇到相机启动报错或画面黑屏的问题所以拿到相机后最好直接上真机调试。配置真机的方式不在这里展开DevEco Studio 的连接向导已经做得很完善签上调试证书就能跑。3.2 拿到相机预览帧ImageReceiver 的正确姿势做 AR 识别必须先回答一个问题识别算法的“输入”到底是什么答案是相机产生的每一帧图像。HarmonyOS 的相机框架里ImageReceiver是一个专门接收图像帧的组件它不负责显示只负责把连续帧转成可操作的数据对象。你需要在创建相机会话之前就创建它因为相机输出目标的 Surface 要和ImageReceiver的 Surface 绑定。这里给出简化后的核心流程import { camera } from kit.CameraKit; import { image } from kit.ImageKit; import { BusinessError } from kit.BasicServicesKit; // 1. 创建 ImageReceiver宽高选 640x480JPEG 格式缓冲区 8 帧 let receiver image.createImageReceiver(640, 480, image.ImageFormat.JPEG, 8); let receiverSurfaceId await receiver.getReceivingSurfaceId(); // 2. 获取相机管理器与默认摄像头 let cameraManager camera.getCameraManager(this.context); let cameras cameraManager.getSupportedCameras(); let cameraInput cameraManager.createCameraInput(cameras[0]); await cameraInput.open(); // 3. 创建预览输出绑定到 ImageReceiver 的 Surface let previewOutput cameraManager.createPreviewOutput(receiverSurfaceId); // 4. 创建会话并配置输入输出 let session cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO); session.addInput(cameraInput); session.addOutput(previewOutput); await session.commitConfig(); await session.start();几个非常容易踩的点第一ImageReceiver的缓冲区数量不是越大越好8 帧在识别场景下已经足够过大会导致内存高涨第二在拿到ImageReceiver的 Surface 后再创建相机会话顺序不能反否则预览输出绑定不成功第三相机会话结束后一定要在页面onDisappear里关闭相机输入、释放ImageReceiver否则再次进入页面时会直接黑屏这是我在真机上反复遇到的第一大问题。从ImageReceiver拿帧的方式一般是在回调里receiver.on(imageArrival)监听接收到帧之后通过receiver.readNextImage()拿到NativeImage再转换成 PixelMap 或直接读取原始字节数组。为了做图像处理我通常选择读取 JPEG 的字节流然后用轻量解码方式转成 RGB 或灰度数据。receiver.on(imageArrival, () { receiver.readNextImage((err: BusinessError, img: image.Image) { if (err || !img) { console.error(readNextImage failed: err?.code); return; } let pixelMap img.getComponent(image.ComponentType.JPEG); // 拿到语义化数组 buffer 后交给后面的分类线程处理 handleFrame(pixelMap.byteBuffer); img.release(); }); });注意img.release()一定不能漏。这个释放动作是把图像缓冲区还给底层相机服务不释放的话跑到第几十帧时会发现readNextImage开始超时或返回空。调试时打开 Memory Profile 能看到明显增长。3.3 图形分类核心算法灰度化、轮廓提取与角点判断拿到帧数据之后分类算法的体重就体现出来了。我的实现顺序是先做灰度化再做二值化然后找连通域提取轮廓最后用凸包角点数判断类别。灰度化在 HarmonyOS 的相机输出里如果你直接读 YUV 数据那么 Y 分量本身就是一个合法的灰度图。不需要再单独转换 RGB 再算灰度。很多人一上来就把 JPEG 解码成 RGBA再做一次灰度化白白浪费了好几倍的 CPU 时间。我建议如果性能敏感优先走 YUV 的 Y 通道。二值化几何图形是线条画出来的亮度与背景差异通常比较大适合使用自适应阈值。固定阈值 128 在光照好的时候没问题但一旦画面里有点阴影效果会迅速劣化。我用的是一种简化的均值阈值把整个 ROI 区域的像素值取平均再乘一个系数作为分割阈值系数根据实测调整到 0.85 附近。你也可以用大津法OTSU计算量略大但更稳定。轮廓提取经过二值化后图形区域会变成一个连通的白色块。你不需要引入复杂的分水岭或边缘检测库做一个简单的连通域标记就能把所有候选区域找出来。用 8 连通的方式从左上角扫描像素遇到未标记的前景像素就做 BFS/DFS 蔓延把整个连通域打成同一个编号同时记录它的外接矩形和像素面积。这一步在原生 ArkTS 里用数组模拟队列并不难代码量大概小几十行。找到连通域以后再沿每个连通域的边界走一圈得到轮廓点序列。判断图形的关键信息就藏在这一串点里。enum ShapeType { UNKNOWN 0, TRIANGLE, QUADRILATERAL, CIRCLE, ELLIPSE, } function classifyByOutline(outline: Point[], area: number): ShapeType { // 如果面积太小很可能是噪点直接过滤 if (area MIN_AREA_PIXELS) { return ShapeType.UNKNOWN; } let hull convexHull(outline); if (hull.length 3) { // 三角形 return ShapeType.TRIANGLE; } if (hull.length 4) { // 进一步判断正方形和矩形的比例此处直接归为四边形 return ShapeType.QUADRILATERAL; } if (hull.length 6) { // 角点多且分布均匀偏向圆形 let perimeter outline.length; let circularity (4 * Math.PI * area) / (perimeter * perimeter); if (circularity 0.85) { return ShapeType.CIRCLE; } } return ShapeType.UNKNOWN; }代码里的convexHull()是经典的凸包算法Andrew 单调链展开也就三四十行。判断圆形用了一个物理里常见的“圆度”公式面积与周长的平方之比。圆形在所有图形里拥有相同周长下最大的面积因此圆度最接近 1正方形的圆度约 0.785三角形更低。加上凸包点数约束分类结果很可靠。这里有一个容易被新手忽略的点手绘几何图形的线条通常不会完美封口有时轮廓上会多出一个小缺口导致连通域分裂或者轮廓上出现毛刺。处理方法是在轮廓提取之前做一次形态学闭运算相当于把细小的断口补上。闭运算的内核我用了 3x3跑在 640x480 的图像上开销可以接受。如果不做这一步连通域会从 1 个变成 5 个、10 个后续分类就全乱套了。3.4 离线先用 matplotlib 验证算法再搬进 ArkTS这部分经验是我踩过几次坑之后总结出来的。图像处理算法的调试在真机上非常痛苦因为你没法在日志里直观看到“这个轮廓到底长什么样”。我的做法是先在电脑上把核心算法用 Python 快速实现一遍输入自己拍摄的测试图片输出一张检测结果图确认无误后再把逻辑翻译成 ArkTS。在 Mac 上跑 Python 验证时记得先处理中文字体问题import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [PingFang HK] # Mac 可用的中文字体 plt.rcParams[axes.unicode_minus] False # 解决负号显示异常然后把你检测到的轮廓点画出来用不同颜色标记每个连通域的凸包顶点旁边标注“三角形”“圆形”“正方形”。这个过程能帮你快速判断阈值是不是合理。我最初把面积阈值设得偏低结果画面远处一张纸片的阴影也被当成图形识别出来在图上看到后立刻能意识到问题。这个离线验证流程表面上是在“用 Python 写一遍”实际上给你省的是真机编译时间。每次在 DevEco Studio 里改一个参数编译到安装运行要几十秒而离线脚本修改参数后刷新图片几乎零成本。我建议你把测试集固定成四五张照片包含不同角度、不同方向、不同大小在电脑上先跑通所有样本再上真机准确率会有明显提升。另外若你后续想用机器学习模型替换规则分类器这个 Python 脚本也可以顺势变成训练脚本的预处理工具比如对图像做统一裁剪、归一化、数据集划分一套沉淀下来的调试工具能复用到多个项目。3.5 AR 标注叠加坐标映射与 Canvas 渲染分类得到结果之后还要把它“显示”到相机画面上。这里有两种路径。第一种是轻量 Canvas 叠加方案不依赖 AR Engine。实现方式是把相机预览铺满整个页面预览上层覆盖一个全透明的 Canvas把识别到的轮廓点坐标换算成 Canvas 坐标然后绘制边框和文字。由于相机预览帧的分辨率、旋转角度和屏幕显示分辨率不一定一致直接拿识别坐标去 Canvas 画肯定会错位核心问题是坐标映射。我封装了一个映射方法思路是拿到相机输出的裁剪区域CropRect和旋转信息ImageRotation再与显示预览的控件尺寸做缩放function mapToDisplay( point: Point, cropRect: CropRect, rotate: ImageRotation, previewWidth: number, previewHeight: number ): Point { let normX (point.x - cropRect.left) / (cropRect.right - cropRect.left); let normY (point.y - cropRect.top) / (cropRect.bottom - cropRect.top); // 根据旋转角交换坐标 if (rotate ImageRotation.ROTATE_90) { let tmp normX; normX normY; normY 1 - tmp; } else if (rotate ImageRotation.ROTATE_270) { let tmp normX; normX 1 - normY; normY tmp; } return { x: normX * previewWidth, y: normY * previewHeight, }; }这个映射函数解决了 90% 的错位问题。如果你发现标签画出来还是对不上请优先检查运行时ImageRotation的值——不同的摄像头方向、不同机型这个值可能完全不同。第二种是真正的 AR Engine 路径。在 HarmonyOS 的 AR 能力里你可以把识别结果作为虚拟对象放置到相机坐标系的世界锚点上利用陀螺仪和视觉特征实现跟踪。问题在于这个实例目前做的是二维平面识别没有深度信息强行挂到三维世界坐标里反而容易漂移。所以我只在工程里保留了接口没有在真机上启用。等以后切光波导 AR 眼镜时再把这部分接上会是一个比较合理的技术演进。3.6 识别与渲染的运行节奏线程、节流与刷新运行节奏是很多类似教程最不爱写、但实际上最影响体验的部分。如果你在imageArrival回调里直接做连通域分析再把结果直接画到 Canvas 上你会发现画面一卡一卡的因为每个回调都占用了大量 CPU而且 Canvas 刷新频率往往跟不上相机的帧率。我的做法是单独开一个 Worker 线程来处理图像帧。主线程收到imageArrival后做的唯一事情是“把缓冲区指针通过消息队列发给 Worker”。Worker 里做灰度化、二值化、轮廓提取和分类耗时大约在 20 毫秒到 50 毫秒之间取决于图片尺寸和连通域数量。分类结束后Worker 把结果发回主线程主线程再更细 Canvas。同时要对识别频率做节流不需要每帧都识别。人的视觉对 200 毫秒一次的画面刷新已经感觉流畅对于“认识图形”这个场景识别频率 5 帧/秒足够。我在代码里加了一个时间戳判断距离上次识别不足 200 毫秒就直接丢弃新帧这样 CPU 占用平均下来会非常低。还有一个小细节Canvas 上的标签在识别失败时不要立刻消失可以保留上一帧的结果并降低透明度否则画面上的标注会不断闪烁。这个“保留上帧结果”的技巧在线框图识别、物体检测等场景都非常常用。4. 真机调试中的常见问题与排查记录4.1 AR 服务起不来错误码 40 怎么查我在调试时也遇到过 AR 相关服务启动失败的情况报错信息里会带一个错误码比如常见到“错误码 40”。第一次遇到时我以为是权限没配置后来发现这是一个非常综合性的错误提示它出现时通常指向以下问题中的某一个。排查顺序建议按照从“系统能力”到“应用配置”来第一确认真机是否在支持的设备列表里模拟器与部分低端机型不支持完整的 AR 能力第二确认系统里是否预置了 AR 引擎服务如果使用的定制 ROM 裁剪掉了这个服务应用层怎么配置都没用第三确认相机权限是否真的弹窗授权了有时候开发者模式下自动授权会绕过弹窗但实际权限状态是拒绝第四确认工程里的 SDK 版本和依赖版本是否匹配不要把 API 11 的旧依赖直接拉进 API 12 工程。如果你的应用走的不是 AR Engine而是我前面说的 Canvas 轻量叠加方案那这个错误基本不会出现因为整套流程不使用系统 AR 服务。但这里也提醒一点一旦你要扩展比如后面接光波导 AR 眼镜就得把系统 AR 能力作为强依赖设备兼容性调研必须提前做。4.2 相机黑屏、收不到帧、页面卡顿“相机启动成功但页面黑屏”是最高频的问题。根据我的排查经验绝大多数情况都和生命周期管理相关。一个常见场景是页面从后台回到前台时相机会话已经在onDisappear里被释放了但你没有在onAppear或恢复逻辑里重新开启画面自然一直是黑的。处理方式是把“创建相机会话”提取成一个可重入的方法在首进和回前台时都调用。还有一个更隐蔽的问题来源于ImageReceiver创建时序。如果ImageReceiver是在相机会话创建之后才去获取 Surface可能拿到的 Surface 已经过期或与底层配置不一致。请务必保证image.createImageReceiver在createPreviewOutput之前完成。收不到帧的问题则多半出在 ImageReceiver 尺寸和相机支持的输出尺寸不匹配上。你设置 640x480但相机传感器可能不支持这个精确尺寸系统会做裁剪或缩放导致注册的 Surface 一直拿不到数据。解决办法是创建ImageReceiver后先查一下cameraManager.getSupportedOutputCapability()选择一个列表里存在的尺寸而不是硬写一个常见分辨率。我之前就遇到过某些机型完全不支持 640x480 的 JPEG 输出。页面卡顿则要检查你是否在imageArrival回调里做了耗时操作。只要把识别算法移到 Worker并把 Canvas 刷新节流到 5Hz卡顿问题基本能消失。如果用的是 ArkTS 的CanvasRenderingContext2D还要留意不要在绘制时频繁创建Path对象尽量复用同一套笔触属性对象。4.3 识别不准光照、角度与面积过滤规则分类器在识别不准时问题通常出在输入图像质量而不是分类逻辑本身。最常见的场景是画面偏暗或偏亮导致二值化后圆形和背景粘连轮廓跟着变形。我给的建议是不要在原始灰度上直接做固定阈值换成自适应阈值如果光照实在不均匀可以先对灰度图做高斯模糊让亮度过渡平滑后再做阈值切分。其次是拍摄角度问题。手机斜着拍桌面上的图形投影会变成透视变形的椭圆或其他形状凸包角点数有时也会受到透视影响。好在几何图形的透视变形在大多数角度下仍保留基本拓扑特征三角形的凸包点数依然是 3正方形的凸包点数依然是 4但“判断它是不是正方形”这一步会因为边比例变化而失真。所以我在四边形分类里只输出“四边形”不强行判断是正方形还是长方形。如果你希望输出二级分类就要引入平行约束和角度约束比如四条边两两平行且相邻边相等才判定为正方形。这是一个合理的复杂度升级方向建议作为后续练习。面积过滤参数也值得单独说。我在代码里设置了一个MIN_AREA_PIXELS在 640x480 的图像下大致取 5000 像素。这个值太小会把桌面纹理、污渍识别成图形太大会漏掉远处的小目标。实际调试时可以把它可视化出来在离线脚本里画一下外接矩形看哪些区域会被错误保留再决定阈值。4.4 标注位置漂移的坐标换算解法标签画上去之后和图形对不准比识别失败更让人崩溃。很多人的第一反应是“计算有问题”但实际上是坐标映射丢失了一个关键环节图像旋转。相机传感器在手机里通常是横向放置的而屏幕是竖屏所以底层的 YUV 或 JPEG 数据并不是你眼睛看到的画面方向如果不按ImageRotation做转换标签会整体偏移 90 度甚至 180 度。建议在代码里加一段调试模式把相机原图完整绘制到 Canvas 上不裁剪、不缩放只在四角标出原始坐标原点然后把识别出来的轮廓点也画上去。如果轮廓点和图形在原图上是对齐的就说明算法正确剩下只是显示层映射问题如果原图上就对不齐那就是图像处理阶段的问题。这一招能把问题快速一分为二省掉大量无头绪排查时间。另一个常见锅是预览控件和相机输出比例不一致。如果页面里的预览组件用了Content模式系统会自动裁剪一部分画面去适配宽高比此时你拿到的图像数据和屏幕上可见内容并不是一一对应。正确做法是让预览组件和ImageReceiver使用相同的宽高比或者自己计算一比一的裁剪映射而不是简单乘个缩放系数。在 API 12 里预览控件的显示区域可以拉成和相机输出比例一致的尺寸这是最省心的办法。4.5 常见问题速查表问题现象可能原因排查与解决相机启动报错或黑屏权限未授权、会话重复创建、生命周期未释放检查权限弹窗提取创建会话方法onDisappear释放相机资源ImageReceiver 收不到帧创建时序不对、尺寸不受支持在创建相机输出前创建 Receiver查询支持输出尺寸后设置识别结果频繁闪跳没有做节流或没有保留上帧结果限制识别频率到 5Hz失败时显示上帧结果并降低透明度标注位置整体偏移未处理 ImageRotation 或裁剪区域实现坐标映射函数处理 90/270 度旋转修正 CropRect阅读标签与图形错位但有偏移预览控件画面裁剪让预览尺寸与 ImageReceiver 比例一致分类准确率低光照不均匀、目标太小、轮廓断裂自适应阈值过滤小面积连通域闭运算修补轮廓内存持续增长Image 未 release每次 readNextImage 后调用 img.release()这张表我建议直接贴在工程注释里后续换设备、换型号调试时快速对照。这个实例最大的价值不是那几行识别代码而是让你体验了一遍“从物理世界的画面到应用里可计算的数据再反馈成物理世界的虚拟标注”的完整回路。我个人的体会是做端侧视觉应用最难的不是单个算法而是摄像头数据如何高效流转、坐标如何在多个坐标系之间准确切换、以及各个线程之间如何不互相阻塞。把这一圈跑通之后以后无论你是接系统 OCR、接端侧模型、还是接光波导 AR 眼镜都会觉得路子是通的。最后再补一个小技巧把这个实例的识别频率降到 1Hz 左右拿它去扫描杂志上的圆形标题和方形色块你会发现“认识图形”其实可以变成“认识版式”它在很多文档自动化场景里还有不少发挥余地。