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

HarmonyOS 6 轻相机应用开发4:用 MindSpore Lite 跑通物品分类

发布时间:2026/9/26 17:12:22

资讯中心
01
ARTICLE

HarmonyOS 6 轻相机应用开发4:用 MindSpore Lite 跑通物品分类

HarmonyOS 6 轻相机应用开发4:用 MindSpore Lite 跑通物品分类
1. 轻相机里的物品分类卡在哪一步HarmonyOS 6 的轻相机应用做到第四篇滤镜、贴纸这些纯图形管线的东西基本都顺了真正让人头疼的是把 AI 推理塞进预览流。物品分类这个需求听起来简单——相机对着杯子、手机、猫框出来标个名字——但落到端侧就有三个硬骨头模型怎么转成 MindSpore Lite 能吃的格式、相机 YUV 数据怎么喂进推理引擎、推理结果怎么在预览层上实时画出来还不掉帧。我试过把 SSD 模型直接丢进工程里跑第一次真机验证时预览直接卡成幻灯片后来才发现是每帧都在重建 Tensor 和内存池。这篇就按「模型转换 → 工程配置 → 取流预处理 → 推理回显 → 真机验证」的顺序把 HarmonyOS 6 上用 MindSpore Lite 跑通物品分类的完整路径拆开讲。适合已经写过相机预览、想接端侧 AI 的鸿蒙开发者也适合从 Android NNAPI 或 TFLite 转过来的朋友对照着看差异。核心检索词先摆清楚HarmonyOS 6 轻相机、MindSpore Lite 端侧推理、SSD 物品分类、NAPI 动态切模型。下面所有代码和配置都是可复制的骨架模型文件需要你自己按业务换。2. 前置准备模型、TaoToken 与工程依赖2.1 模型从哪来、怎么转物品分类用 SSDSingle Shot MultiBox Detector是性价比很高的选择一次前向就能出框和类别。训练侧我不展开假设你已经有一个 320x320 输入的 SSD 模型输出是回归偏移和分类 logits。转 MindSpore Lite 用官方转换工具# 把 ONNX / TF 的 SSD 转成 .ms 格式 ./converter_lite \ --fmkONNX \ --modelFilessd_object.onnx \ --outputFiledetect_regular_nms_quant \ --inputShapeinput:1,320,320,3 \ --quantTypeWeightQuant \ --bitNum8转换完会得到detect_regular_nms_quant.ms。量化这一步很关键8bit 权重量化后模型体积能压到原来的四分之一左右端侧加载和推理都更轻。注意--inputShape要和后面 NAPI 里传的宽高严格一致否则推理会直接报维度不匹配。2.2 用 TaoToken 管理模型与密钥配置工程里如果只是本地放一个.ms文件其实用不到外部服务。但轻相机这类应用往往要支持多模型动态切换模型文件、标签词典、甚至推理服务的鉴权信息都需要一个统一的配置入口。我习惯把这类配置走 TaoToken 的 API Key 体系来管避免把敏感串硬编码进 ArkTS。TaoToken 官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。先在控制台建一个 Key控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite拿到 Key 之后在工程里建一个AiConfig.ets把模型路径、输入尺寸、置信度阈值、以及需要走远端的能力比如标签词典更新统一收口。这样切换模型时只改配置不动推理代码。注意端侧推理本身不依赖网络TaoToken 在这里承担的是配置与密钥管理角色不要把推理主链路设计成必须联网否则相机离线就废了。2.3 工程依赖与目录结构在entry/src/main/cpp下引入 MindSpore Lite 的 so 和头文件CMakeLists.txt里链接find_library(mindspore-lite-lib mindspore-lite) target_link_libraries(entry PUBLIC ${mindspore-lite-lib})模型和标签放resources/rawfile/models/与resources/rawfile/labels/运行时通过resourceManager读进内存。目录大致这样entry/src/main/ ├── cpp/ │ ├── ai_engine.cpp # MindSpore Lite 推理封装 │ └── napi_init.cpp # NAPI 接口 ├── ets/ │ ├── ai/ObjectDetectionUtil.ets │ └── pages/CameraPage.ets └── resources/rawfile/ ├── models/detect_regular_nms_quant.ms └── labels/object_labels.txt3. 可复制配置NAPI 推理引擎与动态切模型3.1 C 侧推理器骨架核心思路是做一个通用推理器基类NAPI 暴露switchAiMode运行时换模型文件、重配输入输出维度、重建内存池。下面是精简后的关键片段// ai_engine.cpp #include mindspore/lite/include/model.h #include mindspore/lite/include/tensor.h class AiEngine { public: bool LoadModel(const std::string path, int w, int h) { model_ mindspore::lite::Model::Import(path.c_str()); if (model_ nullptr) return false; input_w_ w; input_h_ h; context_ std::make_sharedmindspore::lite::Context(); // 线程数按真机核心数调2~4 比较稳 context_-thread_num_ 2; session_ mindspore::lite::LiteSession::CreateSession(context_.get()); session_-CompileGraph(model_); inputs_ session_-GetInputs(); // 按模型实际输入维度重配 inputs_[0]-set_data_type(mindspore::lite::kNumberTypeFloat32); return true; } // 传入已归一化的 float 数据返回输出张量 std::vectormindspore::lite::MSTensor Run(const float* data, size_t len) { memcpy(inputs_[0]-MutableData(), data, len); session_-RunGraph(); return session_-GetOutputs(); } private: mindspore::lite::Model* model_ nullptr; std::shared_ptrmindspore::lite::Context context_; mindspore::lite::LiteSession* session_ nullptr; std::vectormindspore::lite::MSTensor inputs_; int input_w_ 320, input_h_ 320; };NAPI 层把switchAiMode暴露给 ArkTS参数是模型路径和输入宽高// napi_init.cpp 片段 static napi_value SwitchAiMode(napi_env env, napi_callback_info info) { size_t argc 3; napi_value args[3]; napi_get_cb_info(env, info, argc, args, nullptr, nullptr); // 解析 path / width / height调用 engine-LoadModel(...) // 成功后回调 ArkTS 通知 UI 切换完成 return nullptr; }3.2 ArkTS 侧调用与相机取流ArkTS 层一行切换模型和 excerpt 里提到的接口形态一致// CameraController.ets CameraController.switchAiMode(models/detect_regular_nms_quant.ms, 320, 320);相机取流用CameraKit的ImageReceiver拿到 YUV_420_SP 数据后做两件事转 RGB、缩放到 320x320、归一化到 [0,1]。这一步别在 UI 线程做放到 TaskPool 里import { taskpool } from kit.ArkTS; Concurrent function preprocess(yuv: ArrayBuffer, w: number, h: number): Float32Array { // YUV - RGB - resize 320x320 - normalize const out new Float32Array(320 * 320 * 3); // ... 具体转换逻辑 return out; }预处理完把Float32Array通过 NAPI 传给 C 推理结果异步回调回 ArkTS。4. 验证请求SSD 解码、NMS 与结果回显4.1 坐标解码与 NMS模型输出的是相对 Anchor 的偏移得在 ArkTS 层解码。Sigmoid 激活后按阈值筛再算真实框static decodeSSD(reg: number[], cls: number[], anchors: SSDAnchor[], scoreThresh: number): DetectedObject[] { const results: DetectedObject[] []; for (let i 0; i anchors.length; i) { const score 1.0 / (1.0 Math.exp(-cls[i])); if (score scoreThresh) { const p i * 4; const cx reg[p] anchors[i].x_center; const cy reg[p 1] anchors[i].y_center; const w Math.exp(reg[p 2]) * anchors[i].w; const h Math.exp(reg[p 3]) * anchors[i].h; results.push({ x1: cx - w / 2, y1: cy - h / 2, x2: cx w / 2, y2: cy h / 2, score, label: }); } } return NMS(results, 0.45); }NMS 按 IOU 阈值 0.45 抑制重叠框保留置信度最高的。标签从object_labels.txt按类别索引取300 类词典建议预加载成数组别每帧读文件。4.2 Canvas 叠加层绘制结果通过通用 AI 回调返回UI 层在 Canvas 上画框。用Watch监听结果变化只有真变了才重绘避免每帧 clearRect 拖垮 CPUWatch(onAIUpdate) private renderAIOverlay() { if (!this.context) return; this.context.clearRect(0, 0, this.compWidth, this.compHeight); this.detectedObjects.forEach((obj) { const x obj.x1 * this.compWidth; const y obj.y1 * this.compHeight; const w (obj.x2 - obj.x1) * this.compWidth; const h (obj.y2 - obj.y1) * this.compHeight; this.context.lineWidth 3; this.context.strokeStyle #22D3EE; this.context.strokeRect(x, y, w, h); this.context.fillStyle rgba(34,211,238,0.8); this.context.fillRect(x, y - 30, 120, 30); this.context.fillStyle #000000; this.context.fillText(${obj.label} ${(obj.score * 100).toFixed(0)}%, x 8, y - 8); }); }4.3 把识别框拍进照片预览有框、拍照没框是常见坑。用OffscreenCanvas在保存前把框叠到 PixelMap 上const ctx offscreenCanvas.getContext(2d); ctx.drawImage(bgPixelMap, 0, 0, imgW, imgH); if (this.saveAIOverlays) { this.detectedObjects.forEach(obj { ctx.strokeStyle #22D3EE; ctx.lineWidth 4; ctx.strokeRect(obj.x1 * imgW, obj.y1 * imgH, (obj.x2 - obj.x1) * imgW, (obj.y2 - obj.y1) * imgH); }); } const finalPixelMap ctx.getPixelMap(0, 0, imgW, imgH);4.4 真机验证动作跑通后按这个清单验证验证项方法预期分类准确率固定 20 个常见物品逐个拍标签正确率 ≥ 80%单帧耗时打日志统计 Run 前后时间戳320x320 量化模型 ≤ 30ms预览帧率连续识别 60s 看掉帧稳定 ≥ 25fps内存占用DevEco Profiler 看 native heap无持续增长耗时如果超过 50ms先查是不是每帧都在CreateSession正确做法是加载一次、复用 session。5. 本篇常见错排查报错一CompileGraph failed, input shape mismatch模型转换时的--inputShape和 NAPI 传的宽高不一致。回去核对 converter 命令和switchAiMode的参数两边必须都是 320x320x3。报错二推理结果全是同一个标签大概率是预处理归一化错了。SSD 一般要求 [0,1] 或 [-1,1]看你训练时的配置。YUV 转 RGB 时如果通道顺序搞反BGR/RGB分类会整体偏移。报错三预览卡顿、掉帧严重三个排查点预处理是否在 UI 线程、是否每帧重建 Tensor、Canvas 是否每帧无条件重绘。前两个挪到 TaskPool 和复用 session第三个用Watch控制。报错四拍照后框位置偏移预览 Canvas 尺寸和照片实际尺寸比例不一致。绘制时用obj.x1 * imgW而不是预览坐标确保按图片真实宽高缩放。报错五模型加载失败但路径没错.ms文件没放进rawfile或者读取时用了错误的 resourceManager 接口。用getRawFileContent读成 ArrayBuffer 再传给 C别直接传路径字符串。6. 继续往下走物品分类跑通之后轻相机的 AI 能力其实才刚开头。下一步可以试的方向把单模型换成多模型动态切换人脸物品场景用switchAiMode在运行时换.ms文件或者把置信度低的帧丢给远端做二次确认这时候就需要一个稳定的模型服务入口。如果你在接入推理链路时卡在鉴权或配置管理可以直接从 API Keys 页面建 Key 开始https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。想先验证模型对话能力再决定怎么接可以走模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码和 Agent 方向的话Coding Plan 更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。真机验证时记得把日志级别开到 debug把每帧的预处理耗时、推理耗时、绘制耗时分开打这样掉帧时一眼就能定位是哪一段拖后腿。
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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