AI 叠加层 API
AI Overlay Control Client
Control the platform’s AI overlay system that draws detection boxes, labels, and confidence scores directly on NV12 frames before encoding. Zero CPU cost — drawing happens in camera-daemon before encoding.
Two halves:
configuration —
enable/disable/configure/applyvia theUpdateAiOverlayRPC;content —
annotate/annotate_resultpush detection boxes (or classifications / landmarks / OCR lines) through the event bus; camera-daemon’s overlay subscriber renders them with its HAL draw ops.
Uses gRPC over Unix domain socket, consistent with other SDK clients.
- class neoruntime_ipc_sdk.overlay.OverlayConfig(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)[source]
Bases:
objectAI overlay configuration.
strict_frame_lock/strict_wait_cap_msmirror the optional wire fields:None(the default) leaves them unset, so the daemon keeps its current (yaml) strict settings.- enabled: bool = True
- show_label: bool = True
- show_confidence: bool = True
- line_thickness: int = 2
- box_color: int = 0
- label_color: int = 0
- font_size: int = 0
- to_proto()[source]
- __init__(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)
- class neoruntime_ipc_sdk.overlay.OverlayClient(*args, **kwargs)[source]
Bases:
GrpcClientAI Overlay Control Client
Uses gRPC to communicate with camera-daemon’s CameraControl service.
Usage:
from neoruntime_ipc_sdk import OverlayClient oc = OverlayClient() # Enable overlay with default settings oc.enable() # Customize appearance oc.configure( show_label=True, show_confidence=True, line_thickness=3, ) # Disable overlay oc.disable()
- Environment variables:
- CAMERA_CONTROL_ENDPOINT: Camera control gRPC endpoint
(default: unix:///run/aipc/camera-control.sock)
- __init__(*args, **kwargs)[source]
- close()[source]
- property channel_options
gRPC channel options applied at connect time.
- enable(show_label=True, show_confidence=True, line_thickness=2)[source]
Enable AI overlay with specified settings.
- disable()[source]
Disable AI overlay.
- configure(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)[source]
Configure AI overlay with full control.
- Parameters:
enabled (bool) – Enable or disable overlay
show_label (bool) – Show class label on detections
show_confidence (bool) – Show confidence score on detections
line_thickness (int) – Box line thickness (1-10)
box_color (int) – Box color in ARGB format (e.g. 0xFFFF0000 for red)
label_color (int) – Label color in ARGB format
font_size (int) – Font size (8-72)
strict_frame_lock (bool | None) – opt in to / opt out of strict frame-lock on identity-fed display streams (streams whose
stream_mapentry feeds them their own inference results). Strict mode waits — bounded bystrict_wait_cap_ms— for each frame’s own inference result at the bake site, so a box lands on the frame it was computed from instead of the newest frame.Nonekeeps the daemon’s current setting.strict_wait_cap_ms (int | None) – max ms the bake site waits per frame before degrading to the freshest result (warned + counted). 0 derives the cap from the stream fps (2 frame periods, clamp [1, 500]).
Nonekeeps the current setting; only meaningful together withstrict_frame_lock.
- apply(config)[source]
Apply an OverlayConfig object.
- annotate(stream_id, detections=None, *, polygons=None, ttl_ms=None, session_id=None, frame_sequence=None, stream_epoch=None)[source]
Draw detection boxes / zone polygons on
stream_id’s video.- Parameters:
stream_id (str) – the camera stream the boxes belong to (e.g.
"main") — camera-daemon keys overlay results by it.detections (list | None) –
DetectedObjectitems or dicts withlabel/score(orconfidence) andbbox({"x","y","width","height"}or[x, y, w, h]). A detection’strack_id(>= 0) rides along and renders as a#idlabel suffix.Noneleaves the stream’s previous boxes untouched, so polygons can push alone.polygons (list | None) – dicts
{"points": [[x, y], ...], "label": str, "closed": bool, "color": ARGB int, "thickness": px int (-1 = filled)}with onlypointsrequired — points are normalized [0, 1] floats, 2..128 per polygon, at most 16 polygons per call. Any list ([]included) replaces the stream’s previous polygons;Nonekeeps them.ttl_ms (int | None) – positive int — per-event validity override for the stream’s results. Omitted: the daemon derives a default from the stream’s fps (about two frame periods).
session_id (str | None) – tag this event as belonging to a lifecycle session (P2-13). The daemon marks the stream’s results with the tag and a
"session/end"event sweeps matching tagged polygons on disconnect; an untagged write resets the mark.None/""leave the event untagged.frame_sequence (int | None) – bind this event to that frame of the stream (positive int —
InferenceResult.frame_sequencefrom asubscribe()iteration is the natural source). The daemon draws the layer only while the bake site is within its frame-bind slack of it, then expires it, and rejects events whose frame the pipeline already passed.Noneleaves the event unbound (legacy semantics).stream_epoch (int | None) – the stream generation counter (positive int, from
get_stream_status()’sstream_epoch) this event was made against. A ReconfigureEncoder restart bumps the epoch — the daemon rejects events carrying a stale one instead of drawing ghosts over the new generation.Noneskips the check.
- Returns:
The published event id.
- Return type:
Results expire
ttl_msafter the last event (fps-derived default), so call this at a few Hz while detections are fresh — publish an emptydetectionslist to clear boxes, an emptypolygonslist to clear zones. The overlay itself must be on:enable()first.oc = OverlayClient() oc.enable() for result in inf.subscribe(stream, model): oc.annotate(stream, result.objects) # zone polygons ride independently of the boxes channel oc.annotate(stream, polygons=[{"points": ZONE, "label": "yard"}])
- annotate_result(stream_id, result, *, ttl_ms=None, session_id=None, frame_sequence=None, stream_epoch=None)[source]
Push whichever section of an
InferenceResultis populated.Detections take precedence, then classifications, landmarks and OCR lines — a payload carries one kind (that is how camera-daemon’s parser discriminates them). An empty result publishes zero detections, clearing stale boxes.
ttl_msoverrides the stream’s result validity window for this event.session_idtags the event with a lifecycle session (P2-13).frame_sequence/stream_epochbind the event to the stream’s frame generation — seeannotate().
OverlayClient
- class neoruntime_ipc_sdk.OverlayClient(*args, **kwargs)[source]
Bases:
GrpcClientAI Overlay Control Client
Uses gRPC to communicate with camera-daemon’s CameraControl service.
Usage:
from neoruntime_ipc_sdk import OverlayClient oc = OverlayClient() # Enable overlay with default settings oc.enable() # Customize appearance oc.configure( show_label=True, show_confidence=True, line_thickness=3, ) # Disable overlay oc.disable()
- Environment variables:
- CAMERA_CONTROL_ENDPOINT: Camera control gRPC endpoint
(default: unix:///run/aipc/camera-control.sock)
- __init__(*args, **kwargs)[source]
- close()[source]
- property channel_options
gRPC channel options applied at connect time.
- enable(show_label=True, show_confidence=True, line_thickness=2)[source]
Enable AI overlay with specified settings.
- disable()[source]
Disable AI overlay.
- configure(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)[source]
Configure AI overlay with full control.
- Parameters:
enabled (bool) – Enable or disable overlay
show_label (bool) – Show class label on detections
show_confidence (bool) – Show confidence score on detections
line_thickness (int) – Box line thickness (1-10)
box_color (int) – Box color in ARGB format (e.g. 0xFFFF0000 for red)
label_color (int) – Label color in ARGB format
font_size (int) – Font size (8-72)
strict_frame_lock (bool | None) – opt in to / opt out of strict frame-lock on identity-fed display streams (streams whose
stream_mapentry feeds them their own inference results). Strict mode waits — bounded bystrict_wait_cap_ms— for each frame’s own inference result at the bake site, so a box lands on the frame it was computed from instead of the newest frame.Nonekeeps the daemon’s current setting.strict_wait_cap_ms (int | None) – max ms the bake site waits per frame before degrading to the freshest result (warned + counted). 0 derives the cap from the stream fps (2 frame periods, clamp [1, 500]).
Nonekeeps the current setting; only meaningful together withstrict_frame_lock.
- apply(config)[source]
Apply an OverlayConfig object.
- annotate(stream_id, detections=None, *, polygons=None, ttl_ms=None, session_id=None, frame_sequence=None, stream_epoch=None)[source]
Draw detection boxes / zone polygons on
stream_id’s video.- Parameters:
stream_id (str) – the camera stream the boxes belong to (e.g.
"main") — camera-daemon keys overlay results by it.detections (list | None) –
DetectedObjectitems or dicts withlabel/score(orconfidence) andbbox({"x","y","width","height"}or[x, y, w, h]). A detection’strack_id(>= 0) rides along and renders as a#idlabel suffix.Noneleaves the stream’s previous boxes untouched, so polygons can push alone.polygons (list | None) – dicts
{"points": [[x, y], ...], "label": str, "closed": bool, "color": ARGB int, "thickness": px int (-1 = filled)}with onlypointsrequired — points are normalized [0, 1] floats, 2..128 per polygon, at most 16 polygons per call. Any list ([]included) replaces the stream’s previous polygons;Nonekeeps them.ttl_ms (int | None) – positive int — per-event validity override for the stream’s results. Omitted: the daemon derives a default from the stream’s fps (about two frame periods).
session_id (str | None) – tag this event as belonging to a lifecycle session (P2-13). The daemon marks the stream’s results with the tag and a
"session/end"event sweeps matching tagged polygons on disconnect; an untagged write resets the mark.None/""leave the event untagged.frame_sequence (int | None) – bind this event to that frame of the stream (positive int —
InferenceResult.frame_sequencefrom asubscribe()iteration is the natural source). The daemon draws the layer only while the bake site is within its frame-bind slack of it, then expires it, and rejects events whose frame the pipeline already passed.Noneleaves the event unbound (legacy semantics).stream_epoch (int | None) – the stream generation counter (positive int, from
get_stream_status()’sstream_epoch) this event was made against. A ReconfigureEncoder restart bumps the epoch — the daemon rejects events carrying a stale one instead of drawing ghosts over the new generation.Noneskips the check.
- Returns:
The published event id.
- Return type:
Results expire
ttl_msafter the last event (fps-derived default), so call this at a few Hz while detections are fresh — publish an emptydetectionslist to clear boxes, an emptypolygonslist to clear zones. The overlay itself must be on:enable()first.oc = OverlayClient() oc.enable() for result in inf.subscribe(stream, model): oc.annotate(stream, result.objects) # zone polygons ride independently of the boxes channel oc.annotate(stream, polygons=[{"points": ZONE, "label": "yard"}])
- annotate_result(stream_id, result, *, ttl_ms=None, session_id=None, frame_sequence=None, stream_epoch=None)[source]
Push whichever section of an
InferenceResultis populated.Detections take precedence, then classifications, landmarks and OCR lines — a payload carries one kind (that is how camera-daemon’s parser discriminates them). An empty result publishes zero detections, clearing stale boxes.
ttl_msoverrides the stream’s result validity window for this event.session_idtags the event with a lifecycle session (P2-13).frame_sequence/stream_epochbind the event to the stream’s frame generation — seeannotate().
OverlayConfig
- class neoruntime_ipc_sdk.OverlayConfig(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)[source]
AI overlay configuration.
strict_frame_lock/strict_wait_cap_msmirror the optional wire fields:None(the default) leaves them unset, so the daemon keeps its current (yaml) strict settings.- __init__(enabled=True, show_label=True, show_confidence=True, line_thickness=2, box_color=0, label_color=0, font_size=0, strict_frame_lock=None, strict_wait_cap_ms=None)
使用示例
启用叠加层
from neoruntime_ipc_sdk import OverlayClient
overlay = OverlayClient()
# 启用硬件叠加层:在视频上绘制检测框 + 标签 + 置信度
overlay.enable(show_label=True, show_confidence=True, line_thickness=2)
# 关闭
overlay.disable()
自定义样式
# configure 一次设置全部样式参数
overlay.configure(
enabled=True,
show_label=True,
show_confidence=False,
line_thickness=3,
box_color=0x00FF00, # 绿色框
label_color=0xFFFFFF, # 白色标签
font_size=16,
)
结构化配置
from neoruntime_ipc_sdk import OverlayClient, OverlayConfig
config = OverlayConfig(
enabled=True,
show_label=True,
show_confidence=True,
line_thickness=2,
)
OverlayClient().apply(config)
严格对帧模式(P1-6)
# 默认是"实时预览"语义:烘焙用的是有效期内的最新结果,框与画面不
# 保证同帧。严格对帧是 opt-in:configure 打开后,daemon 在烘焙点
# (编码前)为本帧等待它自己的推理结果——等待有上限,超限降级为
# 最新结果并打日志/计数,编码输出永不因等待而停住
overlay.configure(strict_frame_lock=True) # 打开(cap 按 fps 推导)
overlay.configure(strict_frame_lock=True,
strict_wait_cap_ms=0) # 0 = 按流 fps 推导(2 帧周期)
overlay.configure(strict_frame_lock=True,
strict_wait_cap_ms=66) # 显式 66ms 上限
overlay.configure(strict_frame_lock=False) # 显式退出严格模式
# 两个参数都是"可选字段"语义:不传 = 保持 daemon 当前设置;
# enable()/disable() 与不带参数的 configure() 不会改动严格模式
# 严格模式只作用于"自喂"显示流——stream_map 里 display 流的推理结果
# 来自它自己(如 main:main)。交叉喂流的流(如 third:main)保持实时
# 预览语义
# 严格模式只锁定平台结果层,不排除应用图层(annotate() 的绘制):
# 有锁定层时,app 图层按自身 TTL 语义叠加绘制在锁定层之上;SKIP /
# 降级(本帧没有可锁定的平台结果)时,app 图层单独绘制,不再被
# 严格模式清掉。严格模式永不压制已锁定的平台层(帧同步保证正是
# 该模式的意义),也绝不放行交叉喂流的平台层;只有"既无锁定层、
# 又无在效 app 图层"才干净出帧(计入 bake_skips)
与推理结果联动(annotate)
# annotate() 把检测结果经事件总线推给 camera-daemon 的叠加渲染器:
# app 不接触视频帧,检测框在编码前由 daemon 画到码流上。结果
# 500ms 过期,按推理节奏持续调用;发空列表清屏
from neoruntime_ipc_sdk import OverlayClient
overlay = OverlayClient()
overlay.enable()
for result in inference_results: # 如 InferenceClient.subscribe(...)
overlay.annotate("main", result.objects)
overlay.annotate("main", []) # 清屏
其他结果类型
# annotate_result 按 objects > classifications > landmarks >
# ocr_lines 的优先级取 InferenceResult 中已填充的那一段
overlay.annotate_result("main", result)
生命周期会话标记(P2-13)
# annotate / annotate_result / 多边形写入都接受 session_id:同一会话
# 的绘制带同一标记。推理会话结束(含进程被 SIGKILL 后连接断开)时,
# daemon 按 session/end 事件把带该标记的所有 polygon 一次性清掉;
# 检测框本身随结果 TTL 自然过期
overlay.annotate("main", result.objects, session_id="app-1")
overlay.annotate("main", polygons=zones, session_id="app-1")
# 不传 session_id = 运维写入:无条件生效、不参与会话清理。
# StreamPipeline 内部即按此区分——结果绘制带会话标记,启动时的
# 静态 zone 与 stop() 的清屏写入不带标记
两种模式与视频副作用契约
SDK 有两条互不干扰的取数路径,契约上明确区分:
只取结果(零视频副作用):InferenceClient.subscribe(...) 只
返回推理结果,不触碰视频通路。平台上自己的模型(ai-runtime /
auto-infer 发布的结果)默认也**不会**改变码流——除非运维在 daemon
配置里显式绑定(见下节)。annotate() 主动画框不算副作用违规:
它是应用显式下发的绘制指令。
自取帧自画(keep_fd / copy):FdMediaClient 的 keep_fd / copy
模式取到的是**干净帧**(未经叠加层烘焙)。应用在自己的缓冲上组
合:copy 帧直接改写即可;keep_fd 帧经 DSP_IMPORT 进应用自有的 DSP
池做 RESIZE→BLEND(P1-9 池驻留后每调用开销可接受)。
keep_fd 帧永不作为 DSP 写目标或 blend base——平台侧按只读
契约执法(写入即拒绝),测试 dsp-import-write-target-test 守卫。
平台结果绑定(daemon 配置)
平台模型的检测结果默认不画(行为解耦):一个裸 subscribe() 不
应该改变任何视频输出。需要平台框时,在 daemon 配置 yaml 的
[ai_overlay] 段显式绑定:
[ai_overlay]
# 推理流:显示流,方向与 stream_map 相同;逗号分隔多个绑定
bindings: "main:main,third:main"
# 迁移开关:恢复旧行为(所有平台结果自动画 + 默认 stream_map 回填)
legacy_auto_bind: 0
迁移说明:legacy_auto_bind 默认关闭。升级后若发现平台框
消失,两种恢复方式任选——(1) 按上例把需要的流加进 bindings
(推荐:只开需要的流);(2) 设 legacy_auto_bind: 1 完整恢复
旧行为(所有平台结果自动绑定,含默认 stream_map 回填)。应用经
annotate() 下发的绘制不受此开关影响,永远生效。
帧同步绑定(frame_sequence / stream_epoch)
# annotate / annotate_result 接受两个可选的帧同步参数(都走事件
# metadata,不进 payload;不传 = 不绑定,保持既有语义)
#
# frame_sequence:把这次绘制绑定到该帧——只在烘焙点走到该帧附近
# (+2 帧容差)内绘制,过后过期;帧已被流水线越过的指令会被
# 拒收(overlay_late_commands 计数)。subscribe() 结果里的
# result.frame_sequence 就是天然来源
# 注意:frame_sequence 是 HAL 的共享帧计数——同一 media context
# 下所有流(main/sub/...)共用一个计数器,序号同尺可比;但拿
# 别条流的结果当来源会引入跨流延迟(推理耗时落在计数器刻度
# 里),可能吃掉绑定窗口。推荐用目标显示流自己 subscribe()
# 出的结果做序号来源;跨流来源可用但可见帧数更少。绑定窗口
# 按"显示流帧数"自适应换算(多流共享计数器下每帧跨多步)
# stream_epoch:流代计数(get_stream_status() 可读)。重配置
# (ReconfigureEncoder / 分辨率、变换全量重建)会 bump 代数并
# 清空持有的图层;携带旧代数的事件被拒收(overlay_epoch_rejects
# 计数),不再化作幽灵框
for result in stream.subscribe(model):
overlay.annotate_result("main", result,
frame_sequence=result.frame_sequence,
stream_epoch=epoch)
# 重配置后的自愈:读到 epoch 变了就换用新值继续发布。重配置 /
# 流水线重建时平台会 bump 代数并清空该流的序号锚点——HAL 计数
# 可能从新纪元的小值重新开始,app 侧不要沿用重建前拿到的旧
# frame_sequence,重新 subscribe() 取新值即可
status = {s.stream_id: s for s in camera.get_stream_status()}
epoch = status["main"].stream_epoch
keep_fd 帧的只读配方
# copy 模式:帧已是应用自己的内存,直接改写
frame = media.get_frame(stream, copy=True)
draw_boxes(frame, results) # 随便画
# keep_fd 模式:dma-buf 只读。经 DSP_IMPORT 导入应用自有的 DSP
# 池,RESIZE 到目标尺寸后作为 BLEND 的 overlay 层叠加到应用自己
# 的底图上;帧本体永不作为写目标——RESIZE 的 dst / BLEND 的
# base 若指向 keep_fd 帧都会被平台拒绝(平台侧执法,非约定)
dsp.import_buffer(frame.fds, frame_id=frame.frame_id)
dsp.schedule(op=DSP_OP_RESIZE, src=imported, dst=scratch) # 自有 scratch
dsp.schedule(op=DSP_OP_BLEND, base=composed, overlay=scratch) # 自有底图
# 简言之:keep_fd 帧 = BLEND 的 overlay 输入或 RESIZE 的 src,
# 自有缓冲 = 一切写目标
上下文管理器
with OverlayClient() as overlay:
overlay.enable()