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

20 KiB
Raw Permalink Blame History

门店视频 AI 分析系统 PoC 实施方案

生成日期2026-06-15

1. 系统架构

1.1 整体 Pipeline

[门店/摄像头配置]
        |
        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 数据流向

系统按门店、摄像头、日期组织任务。

一个最小任务单元建议定义为:

store_id + camera_id + business_date

单路 24 小时视频不要一次性处理,而是拆成多个 segment

store_001 / cam_01 / 2026-06-14
  -> 24 个 1 小时 segment
  -> 每个 segment 独立拉取、解码、抽帧
  -> 所有帧按时间顺序组成 clip
  -> clip 级模型推理
  -> 汇总为门店级 JSON

核心原则:

  • 视频接入和模型推理解耦。
  • 原始视频、抽帧结果、推理结果都可断点续跑。
  • 模型只对 clip 推理,不逐帧调用 LLM。
  • PoC 阶段优先可观测、可复跑,不追求极致流水线性能。

2. 模块拆解

2.1 视频接入层

目标

从海康云眸获取指定门店、指定摄像头、指定时间范围内的 24 小时录像。

推荐接入方式

优先级如下:

1. 云录像文件下载 URL / MP4 URL
2. HLS 回放 URL
3. RTSP 回放 URL
4. 直播 RTSP 本地录制

PoC 推荐优先使用云录像回放或可下载视频文件。直播 RTSP 只能从当前时间开始录制,不适合补历史 24 小时数据。

鉴权假设

海康云眸常见接入流程可抽象为:

app_key / app_secret
        |
        v
access_token
        |
        v
camera_id + begin_time + end_time
        |
        v
playback_url

系统应把 token 和 URL 视为有有效期的临时凭证。

建议保存:

camera_id
segment_start
segment_end
playback_url_created_at
playback_url_expire_at
fetch_status
retry_count
last_error

24 小时录像拉取策略

不要一次请求 24 小时录像。

推荐拆分:

默认24 个 1 小时 segment
网络不稳定时48 个 30 分钟 segment
URL 过期较快时96 个 15 分钟 segment

每个 segment 独立处理:

pending -> fetching -> fetched -> sampling -> sampled -> inferencing -> inferred -> aggregated

2.2 视频处理层

FFmpeg + NVDEC 硬解码

Ubuntu 24 上需要确认:

ffmpeg -hwaccels
ffmpeg -decoders | grep cuvid

H.264 视频示例:

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 视频示例:

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 抽帧:

1 小时3600 张帧
24 小时86400 张帧
2 路摄像头172800 张帧

PoC 阶段建议输出 JPEG

质量:-q:v 4
分辨率scale=640:-2 或按模型输入要求调整
命名:按 segment 内序号或绝对时间戳

更推荐保存时间戳映射:

{"frame_path":"frames_1fps/000001.jpg","timestamp":"2026-06-14T00:00:01+08:00","segment_id":"seg_0000"}

视频缓存策略

本地 SSD 目录建议:

/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

缓存策略:

raw video segment:
  SSD 临时缓存,抽帧成功后可删除

1 FPS frames:
  SSD 中间产物,推理完成后保留 1-7 天

clip tensor:
  不落盘,运行时构建

result JSONL:
  长期保留

PoC 不建议使用全内存缓存。24 小时视频帧数较多,磁盘缓存更适合断点续跑和调试。

2.3 Clip 构建层

Clip 长度建议

PoC 默认:

clip_length = 10 秒
stride = 10 秒
frames_per_clip = 8 或 10

对应规模:

1 小时视频:
  10 秒 clip -> 360 个 clip
  20 秒 clip -> 180 个 clip

24 小时视频:
  10 秒 clip -> 8640 个 clip
  20 秒 clip -> 4320 个 clip

如果模型推理较慢,可以切换为:

clip_length = 20 秒
stride = 20 秒
frames_per_clip = 12 或 16

帧采样策略

不要用编码关键帧作为模型输入采样依据。关键帧是视频压缩概念,不一定对应行为变化。

推荐均匀采样:

10 秒 clip:
  1 FPS 得到 10 张帧
  模型支持 10 帧时全部输入
  模型只支持 8 帧时均匀采样 8 张

