← 返回课程

第三方算法

Camera全栈开发(Qcom Camx) 第 29 / 30 节

第 27 章:第三方算法集成 — Custom ChiNode


本章导读

你有一个图像处理算法(美颜、滤镜、HDR 等),想把它集成到高通骁龙平台的 Camera 拍照/预览流程中。这一章告诉你具体怎么做。

先想一个问题:算法处理完一帧图像后,怎么把结果送回 Camera 流程里?

答案是:把你的算法封装成一个 Custom ChiNode(自定义处理节点),串到高通 ISP 的 Pipeline 里。ISP 每处理完一帧,数据就会经过你的节点,你处理完再传给下一级。

这一章用高通官方的 Memcpy Node 作为模板(它只拷贝数据,不做任何处理,是最简单的参考实现),带你从零走完整个流程。

你将要做的:
  ① 写一个 .cpp 文件 —— 实现 ChiNode 标准接口
  ② 写一个 .h 文件 —— 声明你的 Node 类
  ③ 写一个 Android.mk —— 编译成 .so
  ④ 改一个 XML 文件 —— 把 Node 串进 Pipeline
  ⑤ adb push —— 部署到手机
  ⑥ 重启验证

27.1 先搞懂几个概念

27.1.1 什么是 Pipeline?

想象一条流水线,传感器(Sensor)拍到的原始数据依次经过多个处理站:

[Sensor] → [IFE] → [IPE] → [CVP] → [Display/JPEG]
  传感器    前端ISP   图像引擎   DSP加速   输出显示/编码

每个 [xxx] 就是一个 Node(处理节点)。Pipeline 就是这些 Node 按顺序串起来的一条线。

27.1.2 ChiNode 是什么?

ChiNode 是 CHI 框架定义的标准插件接口。你写的 Node 可以插到 Pipeline 的任意位置:

修改前:         [Sensor] → [IFE] → [IPE] → [CVP] → [Display]
                                            ↑
修改后:         [Sensor] → [IFE] → [IPE] → [你写的Node] → [CVP] → [Display]
                                            ↑
                                    这里插入你的算法

你的 Node 收到 IPE 输出的 YUV 图像,处理完传给 CVP。CVP 不知道数据被处理过——它只是从上一个 Node 拿数据。

27.1.3 ChiNode 和普通函数的区别

你的算法本来可能是一个函数:

void my_beauty(uint8_t* input, uint8_t* output, int w, int h);

ChiNode 要做的事就是把这个函数包装一下,让 Camera 框架知道:

27.1.4 一个 Node 的生命周期

从手机开机到你拍一张照片,Node 经历的过程:

① 手机开机 → Chi driver 启动
                 ↓
② 扫描 /vendor/lib64/camera/components/ 目录
   找到所有 .so 文件
                 ↓
③ 对每个 .so 调用 ChiNodeEntry()
   Node 在这里注册自己的回调函数
                 ↓
④ App 打开相机 → 创建 Pipeline
   在 XML 配置中找到了你的 Node
                 ↓
⑤ 调用你的 pCreate()
   创建 Node 实例,分配资源
                 ↓
⑥ 调用 pQueryBufferInfo()
   问你的 Node:输入输出要什么格式?
                 ↓
⑦ 每来一帧 → 调用 pProcessRequest()
   你的算法在这里处理图像
                 ↓
⑧ App 关闭相机 → 调用 pDestroy()
   释放资源

27.2 从哪里开始?——先找到参考源码

高通提供了多个参考 ChiNode,最推荐作为模板的是 Memcpy Node(只拷贝数据,没有任何算法逻辑)。

源码位置

chi-cdk/oem/qcom/node/memcpy/
├── camxchinodememcpy.cpp     ← 实现文件(~500行)
├── camxchinodememcpy.h       ← 头文件(~70行)
└── common/build/android/
    └── Android.mk             ← 编译脚本

其他参考例程

节点 路径 特点
Memcpy(推荐作为模板) chi-cdk/oem/qcom/node/memcpy/ 最简单,只拷贝数据,适合入门
GPU chi-cdk/oem/qcom/node/gpu/ 演示 GPU 加速
GridBeauty chi-cdk/oem/qcom/node/grid/gridbeauty/ 实际美颜算法(OEM 实现)
HVX AddConstant chi-cdk/oem/qcom/node/hvx/addconstant/ Hexagon DSP 加速

27.3 开始写代码——完整实现

27.3.1 创建文件目录

首先创建你的 Node 目录,和参考 Memcpy 保持同一级:

cd chi-cdk/oem/qcom/node/
mkdir -p beauty/ && cd beauty/

目录结构:

