CAMX ChiFeature2 FeatureGraph 多相机资源管理实战
2026/9/17 6:30:01 网站建设 项目流程

简介:面向Android相机底层开发与图像处理方向的技术人员,这份PDF围绕高通CAMX架构中的ChiFeature2框架展开,系统梳理了Feature2涉及的关键结构体、端口缓冲信息、锚点帧选择数据、回调接口及多相机资源管理等核心数据结构,并以图形化关系图呈现庞大而复杂的调用与依赖脉络。内容覆盖请求创建、FeatureGraph管理、数据流处理到最终输出,兼顾单摄与多摄场景下的实时视频流、多帧处理、HDR合成和特征提取需求,也涉及锚点同步、帧选择与实时序列化等模块。压缩包仅含1个PDF文件,约82KB,便于离线检索与查阅。目前已有439人学习,适合具备嵌入式或相机模块经验、希望深入理解Android底层相机框架并优化图像质量与处理效率的开发者,可借助结构体关系图与接口定义理清ChiFeature2各组件的交互方式,为自定义相机应用、增强现实或计算机视觉项目提供底层参考。

1. 从一堆结构体到一张图:ChiFeature2 到底在管什么

第一次打开 CAMX 里 ChiFeature2 的头文件,多数人的反应是"这不可能读完"。ChiFeature2BaseChiFeature2GraphDescChiFeature2UsecaseRequestObjectChiFeature2StageDescriptorChiFeature2PrunableVariant……几百个结构体层层嵌套,指针套指针,回调里再挂回调。但真正做过多摄并发或者 HDR 合成的人会发现,它们其实只回答三个问题:这一帧要经过哪些特征节点、每个节点吃哪块 buffer、跑完之后谁来回收。把这三点抽出来,整棵类型树就塌缩成一张有向图,这也是 Feature2 里 FeatureGraph 这个名字的由来。

它解决的是高通相机 HAL 从早期 Feature1 单管线模型向多相机、多帧、可裁剪管线演进时暴露出来的资源管理难题。适合谁读?写过 Camera HAL、调过 multi-camera 同步、被 ZSL 或离线重处理卡过吞吐的工程师。新手直接啃会很吃力,建议先把 Request、Port、Metadata 三件套弄清楚再回头。

2. ChiFeature2 数据结构分层与 FeatureGraph 建模原理

2.1 类型树的四层划分

把输入里那一长串结构体按作用归类,基本落在四层:描述层、请求层、运行层、回调层。

层级代表结构体生命周期谁创建
描述层ChiFeature2GraphDesc/ChiFeature2StageDescriptor/ChiFeature2PipelineDescriptor进程级,静态FeatureGraphManager
请求层ChiFeature2UsecaseRequestObject/ChiFeature2RequestObject/ChiFeature2RequestMap每帧Feature2 框架
运行层ChiFeature2Graph/FeatureGraphManagerSessionData/ChiTargetBufferManager会话级Usecase
回调层ChiFeature2GraphManagerCallbacks/ChiFeature2GraphCallbackData会话级,函数指针OEM/Usecase

描述层是"图纸",它不参与每帧分配。ChiFeature2GraphDesc里挂pStagesnumStagespFeatureInstancesnumFeatureInstances,描述的是这张图有哪些特征、分成几段、实例属性怎么配。请求层才是"零件",每来一帧 captureRequest 就生成一个ChiFeature2UsecaseRequestObject,把物理相机、输入流、输出流、metadata 引用一起打包。

运行层是把图纸实例化之后的活体,FeatureGraphManagerSessionData持有pFeatureGraphppFeatureGraphDescm_pSessionSettings,负责把描述层翻译成真正的ChiFeature2Graph。回调层不存数据,只存函数指针和私有上下文,让 OEM 能在 deserialize、serialize、节点处理这几个时刻插自己的逻辑。

2.2 FeatureGraph 的节点、端口与依赖

ChiFeature2Graph是由ChiFeature2GraphNode组成的,节点之间靠ChiFeature2GraphLinkData连接。每个节点有输入端口和输出端口,ChiFeature2PortDescriptor描述端口名、方向、是否 sink,ChiFeature2DependencyConfigDescriptor则描述端口之间的依赖关系和批次索引。