20 秒 clip:
  1 FPS 得到 20 张帧
  均匀采样 12 或 16 张

Clip Manifest

建议为每个 clip 生成清单:

{
  "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

显存粗略估算:

4B FP16 base model: 约 8GB
LoRA adapter: 通常 < 1GB
vision encoder / projector / activation / KV cache: 4-8GB
运行余量: 3-6GB

推荐启动参数:

dtype: FP16
batch_size: 1
frames_per_clip: 8
image_size: 336 或 448
max_new_tokens: 256-512

稳定后逐步尝试:

batch_size: 1 -> 2 -> 4
frames_per_clip: 8 -> 12 -> 16
image_size: 336 -> 448 -> 640

出现 OOM 时按顺序回退:

1. batch_size 降低
2. frames_per_clip 降低
3. image_size 降低
4. max_new_tokens 降低
5. 必要时使用 8bit / 4bit 量化

推荐推理框架

PoC 首选:

PyTorch + Transformers + PEFT

原因:

  • 4B LoRA 加载简单。
  • 多图 / 视频帧输入适配成本低。
  • 更容易控制显存、batch、prompt、输出解析。

暂不首选:

vLLM

原因:

  • vLLM 更适合文本 LLM 高吞吐服务。
  • 多模态模型和 LoRA 的支持取决于具体模型。
  • 离线 PoC 的主要目标是链路跑通,不是低延迟在线服务。

推理输入

每个 batch 包含多个 clip

[
  {
    "clip_id": "store_001_cam_01_20260614_123120",
    "frames": ["...jpg", "...jpg"],
    "prompt": "请分析该门店监控片段..."
  }
]

Prompt 应约束输出为固定 JSON

只输出 JSON。
event_type 必须来自枚举。
没有事件时输出 events: []。
不要输出解释性文字。

推理输出

clip 级输出建议保存为 JSONL

{
  "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 解析失败时:

1. 用更严格 prompt 重试一次
2. 仍失败则 status = parse_failed
3. 保存 raw_response
4. 聚合层跳过该 clip但记录 failed_clip_count

2.5 输出结构设计

门店级 JSON Schema

{
  "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"
  }
}

事件级结构

事件字段建议固定:

event_id
store_id
camera_id
event_type
start_time
end_time
duration_seconds
confidence
severity
attributes
evidence

event_type 必须枚举化,例如:

customer_enter
customer_leave
queue_detected
staff_absent
staff_present
area_crowded
abnormal_behavior
unknown

PoC 阶段不要让模型自由发明事件类型。

时间聚合规则

合并条件:

same camera_id
same event_type
相邻事件间隔 <= 30 秒

合并方式:

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 数据规模

单路摄像头:

1 小时视频:
  1 FPS -> 3600 张帧
  10 秒 clip -> 360 个 clip
  20 秒 clip -> 180 个 clip

24 小时视频:
  1 FPS -> 86400 张帧
  10 秒 clip -> 8640 个 clip
  20 秒 clip -> 4320 个 clip

双路摄像头:

24 小时:
  1 FPS -> 172800 张帧
  10 秒 clip -> 17280 个 clip

3.2 单路 1 小时处理时间

如果视频已缓存到本地 SSD

NVDEC 抽帧:
  3-10 分钟

模型推理:
  每 clip 0.5 秒 -> 约 3 分钟
  每 clip 1.0 秒 -> 约 6 分钟
  每 clip 2.0 秒 -> 约 12 分钟

聚合:
  <1 分钟

合计:
  约 8-25 分钟 / 小时视频

如果海康云眸回放只能按 1x 实时速度拉流:

1 小时录像最少需要 1 小时获取
24 小时录像最少需要 24 小时获取

这需要在 PoC 第一天优先验证。

3.3 单门店 24 小时处理时间

单路摄像头:

视频可快速下载:
  约 3-10 小时

视频只能 1x 回放:
  至少 24 小时

双路摄像头:

视频可快速下载:
  约 6-20 小时

视频只能 1x 回放:
  至少 48 路小时

3.4 GPU 瓶颈分析

大概率瓶颈:

1. 4B LoRA 多帧推理
2. 云录像下载速度
3. JPEG 编码和磁盘写入

一般不是瓶颈:

NVDEC 解码能力
门店级 JSON 聚合
clip manifest 构建

4. PoC 实施步骤

Day 1打通视频接入和 GPU 解码

任务:

1. 准备 Ubuntu 24 + NVIDIA driver + CUDA runtime。
2. 确认 FFmpeg 支持 NVDEC。
3. 从海康云眸获取 10 分钟历史回放 URL。
4. 使用 FFmpeg 拉取或直接抽帧。
5. 验证输出帧数量。

命令:

ffmpeg -hwaccels
ffmpeg -decoders | grep cuvid

验收标准:

10 分钟视频生成约 600 张 1 FPS 帧
确认使用 h264_cuvid 或 hevc_cuvid
确认不是 CPU 解码

Day 2实现 segment 化和本地缓存

任务:

1. 把 24 小时拆成 24 个 1 小时 segment。
2. 每个 segment 独立请求 playback_url。
3. 每个 segment 独立抽帧。
4. 保存 segment 状态。
5. 抽帧成功后生成 frames.jsonl。

segment 状态:

pending
fetching
fetched
sampling
sampled
failed

验收标准:

能处理 1 小时录像
中断后重跑不会重复处理已完成 segment
失败 segment 有 retry_count 和 last_error

Day 3实现 Clip 构建和模型推理

任务:

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。

验收标准:

1 小时视频生成 360 个 10 秒 clip
模型完成 clip 级推理
JSON 可解析率 > 95%
RTX 3080 20GB 不 OOM

Day 4实现聚合、容错和性能统计

任务:

1. 实现 clip_results.jsonl 到 store_result.json 的聚合。
2. 增加 JSON schema 校验。
3. 解析失败的 clip 重试一次。
4. 统计下载、抽帧、推理耗时。
5. 记录 GPU 峰值显存。

需要记录的指标:

download_time_seconds
decode_time_seconds
frames_per_second
clips_per_second
gpu_memory_peak_mb
failed_segments
failed_clips
parse_failed_clips

验收标准:

1 小时完整链路跑通
生成 store_result.json
失败任务可断点续跑

Day 5跑完整 24 小时

任务:

1. 跑单路摄像头 24 小时。
2. 跑双路摄像头 24 小时。
3. 记录全链路耗时。
4. 固化 PoC 参数。
5. 输出失败清单和优化建议。

最终参数需要确认:

clip_length
stride
frames_per_clip
batch_size
image_size
raw segment 是否保留
frames 保留天数

验收标准:

单门店 1-2 路摄像头 24 小时离线分析完成
输出门店级 JSON
有可复跑的 segment 和 clip 状态

5. 关键设计决策说明

5.1 先单机跑通

PoC 只做 1 家门店,单机单 GPU 足够验证链路。

不建议一开始引入:

Kafka
Spark
Kubernetes
复杂分布式任务系统

PoC 阶段使用:

本地文件系统
SQLite 或 JSONL manifest
单进程或少量 worker

5.2 使用磁盘帧缓存

原因:

1. 方便调试模型输入。
2. 模型调参时不用重复拉视频。
3. 海康 playback_url 过期后不影响后续实验。
4. 支持断点续跑。

后续链路稳定后,可以优化为:

FFmpeg pipe -> frame queue -> model worker

但 PoC 不优先做。

5.3 默认 10 秒 clip

10 秒 clip 的优势:

语义粒度足够细
事件定位较准确
推理量可控
方便聚合

如果吞吐不够,切到 20 秒 clip

clip 数量减少 50%
事件时间定位精度下降

5.4 小 batch 推理

RTX 3080 20GB 显存有限。

建议从保守参数开始:

batch_size = 1
frames_per_clip = 8
image_size = 336 或 448

稳定后再提高 batch。

5.5 结构化输出优先

模型输出必须被工程系统消费。

因此:

输出 JSON
event_type 枚举
schema 校验
失败重试
保留 raw_response

不要依赖自然语言总结作为主输出。

6. 故障与容错设计

6.1 RTSP / 回放 URL 失败

处理方式:

1. 重新获取 playback_url。
2. 使用 -rtsp_transport tcp。
3. 将 1 小时 segment 拆成 30 或 15 分钟。
4. 最多重试 3 次。
5. 仍失败则标记 failed不阻塞其他 segment。

6.2 解码失败

处理方式:

1. 使用 ffprobe 获取 codec。
2. H.264 使用 h264_cuvid。
3. H.265 使用 hevc_cuvid。
4. 失败后缩短 segment 重试。
5. 仍失败则记录 ffmpeg stderr。

由于项目约束要求必须 GPU 解码,不建议默认 fallback 到 CPU 解码。

6.3 模型 OOM

处理顺序:

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 后应:

torch.cuda.empty_cache()
记录失败 clip
用更小配置重试

6.4 JSON 解析失败

处理方式:

1. 严格 prompt 重试一次。
2. 仍失败则 status = parse_failed。
3. 保存 raw_response。
4. 聚合层跳过该 clip。
5. 门店级结果记录 failed_clip_count。

6.5 断点续跑

每层都要有 manifest

segments.sqlite:
  记录每个视频段状态

frames.jsonl:
  记录每张帧和时间戳

clips.jsonl:
  记录每个 clip 和输入帧

clip_results.jsonl:
  记录每个 clip 的推理结果

重跑逻辑:

已有 sampled segment 不重复抽帧
已有 inferred clip 不重复推理
只补 failed / missing / parse_failed

7. 可扩展性设计

7.1 从 1 家门店到 26 家门店

任务分片方式:

store_id + camera_id + business_date

26 家门店规模:

26 家 * 1-2 路摄像头 = 26-52 路摄像头

如果每路 24 小时需要 3-10 GPU 小时:

每日全量处理需要 78-520 GPU 小时

单张 RTX 3080 不适合长期处理 26 家全量 24 小时视频。

7.2 单机阶段

结构:

1 个 downloader
1 个 ffmpeg sampler
1 个 model worker
1 个 aggregator

建议:

下载和抽帧可以提前跑
模型 worker 独占 GPU
不要多个模型进程抢同一张 3080

7.3 单机多 GPU 阶段

结构:

1 个调度进程
N 个 GPU worker
每张 GPU 一个模型实例

任务分配:

按 camera-day 分配给不同 GPU
每个 GPU worker 独立读取 clip manifest

7.4 多机多 GPU 阶段

最小生产化结构:

Postgres:
  任务状态表

对象存储 / NAS:
  原始视频、帧、结果

多台 GPU 服务器:
  拉取任务
  独立处理
  写回状态

暂不需要:

Kafka
Spark
复杂实时流处理

8. 风险与解决方案

风险 1海康云眸回放速度受限

影响:

如果只能 1x 回放24 小时视频至少需要 24 小时获取。

解决:

PoC 第一天验证是否支持快速下载历史录像。
优先使用云录像文件下载 URL。
如果只能 RTSP 回放,需要重新评估每日处理窗口。

风险 2RTX 3080 显存不足

影响:

4B LoRA + 多帧输入可能 OOM。

解决:

batch_size 从 1 开始。
frames_per_clip 从 8 开始。
image_size 从 336 或 448 开始。
必要时使用 8bit / 4bit 量化。

风险 3模型对门店事件识别不稳定

影响:

输出事件类型不一致,难以聚合。

解决:

固定 event_type 枚举。
固定 JSON schema。
在 prompt 中要求没有事件时返回 events: []。
保存证据帧用于人工复核。

风险 4抽帧结果和真实时间不对齐

影响:

事件时间戳错误。

解决:

每个 segment 必须记录 segment_start。
帧时间戳由 segment_start + frame_index 秒计算。
如 FFmpeg 能输出 pts_time则优先使用 pts_time。

风险 5磁盘占用增长过快

影响:

24 小时、多摄像头 JPEG 帧会占用大量 SSD。

解决:

raw segment 抽帧成功后删除。
frames 推理完成后保留 1-7 天。
只长期保留结果 JSON 和事件证据帧。

9. 推荐 PoC 默认参数

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 结束时应交付:

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 范围:

实时告警
Web 管理后台
复杂多租户权限
分布式调度平台
自动模型训练闭环