chi-cdk/oem/qcom/node/beauty/
├── chinodebeauty.h           ← 头文件
├── chinodebeauty.cpp         ← 实现文件
└── Android.mk                ← 编译脚本

27.3.2 头文件——定义 Node 类

// ─── chinodebeauty.h ─────────────────────────────────────────────────
#ifndef CHINODEBEAUTY_H
#define CHINODEBEAUTY_H

#include "chinode.h"            // ChiNode 标准接口定义
#include "camxchinodeutil.h"    // 工具函数(SetNodeInterface 等)

// 你的 Node 类
// 作用:保存 Node 的状态信息(分辨率、算法参数等)
// 每创建一个 Node 实例,就有一个这个类的对象
class ChiBeautyNode {
public:
    // 初始化——在 pCreate 中被调用
    // 参数:pCreateInfo 中包含了 Node ID、camera ID 等信息
    CDKResult Initialize(CHINODECREATEINFO* pCreateInfo);

    // 处理请求——每帧数据都会调用这里
    // 参数:pProcessRequestInfo 中包含了输入/输出 buffer
    CDKResult ProcessRequest(CHINODEPROCESSREQUESTINFO* pProcessRequestInfo);

    // 查询 buffer 需求——告诉框架你需要什么格式的图像
    CDKResult QueryBufferInfo(CHINODEQUERYBUFFERINFO* pQueryBufferInfo);

    // 设置 buffer——框架告诉你最终分配的 buffer 参数
    CDKResult SetBufferInfo(CHINODESETBUFFERPROPERTIESINFO* pSetBufferInfo);

    // 销毁——释放资源
    VOID Destroy();

private:
    // Node 内部保存的信息
    UINT32 m_nodeId;          // Node ID(例如 65537)
    UINT32 m_nodeInstanceId;  // 实例 ID
    UINT32 m_logicalCameraId; // 逻辑 camera ID
    INT32  m_width;           // 图像宽度(由框架告诉 Node)
    INT32  m_height;          // 图像高度
    INT32  m_beautyLevel;     // 你的算法参数(美颜等级 0~100)
};

#endif

27.3.3 实现文件——各部分详解

// ─── chinodebeauty.cpp ─────────────────────────────────────────────────
#include "chinodebeauty.h"

// log 标签,方便在 logcat 中过滤你的 Node 日志
#undef LOG_TAG
#define LOG_TAG "CHIBEAUTY"

// ─── 全局变量 ──────────────────────────────────────────────────────────
// g_ChiNodeInterface:Chi driver 提供给 Node 的接口
// Node 通过这个接口回调 Chi driver(如:报告处理完成、做 cache 操作)
ChiNodeInterface g_ChiNodeInterface;

// g_vendorTagBase:vendor tag 的基地址
// 如果你的 Node 需要发布自定义 metadata,需要用到这个
UINT32 g_vendorTagBase = 0;

// CHI 接口的版本号,和 ChiNodeEntry 中的版本检查对应
static const UINT32 ChiNodeMajorVersion = 0;
static const UINT32 ChiNodeMinorVersion = 0;

27.3.4 回调函数 ①:pGetCapabilities

// ─── pGetCapabilities ──────────────────────────────────────────────────
// 作用:告诉框架你的 Node 有什么能力
// 参数:pCapsInfo->nodeCapsMask 用位掩码表示能力
//
// 对于简单的算法(美颜、滤镜等),不需要特殊能力,设为 0 即可
//
// 可能的能力值(定义在 chinode.h 中):
//   ChiNodeCapsScale         = 1      支持缩放
//   ChiNodeCapsGPUGrayscale  = 1 << 2 支持灰度转换
//   ChiNodeCapsGPURotate     = 1 << 3 支持旋转

static CDKResult BeautyNodeGetCaps(CHINODECAPSINFO* pCapsInfo) {
    CDKResult result = CDKResultSuccess;

    // 安全检查:参数为 NULL 直接返回失败
    if (pCapsInfo == NULL) {
        return CDKResultEInvalidPointer;
    }

    // 检查结构体大小是否符合预期
    // 所有 ChiNode 回调都有这个 size 检查模式,用来做版本兼容
    if (pCapsInfo->size >= sizeof(CHINODECAPSINFO)) {
        pCapsInfo->nodeCapsMask = 0;  // 我不需要特殊能力
    } else {
        result = CDKResultEFailed;
    }

    return result;
}

27.3.5 回调函数 ②:pCreate