理解依赖的关键是三个字段:requestIndexbatchIndexdependencyIndex

  • requestIndex:这个依赖属于第几次请求,多帧处理时 ZSL 会回吐历史帧,靠它区分。
  • batchIndexChiFeature2DependencyBatch里的批次号,一个批次内的依赖可以并行触发。
  • dependencyIndex:同一批次内的序号,用于精确定位是哪一路输入。

ChiFeature2PrunableVariant是 Feature2 里很妙的一个设计。图上每条链路可以挂若干可裁剪变体,字段里有pruneVariantTypepruneVariantMaskgroupIdprunableVariant。运行时根据当前场景(比如 bokeh 模式没开、QR 扫描没触发)把不需要的链路整条剪掉,避免白白跑一遍 GPU。ChiFeature2PruneRule进一步给出 minFrameCountForAnchor、logicalRequestPruneMask 这类规则,配合ChiFeature2GraphSelectorOEM做选择。

2.3 用 Graphviz 把结构体关系画出来

手动读完几百个结构体不现实,常见做法是写脚本从预处理后的头文件里抽字段,生成 DOT,再渲染成图。下面是一个可复用的抽取脚本:

# parse_chi_structs.py # 从预处理过的 CAMX 头文件里抽取结构体之间的"包含"关系 import re, sys # 匹配 struct Foo { ... }; 整体 STRUCT_RE = re.compile(r'struct\s+(Chi\w+)\s*\{(.*?)\}\s*;', re.S) # 抓成员:类型名 + 变量名,只保留 Chi 开头的类型 FIELD_RE = re.compile(r'\b(Chi\w*)\s+[\w\*]+\s*;') def parse(path): src = open(path, encoding='utf-8', errors='ignore').read() edges, nodes = [], set() for name, body in STRUCT_RE.findall(src): nodes.add(name) for ftype in FIELD_RE.findall(body): if ftype != name: edges.append((name, ftype)) nodes.add(ftype) return nodes, edges def to_dot(nodes, edges, out): with open(out, 'w', encoding='utf-8') as f: f.write('digraph ChiFeature2 {\n rankdir=LR;\n node [shape=box,fontsize=10];\n') for n in sorted(nodes): f.write(f' "{n}";\n') for a, b in set(edges): f.write(f' "{a}" -> "{b}";\n') # 有向边表示包含关系 f.write('}\n') if __name__ == '__main__': n, e = parse(sys.argv[1]) to_dot(n, e, sys.argv[2]) # 输出 .dot print(f'nodes={len(n)} edges={len(e)}')

逻辑说明:先按最外层struct ... { ... };切块,正则用了re.S.跨行匹配,否则嵌套结构体一多就会切错。FIELD_RE只抓Chi开头的类型,把uint32_tvoid*这类基础类型过滤掉,不然图会糊成一片。

参数说明:第一个参数是预处理后的头文件路径,建议先跑gcc -E -P -I<camx_inc> feature2.h -o feature2.i展开宏,否则带宏声明的成员会漏;第二个参数是输出 dot 文件。生成之后直接渲染:

# 大图渲染成 SVG,方便浏览器里缩放着看 dot -Tsvg chi.dot -o chi.svg # 只关心某个子图先看局部,避免几万个节点挤在一起 dot -Tsvg -Gdpi=150 -Kfdp chi.dot -o chi_fdp.svg

节点数上千时dot布局会非常慢,换成sfdpfdp通常几十秒内出图。如果只想看请求层的调用链,可以在to_dot里加白名单keep = {'ChiFeature2RequestObject', ...},把不相关的边剪掉。

2.4 从结构体走向运行时对象的时机

结构体只是静态定义,真正决定行为的时机有三个。

第一是图创建时刻。ChiFeature2GraphCreateInputInfo携带pFeatureGraphNamepStreamDescpFeatureDependenciesmaxDependencyPorts,FeatureGraphManager 在这一步校验依赖端口数量是否够用。

