适用版本:
visionpipe.__version__ = 0.1.0(Phase 5 收尾)本文档涵盖 Python 层公开 API。底层 C++/CUDA/TensorRT 细节请参考
DEV_SPEC.md。
visionpipe 包对外暴露的核心符号一览(见 python/visionpipe/__init__.py):
| 分类 | 符号 |
|---|---|
| 枚举 | PipelineState, PipelineStatus, NodeState, OverflowPolicy, DecodeMode |
| 数据 | Frame, Detection, Classification, Track, QueueStats, NodeStats, PipelineStats, PipelineConfig |
| 配置 | SourceConfig, DetectorConfig, ClassifierConfig, SegmentConfig, ByteTrackConfig, AnnotatorConfig, JsonResultSinkConfig, MjpegSinkConfig, WebRTCSinkConfig |
| HAL | IModelEngine, MockModelEngine, TrtModelEngine |
| 节点基类 | NodeBase, SourceNode, SinkNode, ProcessProxyNode, PyNode, CustomNode, FrameView |
| 节点实现 | FileSource, RtspSource, DetectorNode, ClassifierNode, SegmentNode, ByteTrackNode, AnnotatorNode, JsonResultSink, MjpegSink, WebRTCSink |
| 编排 | Pipeline, PipelineBuilder, PipelineManager |
| 序列化 | PipelineSpec, NodeSpec, EdgeSpec, Pipeline.export_yaml, Pipeline.load_yaml, Pipeline.from_yaml, Pipeline.rebuild_from_spec |
| 异常 | VisionPipeError, ConfigError, NotFoundError, CudaError, ModelLoadError, InferError, StreamError |
每帧在 DAG 中流动的载体,所有节点直接读写其字段。Frame 是 move-only(不可在 Python 层重复持有引用)。
| 字段 | 类型 | 说明 |
|---|---|---|
stream_id |
str |
来源标识(多 Source 合并时区分流) |
frame_id |
int |
Source 端单调递增的帧编号 |
pts_us |
int |
帧时间戳(微秒) |
image |
Tensor(C++ 持有) |
GPU 或 CPU 上的图像;通过 frame.image_numpy() 在 CPU 形态时读取为 numpy |
detections |
list[Detection] |
DetectorNode 写入 |
classifications |
list[Classification] |
ClassifierNode 写入(与 detections 通过 detection_index 关联) |
tracks |
list[Track] |
ByteTrackNode 写入 |
user_data |
dict[str, Any] |
各 PyNode/CustomNode 用独立 key 存放业务数据,互不覆盖 |
辅助方法(C++ 暴露给 Python):
frame.has_image() -> boolframe.image_numpy() -> np.ndarray(仅当 image 已在 CPU)frame.has_user_data(key) -> boolframe.get_user_data(key) -> Anyframe.set_user_data(key, value) -> None
| 字段 | 类型 | 说明 |
|---|---|---|
bbox |
list[float] |
[x1, y1, x2, y2](像素坐标) |
class_id |
int |
模型类别索引 |
confidence |
float |
置信度 ∈ [0, 1] |
track_id |
int |
由 Tracker 回填;DetectorNode 输出时为 0/未定义 |
| 字段 | 类型 | 说明 |
|---|---|---|
detection_index |
int |
关联的 frame.detections[i] 下标;整图分类时为 -1 |
class_id |
int |
分类结果类别 |
confidence |
float |
概率 |
| 字段 | 类型 | 说明 |
|---|---|---|
track_id |
int |
跨帧持续的轨迹 ID(> 0) |
class_id |
int |
沿用关联 detection 的类别 |
bbox |
list[float] |
与 detection 同坐标系 |
age |
int |
该轨迹被持续匹配的帧数 |
confidence |
float |
关联 detection 的置信度 |
| 名称 | 关键字段 |
|---|---|
QueueStats |
capacity, size, pushed, popped, dropped, overflow_policy |
NodeStats |
name, state, frames_processed, errors, fps, latency_ms |
PipelineStats |
name, state, node_stats: list[NodeStats] |
| 枚举 | 取值 |
|---|---|
PipelineState / PipelineStatus |
INIT, RUNNING, DRAINING, STOPPED, ERROR |
NodeState |
INIT, RUNNING, STOPPED, ERROR |
OverflowPolicy |
BLOCK, DROP_OLDEST, DROP_NEWEST |
DecodeMode |
AUTO, GPU, CPU |
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
uri |
str |
— | 文件路径或 RTSP URL |
decode_mode |
DecodeMode |
AUTO |
解码后端 |
gpu_device |
int |
0 |
CUDA 设备号 |
queue_capacity |
int |
8 |
下游 input_queue 容量 |
overflow_policy |
OverflowPolicy |
DROP_OLDEST |
队列溢出策略 |
loop |
bool |
False |
文件源播放结束后循环 |
skip_frames |
int |
0 |
解码后每 N 帧丢弃 N-1 帧(降采样) |
max_retries |
int |
0 |
RTSP 断流重连次数 |
retry_interval_ms |
int |
1000 |
重连间隔(毫秒) |
stream_id |
str |
"" |
合并拓扑下区分来源 |
本地视频源,使用 cv::cudacodec(GPU 路径)或 cv::VideoCapture(CPU 路径)。DecodeMode.AUTO 时自动选择,GPU 强制硬解(不可用时抛 CudaError)。
RTSP 流源,CPU 路径解码后通过 GpuMat::upload() 提升到 GPU。支持断流自动重连。
所有 Infer 节点继承自 C++ 的 InferNode,内部使用 parallel_workers 并行执行,输出按 frame_id 重排序。
| 字段 | 默认 | 说明 |
|---|---|---|
input_height |
640 |
模型输入高 |
input_width |
640 |
模型输入宽 |
score_threshold |
0.25 |
分数阈值 |
nms_threshold |
0.45 |
NMS IoU 阈值 |
max_detections |
300 |
单帧最大检测目标数 |
workers |
1 |
并行 worker 数(每 worker 独立 IExecContext) |
DetectorNode(engine: IModelEngine, config: DetectorConfig, name: str = "detector")
运行时热更新:
detector.set_param("score_threshold", 0.5)
detector.set_param("roi", [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]])
detector.set_param("clear_roi", True)| 字段 | 默认 | 说明 |
|---|---|---|
input_height / input_width |
224 |
模型输入尺寸 |
target_classes |
[] |
非空=二级分类(按 detections 裁切);空=整图分类(detection_index = -1) |
max_batch_size |
16 |
单次推理最大 batch(动态攒帧) |
normalize_mean_std |
True |
ImageNet 均值方差归一化 |
workers |
1 |
并行 worker 数 |
ClassifierNode(engine: IModelEngine, config: ClassifierConfig, name: str = "classifier")
| 字段 | 默认 | 说明 |
|---|---|---|
input_height / input_width |
640 |
模型输入尺寸 |
score_threshold |
0.25 |
分数阈值 |
nms_threshold |
0.45 |
NMS IoU |
mask_threshold |
0.5 |
掩码二值化阈值 |
max_detections |
100 |
最大目标数 |
workers |
1 |
并行 worker 数 |
SegmentNode(engine: IModelEngine, config: SegmentConfig, name: str = "segment")
输出同时填充 frame.detections(bbox/类别)和实例掩码(C++ 内部的 Frame::masks)。
| 字段 | 默认 | 说明 |
|---|---|---|
track_thresh |
0.5 |
高分检测阈值 |
match_thresh |
0.8 |
关联 IoU 阈值 |
track_buffer |
30 |
丢失轨迹保留帧数 |
frame_rate |
30 |
视频帧率 |
ByteTrackNode(config: ByteTrackConfig, name: str = "bytetrack")
纯 CPU 实现,依赖上游 frame.detections,回填 frame.tracks 与 Detection.track_id。
| 字段 | 默认 | 说明 |
|---|---|---|
draw_detections |
True |
绘制 bbox + 类别名 + 分数 |
draw_tracks |
True |
绘制 track_id |
draw_masks |
False |
叠加实例分割掩码 |
mask_alpha |
0.5 |
掩码透明度 |
class_names |
{} |
dict[int, str],未指定时用 class_<id> 占位 |
AnnotatorNode(config: AnnotatorConfig, name: str = "annotator")
在 frame.image(CPU 形态)上原地绘制。GPU 形态图像会跳过绘制(不报错)。
所有 Sink 继承自 SinkNode,统一暴露 enabled 属性。运行时通过 sink.set_param("enabled", True/False) 切换;enabled=False 时跳过实际工作,不消耗 CPU/GPU 资源。
| 字段 | 默认 | 说明 |
|---|---|---|
include_detections |
True |
JSON 含 detections 字段 |
include_tracks |
True |
JSON 含 tracks 字段 |
buffer_capacity |
1024 |
内部 BoundedQueue 容量 |
sink = JsonResultSink(JsonResultSinkConfig(), "sink")
# 阻塞最多 200ms,无数据时返回 None
payload: str | None = sink.pop_json(timeout_ms=200)enabled 默认 True。
| 字段 | 默认 | 说明 |
|---|---|---|
jpeg_quality |
80 |
JPEG 编码质量(0-100) |
buffer_capacity |
4 |
最新帧环形队列容量 |
sink = MjpegSink(MjpegSinkConfig(), "mjpeg")
jpeg_bytes: bytes | None = sink.pop_jpeg(timeout_ms=200)enabled 默认 False(按规范作为调试/降级 Sink),需通过 sink.set_param("enabled", True) 或 REST POST /pipelines/{id}/params 显式开启。
| 字段 | 默认 | 说明 |
|---|---|---|
use_nvenc |
True |
启用 NVENC 硬编 H.264 |
video_bitrate_kbps |
2000 |
目标码率 |
fps |
30 |
编码帧率 |
keyframe_interval |
60 |
关键帧间隔(帧) |
stun_server |
"stun:stun.l.google.com:19302" |
STUN 服务器 |
需在 cmake 时启用 -DVISIONPIPE_USE_WEBRTC=ON。Python 侧 SDP/ICE 信令通过 ManagementServer 的 /ws/{id}/webrtc 端点。
from visionpipe import PyNode
class CountNode(PyNode):
def __init__(self) -> None:
super().__init__(name="count")
def process(self, frame) -> None:
frame.set_user_data("n_dets", len(frame.detections))- C++ 调用时自动
gil_scoped_acquire - 通过
node._cpp_node在Pipeline.add_node()和connect()中使用 - 支持
>>链式语法(PyNode.__rshift__自动 unwrap 到_cpp_node)
from visionpipe import CustomNode, FrameView
class HeavyAnalysisNode(CustomNode):
def on_frame(self, frame: FrameView) -> None:
# 跑独立进程,无 GIL 限制;可调用任意 Python 库
frame.user_data["analysis"] = my_heavy_compute(frame.detections)
# 用法
node = HeavyAnalysisNode(name="analysis", process_mode="subprocess")
pipeline.add_node(node._cpp_node)参数:
| 参数 | 默认 | 说明 |
|---|---|---|
name |
"custom_node" |
节点名 |
process_mode |
"subprocess" |
"subprocess" 或 "inline" |
restart_limit |
3 |
子进程崩溃自动重启次数 |
restart_delay |
1.0 |
重启延迟(秒) |
FrameView 是子进程端的安全视图:
- 只读字段:
frame_id,stream_id,pts_us,detections,tracks - 可写字段:
user_data: dict(赋值后由 IPC 同步回主进程) on_frame返回后视图自动失效,禁止在外部线程持有
退出时务必调用 node.stop() 释放子进程。
| 字段 | 默认 | 说明 |
|---|---|---|
name |
"" |
Pipeline 名(用于日志/管理 API) |
default_queue_capacity |
8 |
自动建队列时使用的容量 |
default_overflow_policy |
DROP_OLDEST |
自动建队列的溢出策略 |
pipeline = Pipeline(PipelineConfig())
pipeline.add_node(node) # 注册节点(自动建立 input_queue / output_queue)
pipeline.connect(upstream, downstream)
# 多 Source 合并:多次 connect(src_i, downstream) 共享 downstream.input_queue
pipeline.validate_dag() # 检测环路;抛 ConfigError
pipeline.start() # 启动所有节点 worker
pipeline.run(block=False) # = start();block=True 则等待 source 自然结束
pipeline.wait_stop() # 阻塞至 STOPPED
pipeline.stop(drain=True) # DRAINING → STOPPED
pipeline.stats() -> PipelineStats
pipeline.state() -> PipelineState
pipeline.nodes() -> dict[str, NodeBase]manager = PipelineManager()
pid: str = manager.create_pipeline(pipeline) # 接管 pipeline 的生命周期
manager.start(pid)
status = manager.status(pid) # PipelineStatus 枚举
manager.stop(pid)
manager.destroy(pid) # 必须先 STOPPED
manager.list() -> list[str]
manager.get(pid) -> PipelinePipelineManager 内置 ModelRegistry:多条 pipeline 通过同一 IModelEngine 实例参与时自动复用模型权重,节省显存。
>> 运算符在 python/visionpipe/__init__.py 中以 monkey-patch 实现:
# 线性链
pipe = source >> detector >> sink
# 链式追加(Pipeline 端)
pipe = source >> detector
pipe = pipe >> tracker >> sink
# 合并拓扑(多 source → 一个下游)
pipe = [src1, src2, src3] >> detector >> sink
# PyNode / CustomNode 自动 unwrap _cpp_node
pipe = source >> detector >> MyPyNode("count") >> sink>> 直接返回 Pipeline(不需要 .build())。Pipeline 内部维护 _tail 指针支持继续链式追加。
启动入口(同义):
pipe.run(block=False) # 非阻塞,立即返回
pipe.run(block=True) # 阻塞至 source 自然结束(适合文件视频)
pipe.start(); pipe.wait_stop()
pipe.stop(drain=True)# 导出当前 pipeline 拓扑 + 节点参数到 YAML
pipe.export_yaml("pipeline.yaml")
# 仅解析 YAML 得到 PipelineSpec(不构建 Pipeline,不需要 GPU)
spec: PipelineSpec = Pipeline.load_yaml("pipeline.yaml")
for n in spec.nodes:
print(n.name, n.type, n.params)
for e in spec.edges:
print(e.from_node, "->", e.to_node)
# 完整重建:node_overrides 用于注入需要外部依赖的节点(如 DetectorNode 需要 engine)
rebuilt = Pipeline.from_yaml(
"pipeline.yaml",
node_overrides={
"src": FileSource(SourceConfig("...")),
"det": DetectorNode(engine, DetectorConfig(), "det"),
"sink": JsonResultSink(JsonResultSinkConfig(), "sink"),
},
)NodeSpec 字段:
| 字段 | 说明 |
|---|---|
name |
节点名 |
type |
内置类型字符串(file_source / detector / json_result_sink 等) |
params |
节点配置参数 dict |
module |
CustomNode 专用,待自动 import 的 Python 模块 |
class_name |
CustomNode 专用,类名 |
process_mode |
CustomNode 专用,subprocess / inline |
YAML 解析器(pydantic)会拦截非法节点类型并抛 ConfigError。
from visionpipe.server.management_api import ManagementServer
manager = visionpipe.PipelineManager()
server = ManagementServer(manager, host="0.0.0.0", port=8080)
await server.start()
# ... 业务 ...
await server.stop()| 方法 | 路径 | 状态码 | 说明 |
|---|---|---|---|
POST |
/pipelines |
201 | body {"spec": <PipelineSpec dict or YAML str>},返回 {"id": "..."} |
GET |
/pipelines |
200 | [{id, name, state}, ...] |
POST |
/pipelines/{id}/start |
200 | 转 INIT/STOPPED → RUNNING |
POST |
/pipelines/{id}/stop |
200 | RUNNING → DRAINING → STOPPED |
DELETE |
/pipelines/{id} |
204 | 必须先 STOPPED,否则 409 |
GET |
/pipelines/{id}/health |
200 | {nodes: [{name, queue: QueueStats, fps}, ...]} |
GET |
/pipelines/{id}/nodes |
200 | [NodeStats, ...](含 fps/latency_ms/state/frames_processed/errors) |
POST |
/pipelines/{id}/params |
200 | body {node_id, param_name, value} → 转发到 NodeBase.set_param() |
GET |
/mjpeg/{id} |
200 | multipart MJPEG 流(依赖 MjpegSink 且 enabled=True) |
| 路径 | 协议 | 说明 |
|---|---|---|
/ws/{id}/results |
文本 JSON | 推送 JsonResultSink 每帧结果 |
/ws/{id}/control |
文本 JSON | 通用控制通道(见下) |
/ws/{id}/webrtc |
文本 JSON | SDP/ICE 信令(仅在 WebRTCSink 启用时可用) |
请求:
{"type": "ping"}
{"type": "set_param", "node_id": "det", "param_name": "score_threshold", "value": 0.5}
{"type": "roi", "node_id": "det", "polygons": [[0.1,0.1],[0.9,0.1],[0.9,0.9],[0.1,0.9]], "coord": "normalized"}响应:
{"type": "pong"}
{"type": "ack", "ref_type": "set_param"}
{"type": "error", "message": "node not found: no_such_node"}set_param 内部直接调用对应节点的 set_param(param_name, value),参数类型由节点自己解释(数字、字符串、列表等)。
VisionPipeError (RuntimeError 子类)
├── ConfigError 配置或参数错误
├── NotFoundError Pipeline/Node/模型不存在
├── CudaError CUDA / NVDEC 等 GPU 运行时错误
├── ModelLoadError 模型加载失败 (file, reason)
├── InferError 推理执行错误
└── StreamError 视频源连接 / 断流错误
所有异常通过 nanobind 自动穿透到 Python;可用 try/except visionpipe.ConfigError: 精确捕获。
import json, time, visionpipe as vp
src = vp.FileSource(vp.SourceConfig("video.mp4"))
engine = vp.TrtModelEngine("models/yolov8n_dynamic.engine")
det = vp.DetectorNode(engine, vp.DetectorConfig(), "det")
sink = vp.JsonResultSink(vp.JsonResultSinkConfig(), "sink")
pipe = src >> det >> sink
pipe.run(block=False)
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
payload = sink.pop_json(200)
if payload:
print(json.loads(payload)["frame_id"])
pipe.stop()参见 examples/multi_pipeline_demo.py —— 两条 Pipeline 共享同一 TrtModelEngine,分别 filter 出 vehicle / person 类别,验证 ModelRegistry 复用 + 生命周期隔离。
import asyncio, aiohttp, visionpipe as vp
from visionpipe.server.management_api import ManagementServer
async def main() -> None:
mgr = vp.PipelineManager()
server = ManagementServer(mgr, host="127.0.0.1", port=8080)
await server.start()
async with aiohttp.ClientSession() as sess:
await sess.post("http://127.0.0.1:8080/pipelines", json={"spec": {...}})
# ... start / monitor / set_param via /ws/{id}/control ...
await server.stop()
asyncio.run(main())完整的三层 E2E 验证脚本见 tests/e2e/test_e2e_validation.py,可直接作为更复杂用法的范本。
OverflowPolicy选择:实时流默认DROP_OLDEST保低延迟;离线文件 +InferNode(结果按 frame_id 重排)必须用BLOCK,否则 frame_id 空洞会卡死下游重排序队列。- GPU 解码失败:检查 OpenCV 是否带
WITH_CUDA=ON WITH_NVCUVID=ON编译;DecodeMode.AUTO会自动回退 CPU 路径。 - CustomNode 子进程没退出:脚本结束前确保调用
node.stop();否则 IPC socket 不释放可能残留僵尸进程。 PipelineManager.destroy报 409:先stop()转到 STOPPED 状态再 destroy;REST 接口同样要求。Pipeline.run(block=True)一直不返回:检查 source 是否设置了loop=True或是 RTSP 这类无尽源;非阻塞模式请用run(block=False)+ 自己控制停止时机。