// ─── pCreate ───────────────────────────────────────────────────────────
// 作用:创建 Node 实例
// 调用时机:Pipeline 创建时,框架为每个 Node 调用一次 pCreate
//
// 你要做的事:
//   1. new 一个 ChiBeautyNode 对象(保存状态)
//   2. 调用 Initialize()
//   3. 把对象指针通过 phNodeSession 传回框架
//
// 为什么不能直接用全局变量保存状态?
//   因为同一台手机上可能有多个 camera 同时打开(前摄+后摄),
//   每个 camera 都有自己的 Pipeline,每个 Pipeline 里的 Node 是独立的实例。
//   所以必须通过 phNodeSession 把实例句柄传回框架,后续回调再取出来。

static CDKResult BeautyNodeCreate(CHINODECREATEINFO* pCreateInfo) {
    CDKResult result = CDKResultSuccess;

    if (NULL == pCreateInfo) {
        return CDKResultEInvalidPointer;
    }

    // size 检查:确保结构体版本匹配
    if (pCreateInfo->size < sizeof(CHINODECREATEINFO)) {
        return CDKResultEFailed;
    }

    // 创建 Node 实例
    ChiBeautyNode* pNode = new ChiBeautyNode;
    if (NULL == pNode) {
        return CDKResultENoMemory;
    }

    // 初始化
    result = pNode->Initialize(pCreateInfo);

    if (CDKResultSuccess == result) {
        // ★★★ 关键步骤 ★★★
        // 把 Node 实例的指针传给框架
        // 后续的回调(pProcessRequest、pDestroy 等)通过
        // pProcessRequestInfo->hNodeSession 拿到这个指针
        pCreateInfo->phNodeSession = reinterpret_cast<CHIHANDLE*>(pNode);

        ALOGI("%s: BeautyNode created, nodeId=%u",
              LOG_TAG, pCreateInfo->nodeId);
    } else {
        delete pNode;
    }

    return result;
}

27.3.6 回调函数 ③:pDestroy

// ─── pDestroy ──────────────────────────────────────────────────────────
// 作用:销毁 Node 实例
// 调用时机:Pipeline 销毁时
//
// 注意:参数类型是 CHINODEDESTROYINFO,不是 CHINODECREATEINFO
// 通过 hNodeSession 拿到之前 Create 时保存的实例指针

static CDKResult BeautyNodeDestroy(CHINODEDESTROYINFO* pDestroyInfo) {
    CDKResult result = CDKResultSuccess;

    // 安全检查
    if ((NULL == pDestroyInfo) || (NULL == pDestroyInfo->hNodeSession)) {
        return CDKResultEInvalidPointer;
    }

    if (pDestroyInfo->size >= sizeof(CHINODEDESTROYINFO)) {
        // 从 hNodeSession 取出 Node 实例
        ChiBeautyNode* pNode = static_cast<ChiBeautyNode*>(pDestroyInfo->hNodeSession);

        // 先调用 Destroy 释放内部资源
        pNode->Destroy();
        // 再删除对象
        delete pNode;
        // 置空句柄
        pDestroyInfo->hNodeSession = NULL;

        ALOGI("%s: BeautyNode destroyed", LOG_TAG);
    }

    return result;
}

27.3.7 回调函数 ④:pQueryBufferInfo

// ─── pQueryBufferInfo ───────────────────────────────────────────────────
// 作用:告诉框架你的 Node 需要什么样的输入输出 buffer
// 调用时机:Pipeline 创建时,在 buffer 分配之前
//
// 你在这里声明:
//   输入:什么格式?需要什么内存属性?(CPU 能读写吗?HW 能访问吗?)
//   输出:什么格式?需要什么内存属性?
//
// 然后框架根据你的要求分配 buffer

static CDKResult BeautyNodeQueryBufferInfo(
    CHINODEQUERYBUFFERINFO* pQueryBufferInfo) {

    CDKResult result = CDKResultSuccess;

    if ((NULL == pQueryBufferInfo) || (NULL == pQueryBufferInfo->hNodeSession)) {
        return CDKResultEInvalidPointer;
    }

    ChiBeautyNode* pNode = static_cast<ChiBeautyNode*>(pQueryBufferInfo->hNodeSession);
    result = pNode->QueryBufferInfo(pQueryBufferInfo);

    return result;
}

类内部的实现:

// ChiBeautyNode::QueryBufferInfo
CDKResult ChiBeautyNode::QueryBufferInfo(
    CHINODEQUERYBUFFERINFO* pQueryBufferInfo) {

    // ── 输入 buffer 声明 ──
    pQueryBufferInfo->numInputProperties = 1;  // 1 个输入端口

    // buffer 属性:三个标志位的含义
    //   BufferMemFlagHw       = ISP 硬件可以访问这个 buffer
    //   BufferMemFlagLockable = CPU 可以读写这个 buffer
    //   BufferMemFlagCache    = 可以做 cache 操作(CPU 写完后需要 flush)
    //
    // 如果你的算法在 CPU 上运行:需要 Hw | Lockable | Cache 三个都要
    // 如果你的算法在 DSP 上运行:只需要 Hw(DSP 不经过 CPU cache)
    pQueryBufferInfo->inputOptions[0].bufferProperties =
        BufferMemFlagHw | BufferMemFlagLockable | BufferMemFlagCache;

    // 输入格式:YUV420 NV12(最常用的格式)
    pQueryBufferInfo->inputOptions[0].bufferFormat.format =
        ChiFormatYUV420NV12;

    // ── 输出 buffer 声明 ──
    pQueryBufferInfo->numOutputProperties = 1;

    pQueryBufferInfo->outputOptions[0].bufferProperties =
        BufferMemFlagHw | BufferMemFlagLockable | BufferMemFlagCache;

    pQueryBufferInfo->outputOptions[0].bufferFormat.format =
        ChiFormatYUV420NV12;

    return CDKResultSuccess;
}

27.3.8 回调函数 ⑤:pSetBufferInfo

// ─── pSetBufferInfo ────────────────────────────────────────────────────
// 作用:框架把最终分配的 buffer 参数告诉 Node
// 调用时机:buffer 分配完成后
//
// 你可以在这里获取图像的分辨率信息

static CDKResult BeautyNodeSetBufferInfo(
    CHINODESETBUFFERPROPERTIESINFO* pSetBufferInfo) {

    CDKResult result = CDKResultSuccess;

    if ((NULL == pSetBufferInfo) || (NULL == pSetBufferInfo->hNodeSession)) {
        return CDKResultEInvalidPointer;
    }

    ChiBeautyNode* pNode = static_cast<ChiBeautyNode*>(pSetBufferInfo->hNodeSession);
    result = pNode->SetBufferInfo(pSetBufferInfo);

    return result;
}

// ChiBeautyNode::SetBufferInfo
CDKResult ChiBeautyNode::SetBufferInfo(
    CHINODESETBUFFERPROPERTIESINFO* pSetBufferInfo) {

    // 保存图像分辨率,后续 ProcessRequest 中会用到
    if (pSetBufferInfo->numInputProperties > 0) {
        m_width  = pSetBufferInfo->inputOptions[0].bufferFormat.width;
        m_height = pSetBufferInfo->inputOptions[0].bufferFormat.height;

        ALOGI("%s: Buffer info set: %dx%d", LOG_TAG, m_width, m_height);
    }

    return CDKResultSuccess;
}

27.3.9 回调函数 ⑥:pProcessRequest——核心中的核心

// ─── pProcessRequest ───────────────────────────────────────────────────
// 作用:每帧图像处理的入口!
// 调用时机:每来一帧数据,框架就调用一次
//
// 这是最重要的回调——你的算法就放在这里
//
// 参数 pProcessRequestInfo 中包含:
//   hNodeSession  — 你的 Node 实例句柄(从 Create 传回来的)
//   frameNum      — 当前帧号(可以用来做帧率统计)
//   inputNum      — 输入 buffer 数量
//   phInputBuffer — 输入 buffer 数组(数组元素是 CHINODEBUFFERHANDLE)
//   outputNum     — 输出 buffer 数量
//   phOutputBuffer— 输出 buffer 数组
//
// CHINODEBUFFERHANDLE 其实是 CHIIMAGELIST*
// CHIIMAGELIST 中包含:
//   pImageList[0]          — 第一个图像
//     .pAddr[0]            — Y 平面地址(NV12 格式)
//     .pAddr[1]            — UV 平面地址
//     .fd[0]               — 文件描述符
//     .format.width        — 宽度
//     .format.height       — 高度

static CDKResult BeautyNodeProcessRequest(
    CHINODEPROCESSREQUESTINFO* pProcessRequestInfo) {

    CDKResult result = CDKResultSuccess;

    // 安全检查
    if ((NULL == pProcessRequestInfo) ||
        (NULL == pProcessRequestInfo->hNodeSession)) {
        return CDKResultEInvalidPointer;
    }

    if (pProcessRequestInfo->size < sizeof(CHINODEPROCESSREQUESTINFO)) {
        return CDKResultEFailed;
    }

    // 通过 hNodeSession 取回 Node 实例
    ChiBeautyNode* pNode =
        static_cast<ChiBeautyNode*>(pProcessRequestInfo->hNodeSession);

    // 调用实际的算法处理
    result = pNode->ProcessRequest(pProcessRequestInfo);

    return result;
}

27.3.10 ProcessRequest 内部——拿到 buffer 执行算法