第二是会话建立时刻。FeatureGraphManagerSessionDatam_numInternalLinksm_pEnabledFeaturesm_pSessionDescriptor定型,此时决定启用哪些特征、内链怎么连。

第三是每帧请求时刻。ChiFeature2UsecaseRequestObjectm_flagsm_requestStatecaptureType被打上,然后ChiFeature2RequestObjectm_pPortMap把每个 port 映射到具体 buffer。这三步对应"图纸→生产线→零件",任何一步参数填错,后面全是空指针。

提示:调试 Feature2 时先从ChiFeature2RequestOutputInfo入手反查,它到ChiFeature2RequestObject的链路最短,比从头翻GraphDesc快得多。

3. 数据结构图形化工具的落地实现与参数调优

3.1 交互式浏览:把 DOT 喂给浏览器

静态 SVG 一旦上千节点就难查,我给的做法是转成 JSON 再配前端渲染,或者直接用vis.js的 network。下面把 DOT 转成 JSON 的小脚本:

# dot_to_json.py —— 把 dot 转成前端可加载的节点/边 JSON import json, re, sys def load_dot(path): nodes, edges = {}, [] with open(path, encoding='utf-8') as f: for line in f: line = line.strip() m = re.match(r'"(\w+)"\s*->\s*"(\w+)"', line) if m: edges.append({'from': m.group(1), 'to': m.group(2)}) continue m = re.match(r'"(\w+)";', line) if m: n = m.group(1) # 按前缀分组,前端据此上色 group = 'req' if 'Request' in n else ('graph' if 'Graph' in n else 'base') nodes[n] = {'id': n, 'group': group} return list(nodes.values()), edges if __name__ == '__main__': n, e = load_dot(sys.argv[1]) json.dump({'nodes': n, 'edges': e}, open(sys.argv[2], 'w'), ensure_ascii=False) print(f'nodes={len(n)} edges={len(e)}')

分组字段group是给前端上色用的,请求层、图形层、基础层各一色,肉眼看的时候一眼能分辨层级。边去重很重要,同一个类型被多个结构体引用时会在 DOT 里出现多条同名边。

3.2 大图布局参数对比

不同布局引擎对这类结构体图的适用度差别很大,实测下来:

引擎适用节点量特点建议
dot< 500层次清晰,交叉少画子图
fdp500 ~ 3000力导向,速度可接受全量图首选
sfdp> 3000最快,边会糊大图预览
neato中等类似力导向少用

命令行示例:

# sfdp 处理上万边的大图,加 overlap 避免节点重叠 sfdp -Tsvg -Goverlap=prism -Gsize=30,30 chi.dot -o chi_big.svg

-Goverlap=prism让引擎在布局后做一次去重叠处理,代价是稍慢,但大图上节点不再糊成一团。-Gsize控制画布尺寸,太大浏览器会卡,一般 30 英寸够用。

3.3 输出可交互 HTML

想省掉前端工程,可以直接用pygraphviz生成交互式 HTML,或者套用成熟的 vis-network 模板:

# gen_html.py —— 生成内嵌 JSON 的单文件 HTML import json TEMPLATE = """<!DOCTYPE html><html><head><meta charset="utf-8"> <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> <style>#net{width:100vw;height:100vh;}</style></head> <body><div id="net"></div><script> var data = %s; var nodes = new vis.DataSet(data.nodes); var edges = new vis.DataSet(data.edges); var container = document.getElementById('net'); new vis.Network(container, {nodes:nodes, edges:edges}, { physics:{stabilization:{iterations:200}}, // 布局稳定迭代次数 nodes:{shape:'box', font:{size:12}}, edges:{arrows:'to', smooth:false} }); </script></body></html>""" data = json.load(open('chi.json', encoding='utf-8')) open('chi_view.html', 'w', encoding='utf-8').write(TEMPLATE % json.dumps(data)) print('html written')

physics.stabilization.iterations是布局精度的核心参数,数值越大布局越稳、加载越慢;200 是图规模几千时的折中点。smooth:false关掉曲线边,能省不少渲染开销,大图必开。

