Files
video-ai-analysis/video_ai_analysis_system_plan.md
2026-06-17 11:33:54 +08:00

1194 lines
20 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.

# 门店视频 AI 分析系统 PoC 实施方案
生成日期2026-06-15
## 1. 系统架构
### 1.1 整体 Pipeline
```text
[门店/摄像头配置]
|
v
[海康云眸接入层]
- 获取 access_token
- 查询云录像回放 URL / RTSP / HLS / MP4
- 按 15-60 分钟切片拉取
|
v
[本地视频缓存层: SSD]
- raw_segments/
- segment_manifest.sqlite
- 记录下载、抽帧、推理状态
|
v
[FFmpeg + NVDEC 抽帧层]
- GPU 硬解码 H.264/H.265
- 1 FPS 抽帧
- 输出带时间戳的 JPEG 帧
|
v
[Clip 构建层]
- 10-20 秒窗口
- 每个 clip 8-16 帧
- 生成 clip manifest
|
v
[4B LoRA 模型推理层]
- FP16
- batch size 1-4
- clip 级推理
|
v
[结果标准化层]
- clip_result.jsonl
- schema 校验
- JSON 解析失败重试
|
v
[门店级聚合层]
- 事件合并
- 时间聚合
- 输出 store_result.json
```
### 1.2 数据流向
系统按门店、摄像头、日期组织任务。
一个最小任务单元建议定义为:
```text
store_id + camera_id + business_date
```
单路 24 小时视频不要一次性处理,而是拆成多个 segment
```text
store_001 / cam_01 / 2026-06-14
-> 24 个 1 小时 segment
-> 每个 segment 独立拉取、解码、抽帧
-> 所有帧按时间顺序组成 clip
-> clip 级模型推理
-> 汇总为门店级 JSON
```
核心原则:
- 视频接入和模型推理解耦。
- 原始视频、抽帧结果、推理结果都可断点续跑。
- 模型只对 clip 推理,不逐帧调用 LLM。
- PoC 阶段优先可观测、可复跑,不追求极致流水线性能。
## 2. 模块拆解
### 2.1 视频接入层
#### 目标
从海康云眸获取指定门店、指定摄像头、指定时间范围内的 24 小时录像。
#### 推荐接入方式
优先级如下:
```text
1. 云录像文件下载 URL / MP4 URL
2. HLS 回放 URL
3. RTSP 回放 URL
4. 直播 RTSP 本地录制
```
PoC 推荐优先使用云录像回放或可下载视频文件。直播 RTSP 只能从当前时间开始录制,不适合补历史 24 小时数据。
#### 鉴权假设
海康云眸常见接入流程可抽象为:
```text
app_key / app_secret
|
v
access_token
|
v
camera_id + begin_time + end_time
|
v
playback_url
```
系统应把 token 和 URL 视为有有效期的临时凭证。
建议保存:
```text
camera_id
segment_start
segment_end
playback_url_created_at
playback_url_expire_at
fetch_status
retry_count
last_error
```
#### 24 小时录像拉取策略
不要一次请求 24 小时录像。
推荐拆分:
```text
默认24 个 1 小时 segment
网络不稳定时48 个 30 分钟 segment
URL 过期较快时96 个 15 分钟 segment
```
每个 segment 独立处理:
```text
pending -> fetching -> fetched -> sampling -> sampled -> inferencing -> inferred -> aggregated
```
### 2.2 视频处理层
#### FFmpeg + NVDEC 硬解码
Ubuntu 24 上需要确认:
```bash
ffmpeg -hwaccels
ffmpeg -decoders | grep cuvid
```
H.264 视频示例:
```bash
ffmpeg \
-hide_banner -nostdin -y \
-rtsp_transport tcp \
-hwaccel cuda \
-c:v h264_cuvid \
-i "$INPUT" \
-vf "fps=1,scale=640:-2" \
-q:v 4 \
"$FRAME_DIR/%06d.jpg"
```
H.265 视频示例:
```bash
ffmpeg \
-hide_banner -nostdin -y \
-rtsp_transport tcp \
-hwaccel cuda \
-c:v hevc_cuvid \
-i "$INPUT" \
-vf "fps=1,scale=640:-2" \
-q:v 4 \
"$FRAME_DIR/%06d.jpg"
```
如果输入是本地 MP4 文件,可以去掉 `-rtsp_transport tcp`
#### 1 FPS 抽帧策略
24 小时视频按 1 FPS 抽帧:
```text
1 小时3600 张帧
24 小时86400 张帧
2 路摄像头172800 张帧
```
PoC 阶段建议输出 JPEG
```text
质量:-q:v 4
分辨率scale=640:-2 或按模型输入要求调整
命名:按 segment 内序号或绝对时间戳
```
更推荐保存时间戳映射:
```json
{"frame_path":"frames_1fps/000001.jpg","timestamp":"2026-06-14T00:00:01+08:00","segment_id":"seg_0000"}
```
#### 视频缓存策略
本地 SSD 目录建议:
```text
/data/video-ai/
stores/{store_id}/
cameras/{camera_id}/
{business_date}/
raw_segments/
00_00_00__01_00_00.mp4
frames_1fps/
00_00_00.jpg
00_00_01.jpg
manifests/
segments.sqlite
frames.jsonl
clips.jsonl
results/
clip_results.jsonl
store_result.json
```
缓存策略:
```text
raw video segment:
SSD 临时缓存,抽帧成功后可删除
1 FPS frames:
SSD 中间产物,推理完成后保留 1-7 天
clip tensor:
不落盘,运行时构建
result JSONL:
长期保留
```
PoC 不建议使用全内存缓存。24 小时视频帧数较多,磁盘缓存更适合断点续跑和调试。
### 2.3 Clip 构建层
#### Clip 长度建议
PoC 默认:
```text
clip_length = 10 秒
stride = 10 秒
frames_per_clip = 8 或 10
```
对应规模:
```text
1 小时视频:
10 秒 clip -> 360 个 clip
20 秒 clip -> 180 个 clip
24 小时视频:
10 秒 clip -> 8640 个 clip
20 秒 clip -> 4320 个 clip
```
如果模型推理较慢,可以切换为:
```text
clip_length = 20 秒
stride = 20 秒
frames_per_clip = 12 或 16
```
#### 帧采样策略
不要用编码关键帧作为模型输入采样依据。关键帧是视频压缩概念,不一定对应行为变化。
推荐均匀采样:
```text
10 秒 clip:
1 FPS 得到 10 张帧
模型支持 10 帧时全部输入
模型只支持 8 帧时均匀采样 8 张
20 秒 clip:
1 FPS 得到 20 张帧
均匀采样 12 或 16 张
```
#### Clip Manifest
建议为每个 clip 生成清单:
```json
{
"clip_id": "store_001_cam_01_20260614_123120",
"store_id": "store_001",
"camera_id": "cam_01",
"start_time": "2026-06-14T12:31:20+08:00",
"end_time": "2026-06-14T12:31:30+08:00",
"frame_paths": [
"frames_1fps/12_31_20.jpg",
"frames_1fps/12_31_21.jpg"
],
"status": "pending"
}
```
### 2.4 模型推理层
#### 3080 20GB 上运行 4B LoRA
显存粗略估算:
```text
4B FP16 base model: 约 8GB
LoRA adapter: 通常 < 1GB
vision encoder / projector / activation / KV cache: 4-8GB
运行余量: 3-6GB
```
推荐启动参数:
```text
dtype: FP16
batch_size: 1
frames_per_clip: 8
image_size: 336 或 448
max_new_tokens: 256-512
```
稳定后逐步尝试:
```text
batch_size: 1 -> 2 -> 4
frames_per_clip: 8 -> 12 -> 16
image_size: 336 -> 448 -> 640
```
出现 OOM 时按顺序回退:
```text
1. batch_size 降低
2. frames_per_clip 降低
3. image_size 降低
4. max_new_tokens 降低
5. 必要时使用 8bit / 4bit 量化
```
#### 推荐推理框架
PoC 首选:
```text
PyTorch + Transformers + PEFT
```
原因:
- 4B LoRA 加载简单。
- 多图 / 视频帧输入适配成本低。
- 更容易控制显存、batch、prompt、输出解析。
暂不首选:
```text
vLLM
```
原因:
- vLLM 更适合文本 LLM 高吞吐服务。
- 多模态模型和 LoRA 的支持取决于具体模型。
- 离线 PoC 的主要目标是链路跑通,不是低延迟在线服务。
#### 推理输入
每个 batch 包含多个 clip
```json
[
{
"clip_id": "store_001_cam_01_20260614_123120",
"frames": ["...jpg", "...jpg"],
"prompt": "请分析该门店监控片段..."
}
]
```
Prompt 应约束输出为固定 JSON
```text
只输出 JSON。
event_type 必须来自枚举。
没有事件时输出 events: []。
不要输出解释性文字。
```
#### 推理输出
clip 级输出建议保存为 JSONL
```json
{
"clip_id": "store_001_cam_01_20260614_123120",
"camera_id": "cam_01",
"start_time": "2026-06-14T12:31:20+08:00",
"end_time": "2026-06-14T12:31:30+08:00",
"status": "ok",
"events": [
{
"event_type": "queue_detected",
"confidence": 0.86,
"severity": "medium",
"attributes": {
"estimated_people": 5,
"location": "checkout_area"
}
}
],
"raw_response": null
}
```
JSON 解析失败时:
```text
1. 用更严格 prompt 重试一次
2. 仍失败则 status = parse_failed
3. 保存 raw_response
4. 聚合层跳过该 clip但记录 failed_clip_count
```
### 2.5 输出结构设计
#### 门店级 JSON Schema
```json
{
"store_id": "store_001",
"business_date": "2026-06-14",
"timezone": "Asia/Shanghai",
"pipeline_version": "poc-v1",
"cameras": [
{
"camera_id": "cam_01",
"video_start": "2026-06-14T00:00:00+08:00",
"video_end": "2026-06-15T00:00:00+08:00",
"processed_clip_count": 8640,
"failed_clip_count": 12
}
],
"summary": {
"total_events": 128,
"event_counts": {
"customer_enter": 53,
"staff_absent": 8,
"queue_detected": 12
}
},
"events": [
{
"event_id": "evt_000001",
"camera_id": "cam_01",
"event_type": "queue_detected",
"start_time": "2026-06-14T12:31:20+08:00",
"end_time": "2026-06-14T12:32:10+08:00",
"duration_seconds": 50,
"confidence": 0.86,
"severity": "medium",
"attributes": {
"estimated_people": 5,
"location": "checkout_area"
},
"evidence": {
"clip_ids": ["store_001_cam_01_20260614_123120"],
"frame_refs": ["frames_1fps/12_31_20.jpg"]
}
}
],
"processing": {
"started_at": "2026-06-15T01:00:00+08:00",
"finished_at": "2026-06-15T05:40:00+08:00",
"status": "completed_with_warnings"
}
}
```
#### 事件级结构
事件字段建议固定:
```text
event_id
store_id
camera_id
event_type
start_time
end_time
duration_seconds
confidence
severity
attributes
evidence
```
event_type 必须枚举化,例如:
```text
customer_enter
customer_leave
queue_detected
staff_absent
staff_present
area_crowded
abnormal_behavior
unknown
```
PoC 阶段不要让模型自由发明事件类型。
#### 时间聚合规则
合并条件:
```text
same camera_id
same event_type
相邻事件间隔 <= 30 秒
```
合并方式:
```text
start_time = 最早 start_time
end_time = 最晚 end_time
duration_seconds = end_time - start_time
confidence = max(confidence) 或按 clip 时长加权平均
evidence.clip_ids = 合并所有 clip_id
```
## 3. 性能估算
### 3.1 数据规模
单路摄像头:
```text
1 小时视频:
1 FPS -> 3600 张帧
10 秒 clip -> 360 个 clip
20 秒 clip -> 180 个 clip
24 小时视频:
1 FPS -> 86400 张帧
10 秒 clip -> 8640 个 clip
20 秒 clip -> 4320 个 clip
```
双路摄像头:
```text
24 小时:
1 FPS -> 172800 张帧
10 秒 clip -> 17280 个 clip
```
### 3.2 单路 1 小时处理时间
如果视频已缓存到本地 SSD
```text
NVDEC 抽帧:
3-10 分钟
模型推理:
每 clip 0.5 秒 -> 约 3 分钟
每 clip 1.0 秒 -> 约 6 分钟
每 clip 2.0 秒 -> 约 12 分钟
聚合:
<1 分钟
合计:
约 8-25 分钟 / 小时视频
```
如果海康云眸回放只能按 1x 实时速度拉流:
```text
1 小时录像最少需要 1 小时获取
24 小时录像最少需要 24 小时获取
```
这需要在 PoC 第一天优先验证。
### 3.3 单门店 24 小时处理时间
单路摄像头:
```text
视频可快速下载:
约 3-10 小时
视频只能 1x 回放:
至少 24 小时
```
双路摄像头:
```text
视频可快速下载:
约 6-20 小时
视频只能 1x 回放:
至少 48 路小时
```
### 3.4 GPU 瓶颈分析
大概率瓶颈:
```text
1. 4B LoRA 多帧推理
2. 云录像下载速度
3. JPEG 编码和磁盘写入
```
一般不是瓶颈:
```text
NVDEC 解码能力
门店级 JSON 聚合
clip manifest 构建
```
## 4. PoC 实施步骤
### Day 1打通视频接入和 GPU 解码
任务:
```text
1. 准备 Ubuntu 24 + NVIDIA driver + CUDA runtime。
2. 确认 FFmpeg 支持 NVDEC。
3. 从海康云眸获取 10 分钟历史回放 URL。
4. 使用 FFmpeg 拉取或直接抽帧。
5. 验证输出帧数量。
```
命令:
```bash
ffmpeg -hwaccels
ffmpeg -decoders | grep cuvid
```
验收标准:
```text
10 分钟视频生成约 600 张 1 FPS 帧
确认使用 h264_cuvid 或 hevc_cuvid
确认不是 CPU 解码
```
### Day 2实现 segment 化和本地缓存
任务:
```text
1. 把 24 小时拆成 24 个 1 小时 segment。
2. 每个 segment 独立请求 playback_url。
3. 每个 segment 独立抽帧。
4. 保存 segment 状态。
5. 抽帧成功后生成 frames.jsonl。
```
segment 状态:
```text
pending
fetching
fetched
sampling
sampled
failed
```
验收标准:
```text
能处理 1 小时录像
中断后重跑不会重复处理已完成 segment
失败 segment 有 retry_count 和 last_error
```
### Day 3实现 Clip 构建和模型推理
任务:
```text
1. 读取 frames.jsonl。
2. 按 10 秒窗口构建 clip。
3. 每个 clip 均匀采样 8-10 帧。
4. 加载 4B base model + LoRA adapter。
5. 使用 FP16 推理。
6. batch_size 从 1 开始。
7. 输出 clip_results.jsonl。
```
验收标准:
```text
1 小时视频生成 360 个 10 秒 clip
模型完成 clip 级推理
JSON 可解析率 > 95%
RTX 3080 20GB 不 OOM
```
### Day 4实现聚合、容错和性能统计
任务:
```text
1. 实现 clip_results.jsonl 到 store_result.json 的聚合。
2. 增加 JSON schema 校验。
3. 解析失败的 clip 重试一次。
4. 统计下载、抽帧、推理耗时。
5. 记录 GPU 峰值显存。
```
需要记录的指标:
```text
download_time_seconds
decode_time_seconds
frames_per_second
clips_per_second
gpu_memory_peak_mb
failed_segments
failed_clips
parse_failed_clips
```
验收标准:
```text
1 小时完整链路跑通
生成 store_result.json
失败任务可断点续跑
```
### Day 5跑完整 24 小时
任务:
```text
1. 跑单路摄像头 24 小时。
2. 跑双路摄像头 24 小时。
3. 记录全链路耗时。
4. 固化 PoC 参数。
5. 输出失败清单和优化建议。
```
最终参数需要确认:
```text
clip_length
stride
frames_per_clip
batch_size
image_size
raw segment 是否保留
frames 保留天数
```
验收标准:
```text
单门店 1-2 路摄像头 24 小时离线分析完成
输出门店级 JSON
有可复跑的 segment 和 clip 状态
```
## 5. 关键设计决策说明
### 5.1 先单机跑通
PoC 只做 1 家门店,单机单 GPU 足够验证链路。
不建议一开始引入:
```text
Kafka
Spark
Kubernetes
复杂分布式任务系统
```
PoC 阶段使用:
```text
本地文件系统
SQLite 或 JSONL manifest
单进程或少量 worker
```
### 5.2 使用磁盘帧缓存
原因:
```text
1. 方便调试模型输入。
2. 模型调参时不用重复拉视频。
3. 海康 playback_url 过期后不影响后续实验。
4. 支持断点续跑。
```
后续链路稳定后,可以优化为:
```text
FFmpeg pipe -> frame queue -> model worker
```
但 PoC 不优先做。
### 5.3 默认 10 秒 clip
10 秒 clip 的优势:
```text
语义粒度足够细
事件定位较准确
推理量可控
方便聚合
```
如果吞吐不够,切到 20 秒 clip
```text
clip 数量减少 50%
事件时间定位精度下降
```
### 5.4 小 batch 推理
RTX 3080 20GB 显存有限。
建议从保守参数开始:
```text
batch_size = 1
frames_per_clip = 8
image_size = 336 或 448
```
稳定后再提高 batch。
### 5.5 结构化输出优先
模型输出必须被工程系统消费。
因此:
```text
输出 JSON
event_type 枚举
schema 校验
失败重试
保留 raw_response
```
不要依赖自然语言总结作为主输出。
## 6. 故障与容错设计
### 6.1 RTSP / 回放 URL 失败
处理方式:
```text
1. 重新获取 playback_url。
2. 使用 -rtsp_transport tcp。
3. 将 1 小时 segment 拆成 30 或 15 分钟。
4. 最多重试 3 次。
5. 仍失败则标记 failed不阻塞其他 segment。
```
### 6.2 解码失败
处理方式:
```text
1. 使用 ffprobe 获取 codec。
2. H.264 使用 h264_cuvid。
3. H.265 使用 hevc_cuvid。
4. 失败后缩短 segment 重试。
5. 仍失败则记录 ffmpeg stderr。
```
由于项目约束要求必须 GPU 解码,不建议默认 fallback 到 CPU 解码。
### 6.3 模型 OOM
处理顺序:
```text
1. batch_size: 4 -> 2 -> 1
2. frames_per_clip: 16 -> 12 -> 8
3. image_size: 640 -> 448 -> 336
4. max_new_tokens: 512 -> 256
5. 必要时使用 8bit / 4bit 量化
```
每次 OOM 后应:
```text
torch.cuda.empty_cache()
记录失败 clip
用更小配置重试
```
### 6.4 JSON 解析失败
处理方式:
```text
1. 严格 prompt 重试一次。
2. 仍失败则 status = parse_failed。
3. 保存 raw_response。
4. 聚合层跳过该 clip。
5. 门店级结果记录 failed_clip_count。
```
### 6.5 断点续跑
每层都要有 manifest
```text
segments.sqlite:
记录每个视频段状态
frames.jsonl:
记录每张帧和时间戳
clips.jsonl:
记录每个 clip 和输入帧
clip_results.jsonl:
记录每个 clip 的推理结果
```
重跑逻辑:
```text
已有 sampled segment 不重复抽帧
已有 inferred clip 不重复推理
只补 failed / missing / parse_failed
```
## 7. 可扩展性设计
### 7.1 从 1 家门店到 26 家门店
任务分片方式:
```text
store_id + camera_id + business_date
```
26 家门店规模:
```text
26 家 * 1-2 路摄像头 = 26-52 路摄像头
```
如果每路 24 小时需要 3-10 GPU 小时:
```text
每日全量处理需要 78-520 GPU 小时
```
单张 RTX 3080 不适合长期处理 26 家全量 24 小时视频。
### 7.2 单机阶段
结构:
```text
1 个 downloader
1 个 ffmpeg sampler
1 个 model worker
1 个 aggregator
```
建议:
```text
下载和抽帧可以提前跑
模型 worker 独占 GPU
不要多个模型进程抢同一张 3080
```
### 7.3 单机多 GPU 阶段
结构:
```text
1 个调度进程
N 个 GPU worker
每张 GPU 一个模型实例
```
任务分配:
```text
按 camera-day 分配给不同 GPU
每个 GPU worker 独立读取 clip manifest
```
### 7.4 多机多 GPU 阶段
最小生产化结构:
```text
Postgres:
任务状态表
对象存储 / NAS:
原始视频、帧、结果
多台 GPU 服务器:
拉取任务
独立处理
写回状态
```
暂不需要:
```text
Kafka
Spark
复杂实时流处理
```
## 8. 风险与解决方案
### 风险 1海康云眸回放速度受限
影响:
```text
如果只能 1x 回放24 小时视频至少需要 24 小时获取。
```
解决:
```text
PoC 第一天验证是否支持快速下载历史录像。
优先使用云录像文件下载 URL。
如果只能 RTSP 回放,需要重新评估每日处理窗口。
```
### 风险 2RTX 3080 显存不足
影响:
```text
4B LoRA + 多帧输入可能 OOM。
```
解决:
```text
batch_size 从 1 开始。
frames_per_clip 从 8 开始。
image_size 从 336 或 448 开始。
必要时使用 8bit / 4bit 量化。
```
### 风险 3模型对门店事件识别不稳定
影响:
```text
输出事件类型不一致,难以聚合。
```
解决:
```text
固定 event_type 枚举。
固定 JSON schema。
在 prompt 中要求没有事件时返回 events: []。
保存证据帧用于人工复核。
```
### 风险 4抽帧结果和真实时间不对齐
影响:
```text
事件时间戳错误。
```
解决:
```text
每个 segment 必须记录 segment_start。
帧时间戳由 segment_start + frame_index 秒计算。
如 FFmpeg 能输出 pts_time则优先使用 pts_time。
```
### 风险 5磁盘占用增长过快
影响:
```text
24 小时、多摄像头 JPEG 帧会占用大量 SSD。
```
解决:
```text
raw segment 抽帧成功后删除。
frames 推理完成后保留 1-7 天。
只长期保留结果 JSON 和事件证据帧。
```
## 9. 推荐 PoC 默认参数
```text
segment_length: 1 hour
fallback_segment_length: 15 minutes
decode: NVDEC h264_cuvid / hevc_cuvid
frame_rate: 1 FPS
frame_format: JPEG
frame_width: 640
clip_length: 10 seconds
clip_stride: 10 seconds
frames_per_clip: 8
model_dtype: FP16
batch_size: 1
max_new_tokens: 256
retry_count: 3 for segment
parse_retry_count: 1 for model output
```
## 10. 最小可行交付物
3-5 天 PoC 结束时应交付:
```text
1. 能处理单路 24 小时视频的离线脚本或命令入口。
2. 能处理 1 家门店 1-2 路摄像头。
3. 使用 FFmpeg + NVDEC 完成 1 FPS 抽帧。
4. 使用 4B LoRA 模型完成 clip 级推理。
5. 输出 clip_results.jsonl。
6. 输出 store_result.json。
7. 支持 segment 和 clip 断点续跑。
8. 有性能统计报告。
9. 有失败 segment / failed clip 清单。
```
不在 PoC 范围:
```text
实时告警
Web 管理后台
复杂多租户权限
分布式调度平台
自动模型训练闭环
```