// ChiBeautyNode::ProcessRequest
// 这里的 pProcessRequestInfo 就是上面传下来的
CDKResult ChiBeautyNode::ProcessRequest(
    CHINODEPROCESSREQUESTINFO* pProcessRequestInfo) {

    // ── Step 1:拿到输入输出 buffer ──
    //
    // phInputBuffer[0] 是第 1 个输入端口的 buffer 列表
    //   -> pImageList[0] 是第 1 帧图像
    //     -> pAddr[0] 是 Y 平面的内存地址(对于 NV12 格式)
    //     -> pAddr[1] 是 UV 平面的内存地址
    //
    // phOutputBuffer[0] 同理

    CHIIMAGE* pInputImage  =
        &pProcessRequestInfo->phInputBuffer[0]->pImageList[0];
    CHIIMAGE* pOutputImage =
        &pProcessRequestInfo->phOutputBuffer[0]->pImageList[0];

    // Y 平面地址(亮度数据)
    UINT8* pSrcY  = pInputImage->pAddr[0];
    UINT8* pDstY  = pOutputImage->pAddr[0];

    // UV 平面地址(色度数据)
    UINT8* pSrcUV = pInputImage->pAddr[1];
    UINT8* pDstUV = pOutputImage->pAddr[1];

    int width  = m_width;
    int height = m_height;

    // ── Step 2:执行算法 ──
    //
    // 这里用最简单的模糊作为示例
    // 替换成你的实际算法即可
    //
    // 计算量参考:
    //   1920x1080 的图像,Y 平面有 1920*1080 = 2,073,600 个像素
    //   每个像素做 9 次加法 + 1 次除法 → 约 2000 万次运算
    //   在 CPU 上大约需要 5~15ms

    ALOGI("%s: Processing frame %llu, %dx%d",
          LOG_TAG, pProcessRequestInfo->frameNum, width, height);

    // 对 Y 平面做 3x3 均值模糊(最简单的磨皮效果)
    for (int y = 1; y < height - 1; y++) {
        for (int x = 1; x < width - 1; x++) {
            int idx = y * width + x;
            pDstY[idx] = (pSrcY[idx - width - 1] + pSrcY[idx - width] +
                          pSrcY[idx - width + 1] + pSrcY[idx - 1] +
                          pSrcY[idx]             + pSrcY[idx + 1] +
                          pSrcY[idx + width - 1] + pSrcY[idx + width] +
                          pSrcY[idx + width + 1]) / 9;
        }
    }

    // UV 平面不做处理,直接拷贝
    // 注意:UV 平面的总大小是 width * height / 2(NV12 格式)
    int uvSize = width * height / 2;
    memcpy(pDstUV, pSrcUV, uvSize);

    // ── Step 3:Cache 同步 ──
    //
    // ★★★ 非常重要 ★★★
    //
    // 为什么需要这步?
    //   CPU 和 ISP 硬件访问内存的方式不同:
    //   - CPU 有 cache,你写 pDstY[idx] = ... 时,数据可能还在 CPU cache 里
    //   - ISP 硬件(IFE/IPE/JPEG)没有 cache,直接从内存读数据
    //   - 所以 CPU 写完必须主动把 cache 中的数据"刷"到内存中
    //
    // 如果不做这步,下游 Node(如 CVP/JPEG)拿到的可能是全 0 或旧数据
    //
    // pCacheOps 的参数:
    //   ChiNodeCacheOpsCleanInvalidate = 把 cache 写回内存并失效

    g_ChiNodeInterface.pCacheOps(pOutputImage,
                                 ChiNodeCacheOpsCleanInvalidate);

    return CDKResultSuccess;
}

27.3.11 回调函数 ⑦:pChiNodeSetNodeInterface

// ─── pChiNodeSetNodeInterface ─────────────────────────────────────────
// 作用:Chi driver 把它的接口传给 Node
// 调用时机:Node 创建后立即调用
//
// 这个回调让 Node 拿到全局的 g_ChiNodeInterface
// Node 通过它来调用 Chi driver 的功能:
//   pCacheOps()         — cache 同步(必须!)
//   pGetMetadata()      — 读取 metadata
//   pSetMetadata()      — 设置 metadata
//   pProcessRequestDone() — 报告处理完成
//   pCreateFence()      — 创建 fence
//   ...

static VOID BeautyNodeSetNodeInterface(
    ChiNodeInterface* pNodeInterface) {

    if (NULL != pNodeInterface) {
        // 保存接口
        g_ChiNodeInterface = *pNodeInterface;

        // 通过工具函数获取 vendor tag base
        // 如果你的 Node 需要发布自定义 metadata,后续会用到这个 base
        ChiNodeUtils::SetNodeInterface(pNodeInterface,
            "com.vendor.node.beauty",
            &g_ChiNodeInterface,
            &g_vendorTagBase);
    }
}

27.3.12 回调函数 ⑧:pQueryMetadataPublishList