提示:把生成好的 HTML 和chi.dot一起放进版本库,头文件一改就重跑脚本 diff,能第一时间发现结构体字段被增删。

4. 多相机实时链路与资源管理实战

4.1 多相机图与单相机图的差异点

ChiFeature2GraphDesc里的isMultiCameraGraph是分水岭。置真时,ChiFeature2UsecaseRequestObject会用到physicalCameraIdm_pPhysicalCameraIdChiFeature2UsecaseRequestObjectExtSrcStreamData也要按perCamera展开。单相机图不会走ChiFeature2AnchorSync,多相机图一旦启用同步,ChiFeature2AnchorSyncDataanchorFrameIdxnumberOfFrames就会决定是对齐还是丢帧。

常见任务的两条路径:

  1. 实时预览多摄:ChiFeature2RealTimeMCX负责,m_pHALRequestSem控制提交节流。
  2. 离线重处理:ChiFeature2MCReprocessRT接在实时链路后面,依赖ChiFeature2ZSLData里的lastFrameNumber回捞历史帧。

m_isVideoStreamEnabledm_isRTCapInputYUVStreamEnabled这两个标志位直接决定以上哪条路径被实例化。

4.2 目标 buffer 与 metadata 池的分配

ChiTargetBufferManagerCreateData是这块的核心,字段含义得一个个对:

字段含义典型取值
pTargetBufferNamebuffer 名,对应流名"TBM_Preview"
numOfMetadataBuffersmetadata buffer 总数视帧率与延迟定
minMetaBufferCount/maxMetaBufferCount上下水位2 / 8
pMetadataManager元数据管理器句柄外部传入
numOfInternalStreamBuffers内部流 buffer 数与内链端口数对齐
numOfExternalStreamBuffers外部流 buffer 数与 sink 端口数对齐
isChiFenceEnabled是否用 fence 同步通常 true
isChiGrallocBufferUsed是否走 gralloc依平台

minMetaBufferCount设太小会在高帧率下频繁等 buffer,设太大会撑爆内存。常见做法是按"最大并发请求数 + 2"来配,用ChiFeature2BufferMetadataInfo里的bufferMetaRefCnt观察引用计数,跑到高位就说明缓冲欠配。

metadata 客户端由m_genericMetadataClientIdmetadataClientId标识,ChiFeature2MetadataInfo里的pMetadatapMCCResult是跨节点传递的关键。ChiFeature2FrameInfoStatsRegeneration里的local3ADebugDatatintlessStatsData只在调试时开,线上常关掉避免无谓拷贝。

4.3 用 API 查询图与实例信息

调试阶段先查后跑,别急着改代码:

// 查询 Feature2 能力与已注册的图描述数量 ChiFeature2QueryInfo queryInfo = {}; ChiFeature2GraphManagerCallbacks callbacks = {}; UINT32 numCaps = 0; ChiFeature2Capability* pCaps = nullptr; queryInfo.numCaps = &numCaps; queryInfo.ppCapabilities = &pCaps; // FeatureGraphManager 通过 callbacks 暴露能力查询入口 callbacks.pQueryCaps(&queryInfo); // numCaps 返回可用能力条目数,pCaps 指向能力数组,调用方只读

关键点在numCapsppCapabilities是出参,调用前必须清零,否则返回值会是野指针。ChiFeature2Descriptor里的pFeatureDescnumFeatureInstances可以进一步查看每个特征被实例化几次。

再看请求对象创建的输入结构:

// 构造请求对象时最容易被忽略的几个字段 ChiFeature2RequestObjectCreateInputInfo info = {}; info.pTargetName = "Snapshot"; // 目标名,影响流选择 info.instanceId = 0; // 特征实例,多实例时不能写死 info.pGraphPrivateData = nullptr; // OEM 私有数据,可为空 info.m_frameIDMeta = frameNumber; // 帧号,用于 offset 对齐 info.m_lastZSLFrameNumber = lastZSL; // ZSL 回捞边界,离线处理必填 ChiFeature2RequestObject* pReq = nullptr; g_pFeature2Base->pCreateFeature2(&info, &pReq);

