HarmonyOS 6 自定义人脸识别模型10:基于MindSpore Lite框架的自定义人脸识别功能实现

HarmonyOS 6 自定义人脸识别模型10:基于MindSpore Lite框架的自定义人脸识别功能实现

1. 引言与背景

通过前面几篇文章的摸爬滚打,我们已经学会了 MindSpore Lite 的基本套路 ,也搞定了 第三方 TFLite 模型转 .ms 的绝活。光说不练假把式,今天这篇就是系列的**"终局之战"**!

我们要把前面学到的东西全部串起来,基于真实的业务项目 RealTimeFaceDetection,实现一个实时运行的人脸检测相机应用。前置摄像头一开,我们的脸走到哪,红色的追踪框就跟到哪,完全不依赖系统自带的 Vision Kit,用咱们自己的模型硬刚!

2. 整体打通:相机画面内容流转流程图

在发车看代码之前,我们先得理清整个应用架构的"血液"是怎么流动的。因为涉及到相机硬件捕捉和底层 AI 高强度计算,我们不能把活全推给 ArkTS,必须是 C++ 扛起算力大旗,ArkTS 负责貌美如花(UI展示)

下面这个流程图,清晰地展示了从你打开相机到屏幕画出红框的完整生命周期:

大白话总结一下:C++ 在后台拼命截图算数据,算出结果后"咻"的一下通过 NAPI 扔到前台,ArkTS 接住数据飞速地在屏幕上画个框。

3. Native 核心一:初始化模型与大起底输入输出

任何深度学习模型都是个"娇贵"的黑盒,你不按它的口味喂数据,它分分钟死机崩溃给你看。我们先来看 C++ 侧怎么在应用启动时把它唤醒。

模型加载与上下文配置代码

这一段位于 FaceDetector.cpp,负责从我们打包进来的资源文件中读取 .ms 模型包。

cpp 复制代码
int FaceDetector::Init(NativeResourceManager* resourceManager, const std::string& modelPath) {
    size_t modelSize;
    // 1. 从 RawFile 里把编译好的 .ms 文件读进内存大 Buffer
    void* modelBuffer = ReadModelFile(resourceManager, modelPath, &modelSize);
    if (modelBuffer == nullptr) return -1;

    // 2. 创建上下文,重点:开启 FP16 半精度加速!
    context_ = OH_AI_ContextCreate();
    auto cpu_device_info = OH_AI_DeviceInfoCreate(OH_AI_DEVICETYPE_CPU);
    // 大白话:这句代码能让模型在 ARM 芯片上跑得飞快,且几乎不掉精度
    OH_AI_DeviceInfoSetEnableFP16(cpu_device_info, true);
    OH_AI_ContextAddDeviceInfo(context_, cpu_device_info);

    // 3. 构建模型对象,然后释放临时的文件 Buffer
    model_ = OH_AI_ModelCreate();
    auto ret = OH_AI_ModelBuild(model_, modelBuffer, modelSize, OH_AI_MODELTYPE_MINDIR, context_);
    free(modelBuffer);

    // 4. 重头戏:探查模型需要的 Input 长宽
    auto inputs = OH_AI_ModelGetInputs(model_);
    auto tensor = inputs.handle_list[0];
    size_t shape_num = 0;
    auto shape = OH_AI_TensorGetShape(tensor, &shape_num);
    // 拿到模型的输入宽高(例如 128x128)
    inputHeight_ = (int)shape[1];
    inputWidth_ = (int)shape[2];
    LOGI("Model initialized. Input size: %dx%d", inputWidth_, inputHeight_);

    return 0;
}
深入理解输入(Input)与输出(Output)参数

上面代码在打印 Input size。以我们移植的 BlazeFace 人脸模型为例:

  • 关于 Input 参数
    • 尺寸与通道 :要求输入的分辨率非常袖珍,通常是 128 x 128,通道是 RGB。也就是说不管原生相机给了你多大多高清的 4K 画面,我们统统要缩小。
    • 值范围 (归一化) :模型是不认识 0~255 的颜色值的。它需要被"压扁"到 [-1.0, 1.0] 的区间。具体的数学公式就是 (像素值 / 127.5) - 1.0
  • 关于 Output 参数
    • 模型计算完,并不会直接给你 (x, y, w, h) 的坐标!它吐出的是两个张量 (Tensor) 矩阵数组:
    • Scores (得分) :对应 896 个预设锚点框(Anchor)有人脸的概率。
    • Regressors (回归框偏移量) :也是长度极长的一串数据(896 * 16 = 14336)。它告诉你如果这地方有脸,脸的大小和人脸关键点的微调量是多少。