// ─── pQueryMetadataPublishList ─────────────────────────────────────────
// 作用:告诉框架你的 Node 会发布哪些 metadata
// 调用时机:Pipeline 创建时
//
// 如果你的 Node 不发布 metadata(大多数算法 Node 都不需要),
// 设置 tagCount = 0 即可

static CDKResult BeautyNodeQueryMetadataPublishList(
    CHINODEMETADATALIST* pMetadataList) {

    if ((NULL == pMetadataList) ||
        (NULL == pMetadataList->hNodeSession)) {
        return CDKResultEInvalidPointer;
    }

    // 我们不发布 metadata
    pMetadataList->tagCount = 0;
    return CDKResultSuccess;
}

27.3.13 入口函数:ChiNodeEntry

// ─── ChiNodeEntry ──────────────────────────────────────────────────────
// 作用:.so 的入口,Chi driver 加载 .so 后调用的第一个函数
// 调用时机:Chi driver 启动时扫描 components/ 目录
//
// 你的 .so 必须导出这个函数!否则 Chi driver 不会加载你的 Node
//
// 你在函数里做的事情:
//   1. 检查版本号是否匹配
//   2. 检查结构体大小是否足够
//   3. 把各个回调函数指针填进去

#ifdef __cplusplus
extern "C" {   // 因为 Chi driver 是用 C 方式调用的,所以用 extern "C"
#endif

CDK_VISIBILITY_PUBLIC VOID ChiNodeEntry(
    CHINODECALLBACKS* pNodeCallbacks) {

    if (NULL != pNodeCallbacks) {
        // 版本检查:major version 必须匹配
        if (pNodeCallbacks->majorVersion == ChiNodeMajorVersion &&
            pNodeCallbacks->size >= sizeof(CHINODECALLBACKS)) {

            // 填入版本号和所有回调函数指针
            pNodeCallbacks->majorVersion              = ChiNodeMajorVersion;
            pNodeCallbacks->minorVersion              = ChiNodeMinorVersion;
            pNodeCallbacks->pGetCapabilities          = BeautyNodeGetCaps;
            pNodeCallbacks->pQueryVendorTag           = NULL;  // 可选
            pNodeCallbacks->pCreate                   = BeautyNodeCreate;
            pNodeCallbacks->pDestroy                  = BeautyNodeDestroy;
            pNodeCallbacks->pQueryBufferInfo          = BeautyNodeQueryBufferInfo;
            pNodeCallbacks->pSetBufferInfo            = BeautyNodeSetBufferInfo;
            pNodeCallbacks->pProcessRequest           = BeautyNodeProcessRequest;
            pNodeCallbacks->pChiNodeSetNodeInterface  = BeautyNodeSetNodeInterface;
            pNodeCallbacks->pQueryMetadataPublishList = BeautyNodeQueryMetadataPublishList;
            pNodeCallbacks->pFlushRequest             = NULL;  // 暂不实现
            pNodeCallbacks->pGetFlushResponse         = NULL;  // 暂不实现
            pNodeCallbacks->pPrepareStreamOn          = NULL;  // 暂不实现
            pNodeCallbacks->pOnStreamOn               = NULL;  // 暂不实现
            pNodeCallbacks->pOnStreamOff              = NULL;  // 暂不实现
        }
    }
}

#ifdef __cplusplus
}
#endif

27.3.14 Initialize 和 Destroy

// ChiBeautyNode::Initialize
CDKResult ChiBeautyNode::Initialize(CHINODECREATEINFO* pCreateInfo) {
    // 保存 Node 的基本信息
    m_nodeId          = pCreateInfo->nodeId;
    m_nodeInstanceId  = pCreateInfo->nodeInstanceId;
    m_logicalCameraId = pCreateInfo->logicalCameraId;

    // 初始化成员变量
    m_width       = 0;
    m_height      = 0;
    m_beautyLevel = 50;  // 默认美颜等级

    ALOGI("%s: Initialized (nodeId=%u, camId=%u)",
          LOG_TAG, m_nodeId, m_logicalCameraId);

    return CDKResultSuccess;
}

// ChiBeautyNode::Destroy
VOID ChiBeautyNode::Destroy() {
    ALOGI("%s: Destroyed", LOG_TAG);
    // 如果有动态分配的资源,在这里释放
}

27.3.15 Android.mk 编译脚本

# ─── Android.mk ────────────────────────────────────────────────────────
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

# ★★★ 模块名 = 输出文件名 ★★★
# 高通建议的命名规则:com.<厂商>.<类型>.<算法>.so
#   厂商(vendor):你的公司名(如 qti、vendor 等)
#   类型(category):node(普通算法节点)
#   算法(algorithm):beauty(你的算法名)
#
# 示例:
#   com.qti.node.memcpy.so   —— 高通的 Memcpy 节点
#   com.vendor.node.beauty.so —— 你的美颜节点
LOCAL_MODULE := com.vendor.node.beauty

