CAMX ChiFeature2 FeatureGraph 多相机资源管理实战
发布时间:2026/9/17 6:30:04 作者:尧图编辑部 阅读量:1,286

简介面向Android相机底层开发与图像处理方向的技术人员这份PDF围绕高通CAMX架构中的ChiFeature2框架展开系统梳理了Feature2涉及的关键结构体、端口缓冲信息、锚点帧选择数据、回调接口及多相机资源管理等核心数据结构并以图形化关系图呈现庞大而复杂的调用与依赖脉络。内容覆盖请求创建、FeatureGraph管理、数据流处理到最终输出兼顾单摄与多摄场景下的实时视频流、多帧处理、HDR合成和特征提取需求也涉及锚点同步、帧选择与实时序列化等模块。压缩包仅含1个PDF文件约82KB便于离线检索与查阅。目前已有439人学习适合具备嵌入式或相机模块经验、希望深入理解Android底层相机框架并优化图像质量与处理效率的开发者可借助结构体关系图与接口定义理清ChiFeature2各组件的交互方式为自定义相机应用、增强现实或计算机视觉项目提供底层参考。1. 从一堆结构体到一张图ChiFeature2 到底在管什么第一次打开 CAMX 里 ChiFeature2 的头文件多数人的反应是这不可能读完。ChiFeature2Base、ChiFeature2GraphDesc、ChiFeature2UsecaseRequestObject、ChiFeature2StageDescriptor、ChiFeature2PrunableVariant……几百个结构体层层嵌套指针套指针回调里再挂回调。但真正做过多摄并发或者 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里挂pStages、numStages、pFeatureInstances、numFeatureInstances描述的是这张图有哪些特征、分成几段、实例属性怎么配。请求层才是零件每来一帧 captureRequest 就生成一个ChiFeature2UsecaseRequestObject把物理相机、输入流、输出流、metadata 引用一起打包。运行层是把图纸实例化之后的活体FeatureGraphManagerSessionData持有pFeatureGraph、ppFeatureGraphDesc、m_pSessionSettings负责把描述层翻译成真正的ChiFeature2Graph。回调层不存数据只存函数指针和私有上下文让 OEM 能在 deserialize、serialize、节点处理这几个时刻插自己的逻辑。2.2 FeatureGraph 的节点、端口与依赖ChiFeature2Graph是由ChiFeature2GraphNode组成的节点之间靠ChiFeature2GraphLinkData连接。每个节点有输入端口和输出端口ChiFeature2PortDescriptor描述端口名、方向、是否 sinkChiFeature2DependencyConfigDescriptor则描述端口之间的依赖关系和批次索引。理解依赖的关键是三个字段requestIndex、batchIndex、dependencyIndex。requestIndex这个依赖属于第几次请求多帧处理时 ZSL 会回吐历史帧靠它区分。batchIndexChiFeature2DependencyBatch里的批次号一个批次内的依赖可以并行触发。dependencyIndex同一批次内的序号用于精确定位是哪一路输入。ChiFeature2PrunableVariant是 Feature2 里很妙的一个设计。图上每条链路可以挂若干可裁剪变体字段里有pruneVariantType、pruneVariantMask、groupId、prunableVariant。运行时根据当前场景比如 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(rstruct\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, encodingutf-8, errorsignore).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, encodingutf-8) as f: f.write(digraph ChiFeature2 {\n rankdirLR;\n node [shapebox,fontsize10];\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(fnodes{len(n)} edges{len(e)})逻辑说明先按最外层struct ... { ... };切块正则用了re.S让.跨行匹配否则嵌套结构体一多就会切错。FIELD_RE只抓Chi开头的类型把uint32_t、void*这类基础类型过滤掉不然图会糊成一片。参数说明第一个参数是预处理后的头文件路径建议先跑gcc -E -P -Icamx_inc feature2.h -o feature2.i展开宏否则带宏声明的成员会漏第二个参数是输出 dot 文件。生成之后直接渲染# 大图渲染成 SVG方便浏览器里缩放着看 dot -Tsvg chi.dot -o chi.svg # 只关心某个子图先看局部避免几万个节点挤在一起 dot -Tsvg -Gdpi150 -Kfdp chi.dot -o chi_fdp.svg节点数上千时dot布局会非常慢换成sfdp或fdp通常几十秒内出图。如果只想看请求层的调用链可以在to_dot里加白名单keep {ChiFeature2RequestObject, ...}把不相关的边剪掉。2.4 从结构体走向运行时对象的时机结构体只是静态定义真正决定行为的时机有三个。第一是图创建时刻。ChiFeature2GraphCreateInputInfo携带pFeatureGraphName、pStreamDesc、pFeatureDependencies、maxDependencyPortsFeatureGraphManager 在这一步校验依赖端口数量是否够用。第二是会话建立时刻。FeatureGraphManagerSessionData里m_numInternalLinks、m_pEnabledFeatures、m_pSessionDescriptor定型此时决定启用哪些特征、内链怎么连。第三是每帧请求时刻。ChiFeature2UsecaseRequestObject的m_flags、m_requestState、captureType被打上然后ChiFeature2RequestObject走m_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, encodingutf-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_asciiFalse) print(fnodes{len(n)} edges{len(e)})分组字段group是给前端上色用的请求层、图形层、基础层各一色肉眼看的时候一眼能分辨层级。边去重很重要同一个类型被多个结构体引用时会在 DOT 里出现多条同名边。3.2 大图布局参数对比不同布局引擎对这类结构体图的适用度差别很大实测下来引擎适用节点量特点建议dot 500层次清晰交叉少画子图fdp500 ~ 3000力导向速度可接受全量图首选sfdp 3000最快边会糊大图预览neato中等类似力导向少用命令行示例# sfdp 处理上万边的大图加 overlap 避免节点重叠 sfdp -Tsvg -Goverlapprism -Gsize30,30 chi.dot -o chi_big.svg-Goverlapprism让引擎在布局后做一次去重叠处理代价是稍慢但大图上节点不再糊成一团。-Gsize控制画布尺寸太大浏览器会卡一般 30 英寸够用。3.3 输出可交互 HTML想省掉前端工程可以直接用pygraphviz生成交互式 HTML或者套用成熟的 vis-network 模板# gen_html.py —— 生成内嵌 JSON 的单文件 HTML import json TEMPLATE !DOCTYPE htmlhtmlheadmeta charsetutf-8 script srchttps://unpkg.com/vis-network/standalone/umd/vis-network.min.js/script style#net{width:100vw;height:100vh;}/style/head bodydiv idnet/divscript 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, encodingutf-8)) open(chi_view.html, w, encodingutf-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会用到physicalCameraId、m_pPhysicalCameraIdChiFeature2UsecaseRequestObjectExtSrcStreamData也要按perCamera展开。单相机图不会走ChiFeature2AnchorSync多相机图一旦启用同步ChiFeature2AnchorSyncData的anchorFrameIdx、numberOfFrames就会决定是对齐还是丢帧。常见任务的两条路径实时预览多摄ChiFeature2RealTimeMCX负责m_pHALRequestSem控制提交节流。离线重处理ChiFeature2MCReprocessRT接在实时链路后面依赖ChiFeature2ZSLData里的lastFrameNumber回捞历史帧。m_isVideoStreamEnabled、m_isRTCapInputYUVStreamEnabled这两个标志位直接决定以上哪条路径被实例化。4.2 目标 buffer 与 metadata 池的分配ChiTargetBufferManagerCreateData是这块的核心字段含义得一个个对字段含义典型取值pTargetBufferNamebuffer 名对应流名TBM_PreviewnumOfMetadataBuffersmetadata buffer 总数视帧率与延迟定minMetaBufferCount/maxMetaBufferCount上下水位2 / 8pMetadataManager元数据管理器句柄外部传入numOfInternalStreamBuffers内部流 buffer 数与内链端口数对齐numOfExternalStreamBuffers外部流 buffer 数与 sink 端口数对齐isChiFenceEnabled是否用 fence 同步通常 trueisChiGrallocBufferUsed是否走 gralloc依平台minMetaBufferCount设太小会在高帧率下频繁等 buffer设太大会撑爆内存。常见做法是按最大并发请求数 2来配用ChiFeature2BufferMetadataInfo里的bufferMetaRefCnt观察引用计数跑到高位就说明缓冲欠配。metadata 客户端由m_genericMetadataClientId和metadataClientId标识ChiFeature2MetadataInfo里的pMetadata、pMCCResult是跨节点传递的关键。ChiFeature2FrameInfoStatsRegeneration里的local3ADebugData、tintlessStatsData只在调试时开线上常关掉避免无谓拷贝。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 指向能力数组调用方只读关键点在numCaps与ppCapabilities是出参调用前必须清零否则返回值会是野指针。ChiFeature2Descriptor里的pFeatureDesc、numFeatureInstances可以进一步查看每个特征被实例化几次。再看请求对象创建的输入结构// 构造请求对象时最容易被忽略的几个字段 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 同步ChiFeature2RealTime里m_hSubmissionJob、m_pHALRequestSem、m_maxFeatureExecutionTime三个字段配合着看。m_maxFeatureExecutionTime是软超时门限超过这个时间还没跑完框架会打印超时告警但未必中止任务。m_hSubmissionJob对应 job 句柄下游依赖它做序列化提交。Fence 由isChiFenceEnabled开关配合Feature2BufferOptionsData里的pPortBufferStatus使用。常见失败模式fence 未置位就提前读 buffer表现为随机花屏日志里通常能看到bufferErrorPresent为真。这时候先查ChiFeature2PortBufferMetadataInfo里的isValidForSnapshot和bufferErrorPresent比逐节点加打印效率高得多。5. 裁剪规则与同步调试的几个关键技巧5.1 PruneVariant 裁剪链路的验证裁剪配错不会报错只会静默丢链路所以必须主动验证。用ChiFeature2PrunableVariant的pruneVariantMask和pruneGroup对齐后加一条查询确认当前生效的变体// 打印当前图中每个可裁剪组实际选中的变体 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 mask0x%x pruneGroup%u, pStage-pStageName, pPort-pPortName, pPort-pPruneVariantMask, pPort-pruneGroup); } }mask与groupId一起决定哪条变体被选中mask为 0 的端口代表该链路无条件保留。日志里如果看到预期该保留的端口 mask 为 0说明规则写反了。5.2 AnchorSync 对齐问题定位多相机同步出问题时先看ChiFeature2AnchorSyncData的anchorFrameIdx、numberOfFrames和ChiFeature2AnchorFrameSelectionData里的anchorFrameSelectionMode、max、min、numImagesAllowedAsAnchor。同步选帧逻辑在ChiFeature2AnchorPickInputInfo里focusValue、histogram、minHistrogramBin、maxHistrogramBin参与打分。排查顺序建议按输入是否齐 → 锚点是否选出 → 对齐是否生效来。输入不齐时先看numInputDependency与实际到达数是否一致锚点选不出通常是打分阈值太严isFWRaw16CbStream这类回调流未开也会卡住对齐失效则查ChiFeature2AnchorSyncData的numRealtimeLogicalOutputs有没有超出实际逻辑相机数。5.3 参数速查与常见误配对照参数误配后果建议做法minMetaBufferCount高帧率下等 buffer 掉帧按最大并发 2 配m_lastZSLFrameNumber离线只拿最新帧按用户选择的快门帧填instanceId写死多实例请求挤一个按物理相机区分isChiFenceEnabled关随机花屏线上保持开启pruneVariantMask链路静默丢失用日志逐端口核对m_hasFWRaw16CbStream回调流不可用与流配置对齐一个实操要点把第 3 章生成的 HTML 图和第 2 章的抽取脚本一起用改完裁剪规则后重跑脚本diff 一下 DOT 就能看出哪些边被剪了。这类结构体图工具最大的价值不在看一眼而在于把每次改动的图结构变化变成可回溯的版本差异出问题时能对着上一版图快速定位是哪个结构体字段被改动了。本文还有配套的精品资源点击获取