OrangePi3588Media/Readme.md

1269 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RK3588 智能视频分析系统 PRDv1.2
---
## 一、文档综述
### 1.1 项目背景
为了充分利用瑞芯微 RK3588 芯片提供的:
- **NPU约 6 TOPS**RKNNINT8 推理能力)
- **VPU多路 1080p 硬件解码/编码**
- **RGA硬件图像处理缩放/颜色转换/OSD 等)**
构建一套**纯 C++ 实现、高性能、低延迟、模块化**的边缘计算视频分析平台。平台主要部署在园区、仓库、工厂等场景,实现就地智能分析与处理,减少带宽与中心算力压力。
### 1.2 核心目标
- **高性能**
- 单 RK3588 支持 816 路 1080p 实时分析
- 单路端到端(拉流→推理→输出)延迟 < 500ms
- **低耦合**
- 基于插件 + DAG 流水线架构
- 算法业务逻辑与底层框架完全解耦
- **易运维**
- JSON 配置驱动
- 支持热更新无需重启进程即可变更流程/参数
- **多场景覆盖**
- 同一套框架通过不同配置图覆盖
- 安防报警
- 录像存储
- 转码网关RTSP/HLS
- 混合场景报警 + 存储 + 推流
---
## 二、总体架构设计
### 2.1 系统逻辑架构
```text
[ media-server 主进程 ]
|
+-- Graph Manager (图管理加载配置、构建DAG、热更新/回滚)
|
+-- Plugin Loader (插件加载dlopen .so检查 ABI 版本)
|
+-- AiScheduler (统一调度 NPU 推理请求)
|
+-- Metrics & Logger (监控指标、日志)
|
+-- HTTP Server (REST API + Web 控制台 + 状态查询)
```
- **media-server**主入口负责初始化配置图构建主事件循环
- **Graph ManagerGraphMgr**
- 对应多个 graph每个 graph 一路/一组业务流程)。
- 实例化节点Node建立边Edge负责热更新时的 diff & 切换
- **Plugin Loader**
- 动态加载插件 `.so`校验节点类型和 ABI 版本
- **AiScheduler**
- 所有 AI 节点统一将推理任务提交到 AiScheduler由其管理 NPU 资源
- **HTTP Server**
- 对外提供图状态查询节点指标配置热更新触发等
### 2.2 数据流架构DAG
- **Graph**一个完整业务通道例如Camera 1 拉流AI报警+录像+推流”。
- **Node节点**功能基本单元由动态加载的 `.so` 实例实现
- **Edge**节点间的数据通道每条边对应一个 **SPSC单生产者单消费者环形队列**
- **多分支支持**一个节点可以有多个下游多条边实现一对多分支
示意
```text
[in] → [pre] → [ai_yolo] → [det_post] ───→ [alarm]
└──────→ [osd] → [storage]
└──→ [publish]
```
> 说明:`det_post` 是检测结果后处理节点(可选)。它会对 `frame->det` 做过滤/修正/二次验证,使 OSD 与告警读取到一致的“后处理结果”。
### 2.3 多硬件解耦架构(已完成)
为支持 RK3588 之外的平台Atlas/Jetson 已将硬件强绑定模块抽象为统一接口层并保留 RK3588 默认实现与 DMA-BUF 快路径
- **推理**`IInferBackend`默认 `Rk3588InferBackend`
- **图像处理**`IImageProcessor`默认 `Rk3588ImageProcessor`RGA/Swscale 兜底
- **编解码**`IDecoder` / `IEncoder`默认 RK3588 MPP 路径
- **缓冲区**`FrameBuffer` 统一 DMA/内存同步语义
运行时通过 `HwFactory` 根据配置选择后端实现
- 配置字段`platform` / `hw_platform`不填默认 `rk3588`
- 可扩展实现`Atlas*` / `Jetson*`保持现有业务配置不变仅切换硬件实现
---
## 三、数据模型Frame & 元数据)
### 3.1 检测结果结构Detection & DetectionResult
> 约定:**所有 AI 节点写入此结构**OSD / 报警 等节点只读。
```cpp
struct Rect {
float x; // 左上角X像素或归一化坐标
float y; // 左上角Y
float w; // 宽
float h; // 高
};
struct Detection {
int cls_id; // 类别ID
float score; // 置信度 [0,1]
Rect bbox; // 检测框
int track_id; // 跟踪ID可选无则 -1 或 0
};
struct DetectionResult {
std::vector<Detection> items;
int img_w; // 推理输入宽
int img_h; // 推理输入高
std::string model_name; // 模型名/版本
};
```
**约定:**
- `bbox` 坐标标准在 PRD 中约定好建议像素坐标避免后续插件互相搞混
- class 映射表由 AI 节点自行维护OSD/报警通过 `cls_id` 做规则匹配
### 3.2 Frame 结构
```cpp
enum class PixelFormat {
NV12,
YUV420,
RGB,
BGR,
UNKNOWN
};
struct Frame {
int width;
int height;
PixelFormat format;
int dma_fd; // DMA-BUF FD无则为 -1
uint8_t* data; // CPU 内存指针可选有则指向内存buffer
uint64_t pts; // 时间戳(微秒)
uint64_t frame_id; // 单调递增ID
std::shared_ptr<DetectionResult> det; // AI 检测结果(通用目标检测)
std::shared_ptr<FaceDetResult> face_det; // 人脸检测结果
std::shared_ptr<FaceRecogResult> face_recog;// 人脸识别结果
std::shared_ptr<void> user_meta; // 扩展元数据(具体类型由插件间约定)
};
```
**约定:**
- **零拷贝优先**解码/前处理尽量通过 `dma_fd` 传递必要时才用 data
- 节点默认只读图像数据**可以更新 `det` / `face_det` / `face_recog` / `user_meta`**。
- 所有队列中传递的都是 `std::shared_ptr<Frame>`实现多下游共享
### 3.3 人脸检测结果结构FaceDetResult
```cpp
struct Point2f {
float x;
float y;
};
struct FaceDetItem {
Rect bbox; // 人脸框
float score; // 检测置信度
int track_id; // 跟踪ID可选-1表示无
bool has_landmarks; // 是否有关键点
std::array<Point2f, 5> landmarks; // 5点关键点左眼/右眼/鼻尖/左嘴角/右嘴角
};
struct FaceDetResult {
std::vector<FaceDetItem> faces;
int img_w; // 原始图像宽
int img_h; // 原始图像高
std::string model_name; // 模型名(如 "retinaface"
};
```
### 3.4 人脸识别结果结构FaceRecogResult
```cpp
struct FaceRecogItem {
Rect bbox; // 人脸框(与检测结果对应)
int best_person_id; // 匹配的人员ID-1表示陌生人
std::string best_name; // 匹配的人员姓名("unknown"表示陌生人)
float best_sim; // 最高相似度
float second_sim; // 次高相似度用于margin判定
bool unknown; // 是否为陌生人
std::vector<float> embedding; // 特征向量(可选,调试/注册用)
};
struct FaceRecogResult {
std::vector<FaceRecogItem> items;
int img_w;
int img_h;
std::string model_name; // 模型名(如 "arcface"
};
```
---
## 四、插件模型与生命周期
### 4.1 节点通用字段Node 通用配置)
每个节点配置中推荐包含以下通用字段
```json
{
"id": "唯一节点ID",
"type": "节点类型,如 input_rtsp / ai_yolo",
"enable": true,
"role": "source|filter|sink", // 可选,但建议写上,便于校验
"queue": { // 可选:覆盖边上的默认队列策略
"size": 128,
"policy": "drop_oldest" // drop_oldest / drop_newest / block
},
"debug": { // 可选:调试/日志开关(生产默认建议关闭)
"stats": false, // 各节点的周期性统计日志processed/queue/drops 等)
"stats_interval": 100, // 统计日志间隔(帧数)
"detections": false, // 部分 AI 节点的前几帧检测框调试输出
"ffmpeg_log_level": "error" // FFmpeg 日志级别(影响 HLS/RTSP mux 的内部日志;默认 error
},
"...特定节点参数..."
}
```
- `enable = false`GraphMgr 构建图时可直接忽略该节点及相关 edges
- `role` 主要用于配置校验source 不应有上游sink 不应有下游等)。
**日志/性能建议:**
- 生产环境建议`global.log_level=warn`,并保持各节点 `debug.stats=false`避免高频 stdout/stderr 造成 CPU 抖动
- 需要定位丢帧/队列积压时再对目标节点打开 `debug.stats=true`并把 `stats_interval` 调大 500/1000)。
### 4.2 INode 接口(含 Drain
```cpp
// NodeStatus 用于描述处理结果
enum class NodeStatus {
OK, // 正常处理
DROP, // 丢弃该帧(例如不满足条件)
ERROR // 发生错误,需要记录日志
};
class INode {
public:
virtual ~INode() = default;
// 1. 初始化:解析 JSON 配置,申请静态资源
virtual bool Init(const Json::Value& config) = 0;
// 2. 启动:启动内部线程(如有),准备接收/发送数据
virtual bool Start() = 0;
// 3. 处理数据GraphMgr 从队列中取到 Frame 后调用
// - 对 filter/sink 节点:正常消费上游 Frame
// - 对 source 节点:通常可忽略(见下文约定)
virtual NodeStatus Process(std::shared_ptr<Frame> frame) = 0;
// 4. 动态配置更新支持热更参数如改变报警阈值、bitrate
// 返回 true 表示就地更新成功,无需重建节点
virtual bool UpdateConfig(const Json::Value& new_config) { return false; }
// 5. Drain停止前**不再有新输入**,但需处理完内部队列/缓冲数据
// 如storage 完成当前切片写入publish 发送剩余帧等
virtual void Drain() {}
// 6. 停止释放资源线程、句柄、内存等Drain 之后调用
virtual void Stop() = 0;
};
```
### 4.3 插件导出与 ABI 版本
```cpp
#define ABI_VERSION 20250101
#define REGISTER_NODE(TYPE_NAME, CLASS_NAME) \
extern "C" { \
INode* CreateNode() { \
return new CLASS_NAME(); \
} \
const char* GetNodeType() { \
return TYPE_NAME; \
} \
int GetAbiVersion() { \
return ABI_VERSION; \
} \
const char* GetNodeDescription() { \
return "node: " TYPE_NAME; \
} \
}
```
GraphMgr 在加载插件 `.so`
- 调用 `GetAbiVersion()` 校验兼容性
- 通过 `GetNodeType()` 找到对应 `type` 的构造函数
### 4.4 Source 节点约定
为避免把所有采集逻辑都硬塞进 `Process()`PRD 中统一约定
- `role == "source"` 的节点 `input_rtsp` / `input_file`
- `Start()` 中启动**内部采集线程**。
- 线程中从外部拉流/读文件构造 `Frame`通过框架提供的 `FrameEmitter` 写入下游队列
- `Process()` 可以是空实现或仅做心跳/监控
- `role != "source"` 的节点
- GraphMgr 驱动 `Process(frame)`消费上游队列元素
GraphMgr 负责
- 对非 source 节点循环从对应队列取 `Frame` 调用 `Process()`
- 对所有节点在热更新 / 停止时控制 `Drain()` `Stop()` 的调用顺序
---
## 五、配置系统Config System
整个系统由 `config.json` 驱动主要包含三层
- `global`全局配置
- `templates`pipeline 模板复用节点与拓扑
- `instances` / `graphs`具体业务实例通道
### 5.1 顶层结构示例
```json
{
"global": {
"plugin_path": "/usr/lib/rknodes",
"log_level": "info",
"metrics_port": 9000
},
"templates": { ... },
"instances": [ ... ],
"graphs": [ ... ] // 可选直接手写graph
}
```
> 说明:`global.log_level` 只控制框架内部 Loggerdebug/info/warn/error
> 若启用了 FFmpeg例如 publish 的 HLS mux建议通过节点 `debug.ffmpeg_log_level` 将 FFmpeg 内部日志降到 `error`
> 避免出现大量类似 `hls ... Opening ...` 的 info 日志刷屏。
- **优先方式**使用 `templates + instances` 快速生成多路通道
- **高级用法**直接使用 `graphs` 写复杂拓扑例如一条流拆成多个完全不同的分支)。
### 5.2 DAG 多分支完整示例(带报警 + 存储 + 推流)
```json
{
"graphs": [
{
"name": "cam1_pipeline",
"nodes": [
{
"id": "in_cam1",
"type": "input_rtsp",
"role": "source",
"enable": true,
"url": "rtsp://user:pwd@ip/stream1"
},
{
"id": "pre_cam1",
"type": "preprocess",
"role": "filter",
"enable": true,
"dst_w": 768,
"dst_h": 768,
"dst_format": "rgb",
"resize_mode": "letterbox"
},
{
"id": "ai_cam1",
"type": "ai_yolo",
"role": "filter",
"enable": true,
"model_path": "./models/ppe_det_yolov8_ppe11_768_rk3588.rknn",
"model_version": "v8",
"model_w": 768,
"model_h": 768,
"num_classes": 11,
"conf": 0.35,
"nms": 0.45,
"class_filter": [3, 6]
},
{
"id": "pp_cam1",
"type": "det_post",
"role": "filter",
"enable": true,
"default": { "enable": false },
"per_class": [
{
"class_id": 0,
"enable": true,
"processors": [
{ "type": "conf_gate", "conf_min": 0.65 },
{ "type": "bbox_gate", "min_w": 10, "min_h": 10 }
]
}
]
},
{
"id": "osd_cam1",
"type": "osd",
"role": "filter",
"enable": true,
"draw_bbox": true,
"draw_text": true
},
{
"id": "alarm_cam1",
"type": "alarm",
"role": "sink",
"enable": true,
"rules": [
{
"name": "intrusion_day",
"object": ["person"],
"roi": "roi1",
"min_duration_ms": 500,
"schedule": "08:00-20:00"
}
]
},
{
"id": "storage_cam1",
"type": "storage",
"role": "sink",
"enable": true,
"mode": "continuous",
"segment_sec": 300,
"path": "/rec/cam1"
},
{
"id": "publish_cam1",
"type": "publish",
"role": "sink",
"enable": true,
"codec": {
"video": "h264",
"bitrate_kbps": 2048,
"gop": 50
},
"outputs": [
{ "proto": "rtsp", "port": 8554, "path": "/live/cam1" },
{ "proto": "hls", "path": "/data/hls/cam1/index.m3u8", "segment_sec": 2 }
]
}
],
"edges": [
["in_cam1", "pre_cam1"],
["pre_cam1", "ai_cam1"],
["ai_cam1", "pp_cam1"],
["pp_cam1", "alarm_cam1"],
["pp_cam1", "osd_cam1"],
["osd_cam1", "storage_cam1"],
["osd_cam1", "publish_cam1"]
]
}
]
}
```
> 说明:如果不需要后处理:
>
> - **推荐**:保留 `pp_cam1` 节点(`enable:true`),并确保其不启用任何处理链(例如:`default.enable=false` 且 `per_class` 为空),此时 `det_post` 会作为轻量透传。
> - 若确实要移除该节点:需要**同时删除并重连拓扑**(例如改回 `ai_cam1 -> osd_cam1` 与 `ai_cam1 -> alarm_cam1`),仅设置 `enable=false` 会导致下游节点无输入而构图失败。
通过简单改动即可支持不同组合
- 不报警`alarm_cam1.enable = false` 或移除该节点/
- 只转码网关直接连 `["in_cam1", "publish_cam1"]`并禁用 AI/OSD/报警/storage
- 只录像`["in_cam1", "storage_cam1"]`。
### 5.3 模板templates与实例instances
#### 5.3.1 模板示例:标准安防流程
```json
"templates": {
"standard_security": {
"nodes": [
{ "id": "in", "type": "input_rtsp", "role": "source", "url": "${url}" },
{ "id": "pre", "type": "preprocess", "role": "filter", "dst_w": 768, "dst_h": 768, "resize_mode": "letterbox" },
{ "id": "ai", "type": "ai_yolo", "role": "filter", "model_path": "./models/ppe_det_yolov8_ppe11_768_rk3588.rknn", "model_version": "v8", "num_classes": 11, "conf": 0.35, "class_filter": [3, 6] },
{ "id": "pp", "type": "det_post", "role": "filter", "enable": true, "default": { "enable": false } },
{ "id": "osd", "type": "osd", "role": "filter" },
{ "id": "pub", "type": "publish", "role": "sink", "path": "/live/${name}" }
],
"edges": [
["in", "pre"],
["pre", "ai"],
["ai", "pp"],
["pp", "osd"],
["osd", "pub"]
]
}
}
```
#### 5.3.2 实例与 override禁用某些节点
```json
"instances": [
{
"name": "cam_01",
"template": "standard_security",
"params": {
"url": "rtsp://192.168.1.101/stream1",
"name": "office"
},
"override": {
"nodes": {
"ai": { "enable": true },
"osd": { "enable": true }
}
}
},
{
"name": "cam_02_only_transcode",
"template": "standard_security",
"params": {
"url": "rtsp://192.168.1.102/stream1",
"name": "transcoder"
},
"override": {
"nodes": {
"ai": { "enable": false },
"osd": { "enable": false }
}
}
}
]
```
> GraphMgr 在加载时将模板 + 实例 merge 成具体的 graph节点 ID 会按实例名做前缀防冲突)。
### 5.4 热更新与回滚(简要)
- 通过 inotify 监控 `config.json`
- 检测到变更时
1. 读取新配置 JSON Schema 校验
2. 构建新的图对象dry run包括插件加载与节点实例化
3. 与旧图做 diff
- `UpdateConfig` 的节点优先调用 `UpdateConfig`
- 需要重建的节点先创建新节点并预热如加载模型然后做 **边的原子切换**
4. 对旧节点依次调用 `Drain()` `Stop()`
5. 若任意步骤失败回滚到旧配置并输出错误日志
- 保留上一份有效配置支持手动回滚
---
## 六、功能模块定义(插件类型)
> 下表是“典型插件类型”,后续可以扩展更多。
### 6.1 输入与前处理
| 节点类型 | 功能描述 | 关键参数 | 硬件依赖 |
| :------------- | :------------------------------- | :------------------------- | :--------------- |
| `input_rtsp` | 拉取 RTSP 断线重连 | `url`, `reconnect_sec` | MPP VDEC / 网络 |
| `input_file` | 读取本地 MP4/MKV 文件循环播放 | `path`, `loop` | MPP VDEC |
| `preprocess` | 缩放裁剪NV12RGB/BGR | `dst_w`, `dst_h`, `dst_format`, `resize_mode` | RGA / GPU |
#### Preprocess 节点参数说明
**关键参数:**
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `dst_w` | int | 输出宽度 | 640 |
| `dst_h` | int | 输出高度 | 640 |
| `dst_format` | string | 输出格式: `rgb`, `bgr`, `nv12`, `yuv420` | 保持原格式 |
| `resize_mode` | string | 缩放模式: `stretch`, `keep_ratio`, `letterbox` | `stretch` |
| `use_rga` | bool | 是否使用 RGA 硬件加速 | true |
**resize_mode 说明:**
- `stretch`: 拉伸填充可能改变目标宽高比
- `keep_ratio`: 保持比例缩放可能无法填满目标尺寸
- `letterbox`: 保持比例缩放边缘填充黑色推荐避免目标变形
当使用 `letterbox` 模式时系统会自动记录变换元数据`FrameTransformMeta`后续节点 `ai_yolo`、`osd`会自动处理坐标映射无需手动转换
### 6.2 AI 分析
| 节点类型 | 功能描述 | 关键参数 | 硬件依赖 |
| :-------------- | :------------------------- | :----------------------------------- | :------- |
| `ai_yolo` | 通用目标检测YOLOv5/v8 | `model_path`, `model_version`, `num_classes`, `conf`, `nms`, `class_filter` | NPU(RKNN) |
**ai_yolo 配置示例ppe_det_yolov8_ppe11_768_rk3588.rknn**
```json
{
"id": "yolo_cam1",
"type": "ai_yolo",
"model_path": "./models/ppe_det_yolov8_ppe11_768_rk3588.rknn",
"model_version": "v8",
"model_w": 768,
"model_h": 768,
"num_classes": 11,
"conf": 0.35,
"nms": 0.45,
"class_filter": [3, 6]
}
```
**关键参数:**
| 参数 | 类型 | 说明 |
|------|------|------|
| `model_path` | string | RKNN 模型路径 |
| `model_version` | string | 模型版本: `v5` `v8` |
| `num_classes` | int | 模型类别数必须与实际一致 |
| `conf` | float | 置信度阈值0-1 |
| `nms` | float | NMS IoU 阈值0-1 |
| `class_filter` | array | 只输出指定类别的检测结果 `[3, 6]` 表示只检测 boots Person |
| `ai_face_det` | 人脸检测含5点关键点 | `model_path`, `conf`, `nms`, `max_faces`, `output_landmarks` | NPU(RKNN) |
| `ai_face_recog` | 人脸识别对齐+特征提取+检索 | `model_path`, `gallery`, `threshold`, `align` | NPU(RKNN) |
| `ai_pose` | 关键点检测可选扩展 | `model_path` | NPU |
| `ai_custom` | 自定义 AI 算法 | `model_path`, `custom_param` | NPU |
#### 6.2.1 det_post检测结果后处理按类别策略链
`det_post` 是一个 **Filter** 节点位于 `ai_yolo` 之后负责对 `frame->det->items` **按类别可配置**的后处理
**设计目标:**
- 不同类别可以挂不同的处理器链Processor/Strategy Registry避免一个类别一个插件”。
- 默认改写 `frame->det` `osd` / `alarm` 读到一致结果
**配置示例:**
```json
{
"id": "pp_cam1",
"type": "det_post",
"role": "filter",
"enable": true,
"default": { "enable": false },
"per_class": [
{
"class_id": 3,
"enable": true,
"processors": [
{ "type": "conf_gate", "conf_min": 0.65 },
{ "type": "bbox_gate", "min_w": 10, "min_h": 10 },
{ "type": "roi_gate", "roi": { "x": 0.1, "y": 0.1, "w": 0.8, "h": 0.8 }, "mode": "center" }
]
},
{
"class_id": 5,
"enable": true,
"processors": [
{ "type": "hsv_ratio", "h_min": 0, "h_max": 20, "s_min": 20, "v_min": 70, "ratio_min": 0.7,
"max_samples": 2048, "step": 0, "min_samples": 32,
"crop": { "x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0 } }
]
},
{
"class_id": 7,
"enable": true,
"processors": [
{ "type": "lab_kmeans", "k": 3, "iters": 8, "l_max": 50, "ratio_min": 0.4,
"max_samples": 2048, "step": 0, "min_samples": 64,
"crop": { "x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0 } }
]
}
]
}
```
**内置 processors当前实现**
- `conf_gate`按置信度过滤`conf_min`
- `bbox_gate`按框尺寸过滤`min_w`, `min_h`
- `roi_gate` ROI 过滤`roi:{x,y,w,h}` 归一化`mode:center` 使用 bbox 中心点判断
- `hsv_ratio`框内采样像素转 HSV统计阈值命中占比`h_min/h_max/s_min/s_max/v_min/v_max/ratio_min`支持 `crop/max_samples/step`
- `lab_kmeans`框内采样像素转 LabKMeans 聚类后取最暗簇做判定`k/iters/l_max/ratio_min`支持 `crop/max_samples/step`
> 注意:`hsv_ratio/lab_kmeans` 需要 `Frame` 提供可读的像素数据(`frame->data` 可访问)。若上游是纯 DMA-BUF 且未映射到 CPU这两类处理器会自然退化为“不过滤”。
**ai_yolo 参数说明:**
- `model_version`: `"v5"`, `"v8"` `"auto"`自动检测
- `num_classes`: 模型类别数默认 80
- `class_filter`: 只检测指定类别 ID 的数组 `[0, 1, 2]`
> 所有 AI 节点通过 **AiScheduler** 使用 NPU不直接持有 NPU 句柄。
#### 6.2.2 ai_face_det人脸检测
人脸检测节点基于 RetinaFace/SCRFD 等轻量模型输出人脸框和 5 点关键点
**配置示例:**
```json
{
"id": "face_det_cam1",
"type": "ai_face_det",
"role": "filter",
"enable": true,
"model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn",
"conf": 0.6,
"nms": 0.4,
"max_faces": 10,
"output_landmarks": true,
"input_format": "rgb"
}
```
**参数说明:**
- `model_path`: RetinaFace RKNN 模型路径
- `conf`: 置信度阈值默认 0.6
- `nms`: NMS 阈值默认 0.4
- `max_faces`: 最大检测人脸数
- `output_landmarks`: 是否输出 5 点关键点用于后续对齐
- `input_format`: 输入格式 `rgb` `bgr`
**输出:** 写入 `frame->face_det` `ai_face_recog` `osd` 读取
#### 6.2.3 ai_face_recog人脸识别
人脸识别节点读取检测结果对每个人脸进行对齐特征提取人脸库检索
**配置示例:**
```json
{
"id": "face_recog_cam1",
"type": "ai_face_recog",
"role": "filter",
"enable": true,
"model_path": "./models/face_recog_mobilefacenet_arcface_112_rk3588.rknn",
"align": true,
"emit_embedding": false,
"max_faces": 10,
"input_format": "rgb",
"threshold": {
"accept": 0.45,
"margin": 0.05
},
"gallery": {
"backend": "sqlite",
"path": "./models/face_gallery.db",
"load_on_start": true,
"expected_dim": 512
}
}
```
**参数说明:**
- `model_path`: ArcFace/MobileFaceNet RKNN 模型路径输入 112x112输出 512D embedding
- `align`: 是否使用 5 点关键点做仿射对齐推荐开启
- `emit_embedding`: 是否在结果中输出 embedding调试用生产关闭以省内存
- `threshold.accept`: 相似度接受阈值高于此值认为匹配成功
- `threshold.margin`: top1 top2 相似度差值阈值可选用于降低误识率
- `gallery.backend`: 人脸库后端当前支持 `sqlite`
- `gallery.path`: SQLite 数据库路径
- `gallery.expected_dim`: embedding 维度需与模型匹配
**判定规则:**
- `top1_sim >= accept` 可选`top1_sim - top2_sim >= margin` → 识别为该人
- 否则标记为 `unknown`
**输出:** 写入 `frame->face_recog`,供 `osd``alarm` 读取。
#### 6.2.4 人脸识别 Pipeline 示例
```text
[input_rtsp] → [preprocess] → [ai_face_det] → [ai_face_recog] → [osd] → [publish]
```
**完整配置示例:**
```json
{
"name": "cam1_face_recog",
"nodes": [
{ "id": "in_cam1", "type": "input_rtsp", "url": "rtsp://..." },
{ "id": "pre_cam1", "type": "preprocess", "dst_w": 1280, "dst_h": 720, "dst_format": "rgb" },
{ "id": "face_det_cam1", "type": "ai_face_det", "model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn", "conf": 0.6 },
{ "id": "face_recog_cam1", "type": "ai_face_recog", "model_path": "./models/face_recog_mobilefacenet_arcface_112_rk3588.rknn", "gallery": { "path": "./models/face_gallery.db" } },
{ "id": "osd_cam1", "type": "osd", "draw_face_det": true },
{ "id": "pub_cam1", "type": "publish", "outputs": [{ "proto": "rtsp_server", "port": 8555, "path": "/live/face" }] }
],
"edges": [
["in_cam1", "pre_cam1"],
["pre_cam1", "face_det_cam1"],
["face_det_cam1", "face_recog_cam1"],
["face_recog_cam1", "osd_cam1"],
["osd_cam1", "pub_cam1"]
]
}
```
> **注意**:人脸识别应在**原始分辨率**(如 1280x720上进行检测模型内部会自动缩放到 320x320 推理,输出坐标缩放回原始尺寸。这样从高清图裁剪对齐人脸,识别质量更高。
#### 6.2.5 人脸库Gallery说明
人脸库使用 SQLite 存储,表结构:
```sql
CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT,
extra TEXT -- JSON 扩展字段
);
CREATE TABLE embedding (
id INTEGER PRIMARY KEY,
person_id INTEGER NOT NULL,
embedding BLOB NOT NULL, -- 512D float32 向量
created_at TEXT,
FOREIGN KEY (person_id) REFERENCES person(id)
);
```
**检索实现:**
- 小库≤几千人CPU 暴力点积,无需额外索引
- 大库(≥几万人):可扩展 HNSW/Faiss 等 ANN 索引
### 6.3 业务逻辑与输出
| 节点类型 | 功能描述 | 关键参数 | 硬件依赖 |
| :---------- | :--------------------------------- | :----------------------------------- | :----------------- |
| `osd` | 绘制检测框、文字等 | `draw_bbox`, `draw_text`, `labels`, `line_width`, `font_scale` | RGA / CPU |
| `alarm` | 报警规则 + 多种报警动作 | `rules`, `actions`(见下文详细说明) | CPU |
#### 6.3 Behavior Events
Behavior events are emitted through `frame->behavior_events` and are not encoded as synthetic detection classes.
First-release behavior nodes:
- `region_event`: rule-driven `intrusion` and rule-based `climb`
- `action_recog`: temporal-rule `fall` and `fight`
- `event_fusion`: stable `event_id` assignment and lifecycle normalization
Recommended first-release graph order:
```text
[input_rtsp] -> [preprocess] -> [ai_yolo] -> [tracker] -> [region_event] -> [action_recog] -> [event_fusion] -> [osd] -> [publish] -> [alarm]
```
Behavior event payload includes:
- event type
- lifecycle status
- related `track_id` values
- duration in milliseconds
- event bbox
- optional `region_id`
Behavior event alarm rules should use:
- `use_behavior_events: true`
- `event_types`
- `region_ids` when region filtering is needed
- `min_duration_ms`
- `cooldown_ms`
Example alarm rule:
```json
{
"name": "intrusion_zone_a",
"use_behavior_events": true,
"event_types": ["intrusion"],
"region_ids": ["zone_a"],
"min_duration_ms": 1000,
"cooldown_ms": 10000
}
```
| `storage` | 持续录像MP4/TS 切片) | `mode`, `segment_sec`, `path` | MPP VENC + IO |
| `publish` | 编码并推流RTSP/HLS | `codec`, `bitrate`, `outputs` | MPP VENC + 网络 |
| `zlm_http` | 内嵌 HTTP 文件服务(为 HLS 提供访问) | `root`, `prefix`, `port`, `ssl` | ZLMediaKit(mk_api) |
> 说明:当前实现中 `publish` 的 `proto:"hls"` 为**落盘生成 m3u8/ts 文件**`path` 应指向 `index.m3u8` 文件路径;若需要通过网络播放,建议新增 `zlm_http` 节点启动内嵌 HTTP并将 `root` 指向 HLS 父目录(如 `/data/hls`)。
**osd 参数说明:**
- `labels`: 自定义类别标签数组,如 `["cat", "dog", "bird"]`,为空则使用 COCO 默认标签
### 6.4 alarm 插件详细说明
alarm 插件采用**模块化 Actions 架构**,支持多种报警动作的灵活组合:
**架构设计:**
```
┌─────────────────────────────────────────────────────────┐
│ AlarmNode │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ RuleEngine │ │ FrameRingBuffer (pre-event) │ │
│ │ - 类别过滤 │ │ - 保存最近 N 秒的帧 │ │
│ │ - ROI 检测 │ └──────────────────────────────────┘ │
│ │ - 持续时间 │ │
│ │ - 冷却控制 │ ┌──────────────────────────────────┐ │
│ └──────────────┘ │ AlarmActions (可配置) │ │
│ │ - LogAction → 日志输出 │ │
│ │ - HttpAction → HTTP 回调 │ │
│ │ - ExternalApiAction → 外部API报警 │ │
│ │ - SnapshotAction → 截图+上传 │ │
│ │ - ClipAction → 视频片段+上传 │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
**完整配置示例:**
```json
{
"id": "alarm_cam1",
"type": "alarm",
"role": "sink",
"labels": ["矿体A", "矿体B", "异物"],
"rules": [
{
"name": "ore_detection",
"class_ids": [0, 1],
"objects": ["矿体A"],
"roi": { "x": 0.1, "y": 0.1, "w": 0.8, "h": 0.8 },
"min_duration_ms": 500,
"cooldown_ms": 5000,
"schedule": "08:00-20:00"
}
],
"actions": {
"log": { "enable": true, "level": "info" },
"http": {
"enable": true,
"url": "http://业务系统/api/alarm",
"timeout_ms": 3000,
"include_media_url": true
},
"external_api": {
"enable": false,
"getTokenUrl": "http://业务系统/api/getToken",
"putMessageUrl": "http://业务系统/api/putMessage",
"tenantCode": "32",
"channelNo": "${vod_channelNo}",
"timeout_ms": 3000,
"include_media_url": true,
"token_header": "X-Access-Token",
"token_json_path": "responseBody.token",
"token_cache_sec": 600
},
"snapshot": {
"enable": true,
"format": "jpg",
"quality": 85,
"upload": {
"type": "minio",
"endpoint": "http://minio:9000",
"bucket": "alarms",
"access_key": "${MINIO_ACCESS_KEY}",
"secret_key": "${MINIO_SECRET_KEY}"
}
},
"clip": {
"enable": true,
"pre_sec": 5,
"post_sec": 10,
"format": "mp4",
"upload": { "type": "local", "path": "/tmp/alarms" }
}
}
}
```
**external_api 动作说明(对接外部系统报警:先取 token再上报告警**
- 取 token`getTokenUrl` 发起 HTTP POST默认从响应 JSON 的 `responseBody.token` 读取(可用 `token_json_path` 修改点号路径)。
- 上报告警:对 `putMessageUrl` 发起 HTTP POST并携带 Header`Content-Type: application/json`、`X-Access-Token: <token>`(可用 `token_header` 修改)。
- 上报请求体(固定结构):
```json
{
"tenantCode": "32",
"channelNo": "<vod_channelNo>",
"alarmContent": "<msg>",
"alarmTime": "YYYY-MM-DD HH:MM:SS",
"picInfo": [{"url": "<picUrl>"}],
"videoInfo": [{"url": "<videoUrl>"}]
}
```
- `alarmTime` 使用本地时间格式化。
- `picUrl/videoUrl` 来自 `snapshot/clip` 动作上传后的 URL通常为 MinIO 返回链接);若为空则对应数组为空。
- `alarmContent`:若配置中未提供 `alarmContent`,默认使用 `rule_name`
**external_api 参数说明:**
- 必填:`getTokenUrl`, `putMessageUrl`, `tenantCode`, `channelNo`
- 常用可选:`timeout_ms`(默认 3000、`include_media_url`(默认 true、`token_cache_sec`(默认 600为 0 时仅在 401/403 时刷新 token
- 其它可选:`token_header`、`token_json_path`、`alarmContent`、`max_queue_size` / `queue_policy`、`max_retries` / `retry_backoff_ms`
**规则参数说明:**
- `class_ids`: 要监控的类别 ID 数组(与模型输出对应)
- `objects`: 要监控的类别名称数组(需配合 `labels` 使用)
- `roi`: 感兴趣区域,归一化坐标 (0-1)
- `min_duration_ms`: 目标在 ROI 内持续多久才触发报警
- `cooldown_ms`: 同一规则连续触发的冷却时间
- `schedule`: 有效时间段,格式 "HH:MM-HH:MM"
**上传器类型:**
- `local`: 保存到本地文件系统
- `minio`: 上传到 MinIO 对象存储
- `s3`: 上传到 AWS S3兼容 MinIO
### 6.5 storage 插件详细说明
storage 插件用于**持续录像**7x24 小时),与 alarm 的事件录像功能互补。
**配置示例:**
```json
{
"id": "storage_cam1",
"type": "storage",
"role": "sink",
"mode": "continuous",
"format": "mp4",
"codec": "h264",
"segment_sec": 300,
"path": "/rec/cam1",
"filename_pattern": "%Y%m%d/%H%M%S",
"fps": 25,
"bitrate_kbps": 2000
}
```
**参数说明:**
- `mode`: 录像模式,当前支持 `continuous`(持续录像)
- `format`: 输出格式,`mp4` 或 `ts`
- `segment_sec`: 单个文件时长(秒)
- `filename_pattern`: 文件名模式,支持 strftime 格式
---
## 七、线程模型Thread Model
### 7.1 线程类型与职责
| 线程类型 | 职责 | 绑核策略(可配置) |
| :-------------- | :-------------------------------------- | :-------------------------- |
| `main` | 程序入口、配置加载、GraphMgr 调度 | 默认 CPU0 |
| `input-io` | RTSP 网络 IO | 大核/通用 |
| `decode` | 调用 MPP 解码 | 大核 |
| `preprocess` | RGA/GPU 前处理 | 视情况绑核 |
| `ai-worker` | 调度 AiScheduler异步调用 NPU | 大核 |
| `post/osd` | OSD 绘制、报警逻辑等 | 小核优先 |
| `storage` | 文件写入、切片管理 | 小核 |
| `publish` | RTSP/HLS 推流 | 小核 |
| `metrics/log` | 指标采集、日志异步写入 | 任意核 |
### 7.2 配置控制
- 节点配置中可增加字段:
```json
"cpu_affinity": [2, 3] // 可选
```
- 若未配置,由框架根据 `role` 和负载自动分配。
---
## 八、性能指标Performance Targets
| 场景 | 指标 | 目标值 |
| :------------- | :------------- | :--------------------- |
| 8×1080p@25fps | 整机总处理帧率 | ≥ 200 fps |
| 单路端到端 | 延迟 | ≤ 500 ms典型 ≤ 380 |
| NPU | 利用率 | ≥ 90% |
| 8 路全开 | CPU 占用 | ≤ 30% |
| 7×24 小时运行 | 内存泄漏 | 无明显增长(< 10MB |
测试要求简要
- 在实际 RK3588 设备上使用典型 YOLO 模型和 8 RTSP 输入进行长稳测试
- 统计长时间运行中的 CPU内存NPU 使用情况
---
## 九、监控与运维Metrics & Ops
### 9.1 节点级指标
每个节点提供以下基本指标通过 HTTP / Prometheus 等暴露
- `node_input_fps` / `node_output_fps`
- `queue_length`入队/出队队列长度
- `frame_drop_count`
- `error_count`
- `avg_process_time_ms`Process 平均处理耗时
### 9.2 图 / 通道级指标
- 通道状态running / error / disabled
- 报警次数 / 最近报警时间
- 当前推流客户端数RTSP/HLS
- 当前录像文件信息路径 / 大小 / 最近片段时间
### 9.3 管理接口REST 简要)
- `GET /api/graphs`列出所有 graph 状态
- `GET /api/graphs/{name}`单通道详情
- `POST /api/config/reload`触发配<EFBFBD><EFBFBD><EFBFBD>重加载
- `GET /api/nodes/{id}/metrics`查询节点指标
- `POST /api/nodes/{id}/config`动态更新节点配置调用 `UpdateConfig`
---
## 十、开发与部署环境
- **硬件平台**RK35884×A76 + 4×A55 + NPU + VPU + RGA
- **工具链**
- 板上编译GCC 10+推荐
- 交叉编译`aarch64-linux-gnu-gcc 12+`可选
- 构建CMake 3.20
- **调试**
- `gdbserver` + VSCode 远程调试
- RKNN Profiler 分析 NPU 利用率
- **日志**
- `spdlog` 异步日志
- 日志级别可运行时调整通过配置或 REST 接口
### 10.1 板上编译(推荐)
RK3588 板子上直接编译无需配置交叉编译环境
**依赖安装:**
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y build-essential cmake git pkg-config \
libavformat-dev libavcodec-dev libavutil-dev libswscale-dev
```
**编译命令:**
```bash
cd Rk3588Sys
# 配置(启用所有硬件加速)
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DRK3588_ENABLE_FFMPEG=ON \
-DRK3588_ENABLE_MPP=ON \
-DRK3588_ENABLE_RGA=ON \
-DRK3588_ENABLE_ZLMEDIAKIT=ON \
-DRK_ZLMK_API_LIB_PATH=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/aarch64/libmk_api.so \
-DRK_ZLMEDIAKIT_INCLUDE_DIR=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/include
# 编译
cmake --build build -j$(nproc)
```
**CMake 选项说明:**
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `RK3588_ENABLE_FFMPEG` | 启用 FFmpeg 拉流/软解码 | OFF |
| `RK3588_ENABLE_MPP` | 启用 MPP 硬件编解码 | OFF |
| `RK3588_ENABLE_RGA` | 启用 RGA 硬件图像处理 | OFF |
| `RK3588_ENABLE_RKNN` | 启用 RKNN NPU 推理 | OFF |
| `RK3588_ENABLE_ZLMEDIAKIT` | 启用内置 RTSP Server | OFF |
**运行:**
```bash
# 纯转码网关
./build/media-server --config configs/sample_cam1.json
# 带预处理的 pipeline
./build/media-server --config configs/sample_preprocess.json
# 内置 RTSP Server 模式
./build/media-server --config configs/sample_cam1_rtsp_server.json
```
### 10.2 交叉编译(可选)
x86 主机上交叉编译适合 CI/CD 或批量部署
```bash
# 需要安装交叉编译工具链
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# 配置
cmake -S . -B build-cross \
-DCMAKE_TOOLCHAIN_FILE=cmake/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DRK3588_ENABLE_FFMPEG=ON \
-DRK3588_ENABLE_MPP=ON \
-DRK3588_ENABLE_RGA=ON
# 编译
cmake --build build-cross -j$(nproc)
# 同步到板子
rsync -avz build-cross/ user@board_ip:/opt/media-server/
```
---
## 十一、扩展与演进
1. **新增算法插件**
- 实现新的 `INode` 子类例如 `ai_cls`, `ai_reid`)。
- 使用 `REGISTER_NODE` 编译为 `.so` 放入 `plugin_path`
- `config.json` 中新增节点即可接入全流程
2. **新增输出类型**
- 新增 `publish_webrtc`、`storage_s3` 等节点类型
- 不改动上游图结构只调整 sink 部分
3. **分布式扩展(后续版本)**
- 引入 ZeroMQ / NanoMSG将远端设备作为特殊节点处理 `remote_sink`)。
- 中控只维护抽象图实际在多台 RK3588 设备上分布执行
---
---
## 附录:问题修复与调试文档
### 已知问题修复
| 问题 | 文档 | 日期 |
|------|------|------|
| YOLOv8 FP16 数据解析错误 | [docs/bugfix_yolov8_fp16.md](docs/bugfix_yolov8_fp16.md) | 2025-02 |
| RKNN 输出格式配置错误 | [docs/fix_rknn_output_format.md](docs/fix_rknn_output_format.md) | 2026-02 |
### 调试与开发文档
- [docs/models.md](docs/models.md) - 模型转换与部署指南
- [docs/Agent_API_Extensions.md](docs/Agent_API_Extensions.md) - Agent API 扩展说明
- [docs/API_Device_RemoteMgmt_InterfaceTable.md](docs/API_Device_RemoteMgmt_InterfaceTable.md) - 远程管理接口