# 源文件
LOCAL_SRC_FILES := \
    chinodebeauty.cpp

# 依赖库
LOCAL_SHARED_LIBRARIES := \
    libchi-cdk           # CHI 核心库

# 放在 vendor 分区
LOCAL_VENDOR_MODULE := true

# ★★★ 输出路径 = camera/components/ ★★★
# Chi driver 启动时扫描这个目录,加载所有 .so
# 所以你的 Node 必须放在这里!
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/lib64/camera/components

include $(BUILD_SHARED_LIBRARY)

27.4 编译

# 进入你的 Node 目录
cd chi-cdk/oem/qcom/node/beauty/

# 编译(mmm 是编译指定目录下的模块)
mmm .

# 编译成功后在 out/target/product/<platform>/vendor/lib64/camera/components/
# 下生成 com.vendor.node.beauty.so

如果 mmm 命令不可用,也可以用 mm(编译当前目录)或在源码根目录用 make

# 或者编译整个模块
cd <源码根目录>
source build/envsetup.sh
lunch <你的平台>
make com.vendor.node.beauty -j8

27.5 串入 Pipeline——在 XML 中告诉系统你的 Node 放在哪

Node 编译好了,但系统还不知道你的 Node 要插在 Pipeline 的什么位置。这一步要修改 XML 配置文件。

27.5.1 找到正确的 XML 文件

Pipeline 拓扑定义在 XML 文件中,位置取决于平台:

# 新平台(xxx/xxx 等)—— usecase-components 方式
chi-cdk/oem/qcom/topology/usecase-components/usecases/
    └── UsecaseZSL/
        └── camxUsecaseZSL.xml    ← 预览/ZSL 的拓扑
    └── UsecaseSnapshot/
        └── camxUsecaseSnapshot.xml ← 拍照的拓扑

27.5.2 在 XML 中添加你的 Node

打开对应的 XML 文件(例如 camxUsecaseZSL.xml),找到 <NodeList> 段,添加你的 Node:

<NodeList>
    <!-- ════════════════════════════════════════ -->
    <!-- 高通默认的 Node(不要改)                 -->
    <!-- ════════════════════════════════════════ -->
    <Node>Sensor-0</Node>
    <Node>IFE-0</Node>
    <Node>IPE-0</Node>

    <!-- ════════════════════════════════════════ -->
    <!-- 你的自定义 Node(新增)                   -->
    <!-- ════════════════════════════════════════ -->
    <Node>
        <NodeId>65537</NodeId>          <!-- OEM 自定义 ID -->
        <NodeInstanceId>0</NodeInstanceId>
        <NodeName>Beauty</NodeName>
        <Property>
            <PropertyId>2</PropertyId>   <!-- NodePropertyProfileId -->
            <PropertyValue>1</PropertyValue>
        </Property>
    </Node>

    <!-- ════════════════════════════════════════ -->
    <!-- 高通默认的 Node(继续)                   -->
    <!-- ════════════════════════════════════════ -->
    <Node>CVP-0</Node>
</NodeList>

27.5.3 在 XML 中添加连接关系

找到 <PortLinkages> 段,添加你的 Node 的上下游连接:

<PortLinkages>
    <!-- 原有连接(保持不动) -->
    <Link>Sensor-0 → IFE-0</Link>
    <Link>IFE-0 → IPE-0</Link>

    <!-- ★ 新增:IPE 的输出接你的 Node ★ -->
    <Link>IPE-0 → Beauty-0</Link>

    <!-- ★ 新增:你的 Node 的输出接 CVP ★ -->
    <Link>Beauty-0 → CVP-0</Link>

    <!-- 原有连接(保持不动) -->
    <Link>CVP-0 → Display</Link>
</PortLinkages>

27.5.4 Node ID 注意事项

高通保留了一些 Node ID 用于内部硬件模块:

Node ID 硬件模块 说明
0 Sensor 图像传感器
65536 (0x10000) IFE 图像前端
65538 (0x10002) IPE 图像处理引擎
65540 (0x10004) FD 人脸检测
65543 (0x10007) CVP 计算机视觉处理器
65537 你的 Node OEM 自定义,避开以上范围即可

规则:OEM 自定义 Node 的 ID 使用 ≥ 65544< 65536 的范围,不要和高通保留的 ID 冲突。


27.6 部署到设备

# 1. 确认设备已连接
adb devices

# 2. root 并 remount(需要 root 权限的设备)
adb root && adb remount