4. Native 核心二:推理与 NAPI 跨线程异步回调

相机拿到画面后,立刻进行推断,并通过一个神奇的桥梁把结果发给 ArkTS。

推理与图像预处理逻辑

来看看底层 C++ 捕获到一帧图片 imageData 后是怎么操作的:

cpp 复制代码
std::vector<std::vector<float>> FaceDetector::Detect(const unsigned char* imageData, int width, int height) {
    auto inputs = OH_AI_ModelGetInputs(model_);
    float* data = static_cast<float*>(OH_AI_TensorGetMutableData(inputs.handle_list[0]));

    // 核心大缩水阶段:把源相机的高清水灵大图,缩并抽出变成 128x128 的小块
    for (int y = 0; y < inputHeight_; ++y) {
        for (int x = 0; x < inputWidth_; ++x) {
            // 粗暴简单的最邻近插值法算原图坐标
            int src_x = x * width / inputWidth_;
            int src_y = y * height / inputHeight_;
            int idx = (src_y * width + src_x) * 4; // 假设原图是 RGBA 的 4 通道
            
            // 下面就是标准模型预处理:RGBA 抠出 RGB,同时从 0~255 变成 -1~1 的浮点数
            int offset = (y * inputWidth_ + x) * 3;
            if (data) {
                // (数值 / 127.5) - 1.0 完美归一化
                data[offset] = (static_cast<float>(imageData[idx]) / 127.5f) - 1.0f;
                data[offset + 1] = (static_cast<float>(imageData[idx + 1]) / 127.5f) - 1.0f;
                data[offset + 2] = (static_cast<float>(imageData[idx + 2]) / 127.5f) - 1.0f;
            }
        }
    }

    // "脑力劳动"开始:真正调用底层引擎执行推理
    auto outputs = OH_AI_ModelGetOutputs(model_);
    OH_AI_ModelPredict(model_, inputs, &outputs, nullptr, nullptr);

    // 把算出来的结果 (Scores 和 Regressors) 捞出来装进 std::vector 里返回
    std::vector<std::vector<float>> results;
    for (size_t i = 0; i < outputs.handle_num; i++) {
        float* outData = static_cast<float*>(OH_AI_TensorGetMutableData(outputs.handle_list[i]));
        size_t num = OH_AI_TensorGetElementNum(outputs.handle_list[i]);
        results.push_back(std::vector<float>(outData, outData + num));
    }
    return results;
}
跨线程数据"偷渡"者:ThreadSafeFunction

大坑预警 :C++ 的那一套相机帧回调是在独立的子线程发生的。如果你在这个线程直接去调 JS 的函数,应用 100% 会 Crash!

正确的破局之道是利用鸿蒙提供的 NAPI 特性:napi_create_threadsafe_function(线程安全函数)。它就像一个异步快递员。

cpp 复制代码
// 位于 napi_init.cpp
void HandleFacesDetected(std::vector<std::vector<float>> faces, void* context) {
    if (!g_tsfn) return; // 检查快递员是否注册
    // 把得到的人脸数据 new 一份在堆里扔包裹
    auto data = new std::vector<std::vector<float>>(faces);
    // 把数据扔给快递通道,底层的 JS 线程排队之后会安全收到!
    napi_call_threadsafe_function(g_tsfn, data, napi_tsfn_blocking);
}
5. ArkTS 核心三:XComponent 与 Canvas 画框逻辑

经过 C++ 的一番折腾,我们的 JS 层终于收到了回传的置信度得分和矩阵。

这里的难点有两处:

  1. 怎么画:底下一层铺满相机预览,上面盖一层透明玻璃(Canvas)用来拿画笔画画。
  2. 画在哪里:前置摄像头的逻辑都是镜像的,坐标一定要反过来算!

Index.ets 的精华结构:

typescript 复制代码
@Entry
@Component
struct Index {
  @State faces: Face[] = [];
  // ... 初始化定义
  
