你有一个图像处理算法(美颜、滤镜、HDR 等),想把它集成到高通骁龙平台的 Camera 拍照/预览流程中。这一章告诉你具体怎么做。
先想一个问题:算法处理完一帧图像后,怎么把结果送回 Camera 流程里?
答案是:把你的算法封装成一个 Custom ChiNode(自定义处理节点),串到高通 ISP 的 Pipeline 里。ISP 每处理完一帧,数据就会经过你的节点,你处理完再传给下一级。
这一章用高通官方的 Memcpy Node 作为模板(它只拷贝数据,不做任何处理,是最简单的参考实现),带你从零走完整个流程。
你将要做的:
① 写一个 .cpp 文件 —— 实现 ChiNode 标准接口
② 写一个 .h 文件 —— 声明你的 Node 类
③ 写一个 Android.mk —— 编译成 .so
④ 改一个 XML 文件 —— 把 Node 串进 Pipeline
⑤ adb push —— 部署到手机
⑥ 重启验证
想象一条流水线,传感器(Sensor)拍到的原始数据依次经过多个处理站:
[Sensor] → [IFE] → [IPE] → [CVP] → [Display/JPEG]
传感器 前端ISP 图像引擎 DSP加速 输出显示/编码
每个 [xxx] 就是一个 Node(处理节点)。Pipeline 就是这些 Node 按顺序串起来的一条线。
ChiNode 是 CHI 框架定义的标准插件接口。你写的 Node 可以插到 Pipeline 的任意位置:
修改前: [Sensor] → [IFE] → [IPE] → [CVP] → [Display]
↑
修改后: [Sensor] → [IFE] → [IPE] → [你写的Node] → [CVP] → [Display]
↑
这里插入你的算法
你的 Node 收到 IPE 输出的 YUV 图像,处理完传给 CVP。CVP 不知道数据被处理过——它只是从上一个 Node 拿数据。
你的算法本来可能是一个函数:
void my_beauty(uint8_t* input, uint8_t* output, int w, int h);
ChiNode 要做的事就是把这个函数包装一下,让 Camera 框架知道:
ChiNodeEntry 注册)pQueryBufferInfo 声明)pProcessRequest 触发)从手机开机到你拍一张照片,Node 经历的过程:
① 手机开机 → Chi driver 启动
↓
② 扫描 /vendor/lib64/camera/components/ 目录
找到所有 .so 文件
↓
③ 对每个 .so 调用 ChiNodeEntry()
Node 在这里注册自己的回调函数
↓
④ App 打开相机 → 创建 Pipeline
在 XML 配置中找到了你的 Node
↓
⑤ 调用你的 pCreate()
创建 Node 实例,分配资源
↓
⑥ 调用 pQueryBufferInfo()
问你的 Node:输入输出要什么格式?
↓
⑦ 每来一帧 → 调用 pProcessRequest()
你的算法在这里处理图像
↓
⑧ App 关闭相机 → 调用 pDestroy()
释放资源
高通提供了多个参考 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 加速 |
首先创建你的 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 ← 编译脚本
// ─── 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
// ─── 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;
// ─── 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;
}
// ─── 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;
}
// ─── 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;
}
// ─── 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;
}
// ─── 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;
}
// ─── 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;
}
// 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;
}
// ─── 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);
}
}
// ─── 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;
}
// ─── 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
// 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);
// 如果有动态分配的资源,在这里释放
}
# ─── 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)
# 进入你的 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
Node 编译好了,但系统还不知道你的 Node 要插在 Pipeline 的什么位置。这一步要修改 XML 配置文件。
Pipeline 拓扑定义在 XML 文件中,位置取决于平台:
# 新平台(xxx/xxx 等)—— usecase-components 方式
chi-cdk/oem/qcom/topology/usecase-components/usecases/
└── UsecaseZSL/
└── camxUsecaseZSL.xml ← 预览/ZSL 的拓扑
└── UsecaseSnapshot/
└── camxUsecaseSnapshot.xml ← 拍照的拓扑
打开对应的 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>
找到 <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>
高通保留了一些 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 冲突。
# 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
重启后,查看 logcat 中是否出现你的 Node 的 log:
adb logcat | grep CHIBEAUTY
如果看到 BeautyNode created,说明 Node 被成功加载和创建。
搜索 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 工具分析
# 查 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
可能的原因:
.so 依赖的库(如 libchi-cdk)不存在 → 检查 LOCAL_SHARED_LIBRARIESCreate 中崩溃了 → 检查 phNodeSession 是否正确赋值≥ 65544 的范围# 检查 components 目录
adb shell ls -la /vendor/lib64/camera/components/ | grep beauty
# 检查驱动是否加载了 .so
adb logcat | grep -E "ChiNode|components|dlopen"
可能的原因:
.so 没有导出 ChiNodeEntry 函数 → 检查是否有 extern "C" 和 CDK_VISIBILITY_PUBLICCHINODECALLBACKS 的 size 或 majorVersion 检查不通过 → 在 ChiNodeEntry 中加 log.so 不在 components/ 目录 → 确认 adb push 路径正确可能的原因:
pProcessRequest 没被调用 → 加 log 确认BufferMemFlagCache → 在 QueryBufferInfo 中加上// 在 ProcessRequest 中加耗时统计
uint64_t start = /* 获取当前时间戳 */;
// ... 算法处理 ...
uint64_t end = /* 获取当前时间戳 */;
ALOGI("%s: Frame %llu processed in %lld ms",
LOG_TAG, pProcessRequestInfo->frameNum, (end - start) / 1000000);
如果每帧处理时间 > 33ms(30fps),说明算法太慢。优化方向:
你的最终目录应该是这样的:
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/
第三方算法集成 = 写一个自定义 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 中)
chi-cdk/api/node/chinode.hchi-cdk/oem/qcom/node/memcpy/camxchinodememcpy.cppchi-cdk/oem/qcom/topology/usecase-components/