# 3. 把 .so 推送到 components 目录
#    路径必须和 Android.mk 中 LOCAL_MODULE_PATH 一致
adb push $OUT/target/product/<platform>/vendor/lib64/camera/components/com.vendor.node.beauty.so \
       /vendor/lib64/camera/components/

# 4. 设置权限
adb shell chmod 644 /vendor/lib64/camera/components/com.vendor.node.beauty.so

# 5. 重启(Chi driver 只有在启动时扫描 components/ 目录)
adb reboot

27.7 验证——怎么确认 Node 在工作

第一步:确认 Node 被加载

重启后,查看 logcat 中是否出现你的 Node 的 log:

adb logcat | grep CHIBEAUTY

如果看到 BeautyNode created,说明 Node 被成功加载和创建。

第二步:确认 Node 被串入 Pipeline

搜索 Pipeline 创建时的日志:

adb logcat | grep -E "NodeId|Pipeline|Beauty"

应该能看到系统创建 Pipeline 时包含了你的 Node ID(65537)。

第三步:确认每帧都经过了你的算法

adb logcat | grep "Processing frame"

拍照或预览时,应该能看到每帧都输出 Processing frame N 的日志。

第四步:确认算法生效

拍一张照片,观察输出图像是否经过了你算法的处理。

检查帧率

如果算法处理太慢,帧率会下降:

# 粗略看帧率——搜 preview 相关的 FPS log
adb logcat | grep -E "FPS|fps|frame"
# 或者用 systrace 工具分析

27.8 调试指南——出问题了怎么办

问题 1:相机打不开或闪退

# 查 crash 栈
adb logcat | grep -E "crash|panic|SIGSEGV|FATAL"

# 查 Camera HAL 错误
adb logcat | grep -E "CamX|CHI|Camera"

# 查 .so 依赖是否完整
adb shell ldd /vendor/lib64/camera/components/com.vendor.node.beauty.so

可能的原因

问题 2:Node 未被加载

# 检查 components 目录
adb shell ls -la /vendor/lib64/camera/components/ | grep beauty

# 检查驱动是否加载了 .so
adb logcat | grep -E "ChiNode|components|dlopen"

可能的原因

问题 3:画面没变化(算法没生效)

可能的原因

问题 4:帧率很低

// 在 ProcessRequest 中加耗时统计
uint64_t start = /* 获取当前时间戳 */;

// ... 算法处理 ...

uint64_t end = /* 获取当前时间戳 */;
ALOGI("%s: Frame %llu processed in %lld ms",
      LOG_TAG, pProcessRequestInfo->frameNum, (end - start) / 1000000);

如果每帧处理时间 > 33ms(30fps),说明算法太慢。优化方向:


27.9 完整文件清单

你的最终目录应该是这样的:

chi-cdk/oem/qcom/node/beauty/
│
├── chinodebeauty.h              ← 头文件:Node 类声明
│
├── chinodebeauty.cpp            ← 实现文件:所有回调 + 算法逻辑
│    ├── 全局变量(g_ChiNodeInterface)
│    ├── BeautyNodeGetCaps()          ① 能力声明
│    ├── BeautyNodeCreate()           ② Node 创建
│    ├── BeautyNodeDestroy()          ③ Node 销毁
│    ├── BeautyNodeQueryBufferInfo()  ④ buffer 需求
│    ├── BeautyNodeSetBufferInfo()    ⑤ buffer 设置
│    ├── BeautyNodeProcessRequest()   ⑥ ★ 核心处理
│    ├── BeautyNodeSetNodeInterface() ⑦ Chi 接口保存
│    ├── BeautyNodeQueryMetadata...() ⑧ metadata 声明
│    └── ChiNodeEntry()               ⑨ ★ 入口函数
│
└── Android.mk                   ← 编译脚本
    └── LOCAL_MODULE := com.vendor.node.beauty
    └── LOCAL_MODULE_PATH := camera/components/

27.10 本章总结

第三方算法集成 = 写一个自定义 ChiNode → 编译 .so → 串进 Pipeline → 部署

你的算法代码放在:pProcessRequest 回调里
系统怎么找到你的 Node:ChiNodeEntry 入口函数
Node 的状态保存在哪:通过 phNodeSession 传递的实例句柄
Node 放在 Pipeline 的哪:XML 配置中的 <NodeList> + <PortLinkages>
.so 放在哪里:/vendor/lib64/camera/components/

最容易出错的三件事:
  1. 忘记把实例通过 phNodeSession 传回框架(Create 中)
  2. CPU 写完 buffer 后忘记做 cache flush(ProcessRequest 中)
  3. buffer 属性忘记设 BufferMemFlagLockable(QueryBufferInfo 中)

参考