  // 画框的核心函数
  drawFaces() {
    this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // 清空上一帧
    this.canvasContext.strokeStyle = '#FF0000'; // 设定画笔是骚气的红色
    this.canvasContext.lineWidth = 3;
    
    // 细节填坑: 我们的模型只吃了一个正方形(1:1),屏幕是细长的,所以要算居中的 Offset 偏移量
    const isPortrait = this.canvasHeight > this.canvasWidth;
    const size = isPortrait ? this.canvasWidth : this.canvasHeight;
    const offsetX = isPortrait ? 0 : (this.canvasWidth - size) / 2;
    const offsetY = isPortrait ? (this.canvasHeight - size) / 2 : 0;

    this.faces.forEach((face) => {
      // 史诗级填坑:如果是前置摄像头抓脸,屏幕看到的是镜像,X 轴必须反转!
      const mirroredX = 1 - face.x2; 
      
      // 把 AI 给的 [0~1] 的比例系数值,用屏幕 size 映射回来
      const x = mirroredX * size + offsetX;
      const y = face.y1 * size + offsetY;
      const w = (face.x2 - face.x1) * size;
      const h = (face.y2 - face.y1) * size;
      
      this.canvasContext.strokeRect(x, y, w, h); // 最终画出红色矩形
    });
  }

  build() {
    Stack() {
      // 1. 底层:Camera Preview 相机预览
      XComponent({
        id: 'cameraPreview',
        type: 'surface',
        controller: this.xComponentController
      }).onLoad(() => {
        // 初始化时把这个画布的 ID 传给 C++ 绑卡相机去
        entry.initCamera(this.xComponentController.getXComponentSurfaceId());
      })
      .width('100%')
      .height('100%')

      // 2. 顶层:Drawing Overlay 在玻璃上画红框
      Canvas(this.canvasContext)
        .width('100%')
        .height('100%')
        .onReady(() => { this.drawFaces(); })
    }
  }
}

!NOTE

收到 C++ 回传那大量的 regressorsscores 时,我们在 ArkTS 层还需要执行 decodeBlazeFace(按 Anchor 解码出绝对坐标)和 nonMaxSuppression (非极大值抑制 NMS,把脸上重叠十几个框剃成唯一的一个最佳框),这里因为偏向纯前端算法处理,暂不在此篇堆砌太多 JS 代码。

6. 运行效果展示

打通所有的任督二脉之后,把应用安装到真机上跑起来。

首先系统会弹框让你授予相机权限。点同意以后,瞬间唤起 XComponent 呈现顺滑的镜头预览流。

与此同时,C++ 开始在黑暗的高速通道狂奔,你只要一露脸就可以看到下图的惊艳效果:一层又厚又红的边框立刻将你的整个人脸锁定,不管你是摇头还是移动,这个红色追分框仿佛长了眼睛跟得死死的!

下面是我自己用摄像头进行的效果,人脸遮挡一下:

下面是实时预览过程中的日志:

总结

从模型格式转换到今天的高压实战。在这场硬仗中,我们通过 C++ 高效截取底层视频流并完成像素映射转换,驱动 MindSpore Lite 获取极致的推理性能;再从多线程通信中化险为夷,最后加上灵活的 ArkTS UI 和逆映射策略。

这就打通了鸿蒙边缘侧 AI 推理的全链路闭环 。有了这套骨架,你完全可以把 face_detection_front.ms 剥掉,换成自己训练的 宠物识别红绿灯判定,直接开启你在鸿蒙生态的"超级视觉大门"吧!也可以基于检测模型得到的输出结果上传云端对人脸校验,实现真正的人脸识别功能。

相关推荐
提子拌饭1334 小时前
生命组学架构下的细胞分化与基因突变生存模拟器:基于鸿蒙Flutter的情景树渲染与状态溢出防御
flutter·华为·架构·开源·harmonyos
HarmonyOS_SDK5 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 —Media Library Kit
harmonyos
SoraLuna7 小时前
「鸿蒙智能体实战记录 14」项目复盘:岁时春信智能体完整实现与能力体系总结
华为·harmonyos
HwJack207 小时前
HarmonyOS开发中 `onKeyEvent` 事件总线:从“瞎按”到“指哪打哪”的终极掌控
华为·harmonyos
想你依然心痛8 小时前
HarmonyOS 5.0智慧交通开发实战:构建分布式车载智能座舱与手机无缝互联系统
分布式·智能手机·harmonyos·智慧交通·智能座舱
麒麟ZHAO9 小时前
鸿蒙flutter第三方库适配 - 动态表单
flutter·华为·harmonyos
见山是山-见水是水9 小时前
鸿蒙flutter第三方库适配 - 页面转场应用
flutter·华为·harmonyos
key_3_feng10 小时前
鸿蒙6.0开发深度排障实战:从崩溃到稳定的全链路解析
华为·harmonyos
见山是山-见水是水10 小时前
鸿蒙flutter第三方库适配 - 主题切换应用
flutter·华为·harmonyos