m_lastZSLFrameNumber填错是离线处理的经典坑:图能建、能跑,但拿回来的永远是最近一帧,因为边界值被当成了无效范围直接退化。instanceId在多相机多实例场景必须按物理相机区分,写死 0 会导致所有请求挤到同一个实例上。

4.4 提交作业与 fence 同步

ChiFeature2RealTimem_hSubmissionJobm_pHALRequestSemm_maxFeatureExecutionTime三个字段配合着看。m_maxFeatureExecutionTime是软超时门限,超过这个时间还没跑完,框架会打印超时告警但未必中止任务。m_hSubmissionJob对应 job 句柄,下游依赖它做序列化提交。

Fence 由isChiFenceEnabled开关,配合Feature2BufferOptionsData里的pPortBufferStatus使用。常见失败模式:fence 未置位就提前读 buffer,表现为随机花屏,日志里通常能看到bufferErrorPresent为真。这时候先查ChiFeature2PortBufferMetadataInfo里的isValidForSnapshotbufferErrorPresent,比逐节点加打印效率高得多。

5. 裁剪规则与同步调试的几个关键技巧

5.1 PruneVariant 裁剪链路的验证

裁剪配错不会报错,只会静默丢链路,所以必须主动验证。用ChiFeature2PrunableVariantpruneVariantMaskpruneGroup对齐后,加一条查询确认当前生效的变体:

// 打印当前图中每个可裁剪组实际选中的变体 for (UINT32 i = 0; i < pGraphDesc->numStages; ++i) { ChiFeature2StageDescriptor* pStage = &pGraphDesc->pStages[i]; for (UINT32 j = 0; j < pStage->numPorts; ++j) { ChiFeature2PortDescriptor* pPort = &pStage->pPorts[j]; // pruneVariantMask 为 0 表示该端口无可裁剪变体 CAMX_LOG_INFO(CamxLogGroupChi, "stage=%s port=%s mask=0x%x pruneGroup=%u", pStage->pStageName, pPort->pPortName, pPort->pPruneVariantMask, pPort->pruneGroup); } }

maskgroupId一起决定哪条变体被选中,mask为 0 的端口代表该链路无条件保留。日志里如果看到预期该保留的端口 mask 为 0,说明规则写反了。

5.2 AnchorSync 对齐问题定位

多相机同步出问题时,先看ChiFeature2AnchorSyncDataanchorFrameIdxnumberOfFramesChiFeature2AnchorFrameSelectionData里的anchorFrameSelectionModemaxminnumImagesAllowedAsAnchor。同步选帧逻辑在ChiFeature2AnchorPickInputInfo里,focusValuehistogramminHistrogramBinmaxHistrogramBin参与打分。

排查顺序建议按"输入是否齐 → 锚点是否选出 → 对齐是否生效"来。输入不齐时先看numInputDependency与实际到达数是否一致;锚点选不出通常是打分阈值太严,isFWRaw16CbStream这类回调流未开也会卡住;对齐失效则查ChiFeature2AnchorSyncDatanumRealtimeLogicalOutputs有没有超出实际逻辑相机数。

5.3 参数速查与常见误配对照

参数误配后果建议做法
minMetaBufferCount高帧率下等 buffer 掉帧按最大并发 + 2 配
m_lastZSLFrameNumber离线只拿最新帧按用户选择的快门帧填
instanceId写死多实例请求挤一个按物理相机区分
isChiFenceEnabled随机花屏线上保持开启
pruneVariantMask链路静默丢失用日志逐端口核对
m_hasFWRaw16CbStream回调流不可用与流配置对齐

一个实操要点:把第 3 章生成的 HTML 图和第 2 章的抽取脚本一起用,改完裁剪规则后重跑脚本,diff 一下 DOT 就能看出哪些边被剪了。这类结构体图工具最大的价值不在"看一眼",而在于把每次改动的图结构变化变成可回溯的版本差异,出问题时能对着上一版图快速定位是哪个结构体字段被改动了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询