笔记:Orange Pi AI Pro 的端侧暴力行为检测系统(一)——概览
1. 项目背景与痛点
1.1 为什么要在端侧做打斗检测
暴力行为检测首先是一个视觉识别问题,但一旦进入监控、安防、校园、园区等实际场景,它很快就不再只是一个“分类准确率”问题,而是一个系统问题。将识别任务部署在边缘设备,而不是全部送往云端,主要有四个原因。
| 维度 | 云端集中处理 | 端侧处理 |
|---|---|---|
| 隐私合规 | 原始视频上云,数据暴露面更大 | 原始画面可留在本地,风险更低 |
| 响应延迟 | 依赖网络传输与服务端排队 | 本地直接推理,链路更短 |
| 带宽成本 | 连续视频上传成本高 | 只上传事件结果或摘要即可 |
| 可用性 | 网络异常会直接影响识别 | 弱网甚至离线情况下仍可工作 |
对于“打斗检测”这类任务,这四点不是锦上添花,而是刚需。因为系统一旦真正面对摄像头流,结果必须满足两个条件:
- 发现得足够快,否则警报没有意义。
- 误报不能太频繁,否则使用者很快就会失去信任。
换句话说,这类项目的本质不是“离线跑一个不错的分类模型”,而是“在有限算力下,把检测、判断、可视化、报警全部压进一条稳定的实时链路”。
1.2 从原始监控画面到结构化动作特征
监控视频中的暴力行为通常伴随着三个典型难点:
- 背景复杂,照明不稳定,画质往往较差。
- 人物尺度变化大,且可能被遮挡。
- 判别依据不在静态外观,而在连续动作模式。
这也是为什么本项目最终没有把“整张图像”直接送进一个视频分类大模型,而是先做人体姿态估计,再做时序动作识别。

图 1:同一监控场景下,左侧是输入视频帧,右侧是 YOLO11n-pose 提取人体框与关键点后的结果。可以看到,算法关注点已经从背景纹理转移到人体运动结构。
2. 核心算法选型:YOLO11n-pose + 轻量时序分类器
2.1 为什么不直接使用端到端视频大模型
如果只从论文效果出发,3D-CNN、SlowFast、VideoMAE、Video Swin Transformer 等端到端视频模型当然更“正统”。问题在于,项目不是部署在高端 GPU 服务器上,而是要最终落到 Orange Pi AI Pro 这类边缘硬件上。
算法选型必须服从硬件约束,而不能反过来要求硬件迁就模型。以工程视角来看,端到端视频大模型存在几个明显问题:
| 方案 | 优点 | 局限 |
|---|---|---|
| 3D-CNN / Video Transformer | 时空联合建模能力强 | 参数量大、显存和算力消耗高、端侧难以实时 |
| 直接图像分类 | 实现简单 | 只能看单帧,无法理解动作连续性 |
| 姿态估计 + 时序分类 | 特征维度低、结构清晰、便于部署 | 前处理设计要足够仔细,依赖姿态质量 |
这正是本项目采用级联架构的根本原因:
- 第一阶段负责把原始图像压缩成更抽象、更结构化的人体动作表示。
- 第二阶段只需要在较低维的序列特征上做时序判断。
- 这样做能显著降低分类器输入维度,减少板端实时推理的压力。
2.2 为什么选人体关键点,而不是直接吃 RGB 视频
在监控场景中,真正决定“是否发生暴力行为”的核心信息,并不是墙壁、桌椅、灯光或者背景纹理,而是人体姿态、肢体相对关系和短时间内的运动变化。人体关键点恰好能把这些信息提炼出来。
从表示学习角度看,姿态特征至少有三方面优势:
- 降噪:把背景和纹理细节大幅过滤掉,只保留与动作最相关的人体几何结构。
- 轻量:相比整帧图像,关键点序列的维度小得多,更适合端侧连续推理。
- 可解释:当模型输出“打斗”时,我们可以回头检查人体骨架变化,而不是面对不可解释的整图特征。
本项目中,单帧特征设计得非常明确:
- 每帧最多保留 3 个人。
- 每个人保留 17 个关键点。
- 每个关键点包含
x、y和confidence三个数值。
因此,单帧特征维度为:
d f r a m e = 3 × 17 × 3 = 153 d_{frame} = 3 \times 17 \times 3 = 153 dframe=3×17×3=153
而序列窗口长度固定为 41 帧,所以分类器输入张量可以写成:
X ∈ R 41 × 153 X \in \mathbb{R}^{41 \times 153} X∈R41×153
这个设计非常关键。它意味着后续的时序分类器不再需要理解完整图像,只需要理解“在 41 帧时间窗口内,这些关键点是如何变化的”。这正是轻量化的核心。
2.3 流水线是如何串起来的
从系统视角看,整条算法链路可以抽象为下面这个过程:
这条流水线背后的思想其实非常朴素:
- 先用检测器把“看图像”变成“看人”。
- 再用时序模型把“看人”变成“看动作变化”。
- 最后再用状态机与平滑策略把“看动作变化”变成“稳定报警”。
2.4 仓库当前实现中的时序分类器事实
尽管本系列标题沿用了 LSTM 的表述,但仓库当前可运行的主线实现实际上是 GRU。下面这段代码就是项目中用于导出 ONNX 的 PyTorch 版本核心结构,已经很能说明问题:
class TorchGRUClassifier(nn.Module):
def __init__(self, input_size=153, hidden_size=128, num_layers=2, dropout=0.2):
super().__init__()
self.gru = nn.GRU(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout,
)
self.head = nn.Sequential(
nn.Linear(hidden_size, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 1),
nn.Sigmoid(),
)
为什么这里选择的是 GRU 而不是更复杂的时序模型?原因依然和部署目标有关:
- 循环网络对固定长度姿态序列的建模已经足够。
- 相比更重的时序架构,GRU/LSTM 更容易导出、更容易迁移到 ONNX Runtime。
- 对这种“二分类 + 中等长度窗口”的任务而言,工程收益往往大于继续堆复杂度的理论收益。
所以,严格地说,项目的真正设计思想不是“某一个具体的 RNN 单元一定最优”,而是“在端侧约束下,采用一个足够轻、足够稳、足够容易迁移的时序分类器”。
2.5 为什么还需要平滑与状态机
很多初学者会把模型输出的每一帧概率直接拿来显示,这在 demo 里几乎一定会导致一个问题:抖动。
暴力行为本身是连续动作,但逐帧模型输出往往会受到姿态漏检、局部遮挡、单帧误判等因素影响。如果直接按瞬时概率做报警,界面会频繁在“打斗”和“非打斗”之间来回跳变。
因此,本项目在分类器之后又增加了一层非常关键的工程逻辑:
- 维护一个概率缓冲区,对最近若干帧做滑动平均。
- 只有平滑后的概率超过阈值,才认为当前进入打斗状态。
- 报警事件还需要加冷却时间,避免短时间内重复计数。

图 2:原始预测概率存在明显抖动,经过滑动平均后,红色曲线更加平稳,也更适合作为报警依据。
这一层逻辑非常“工程”,却恰恰决定了最终系统是否可用。很多时候,真正让项目从“能跑”变成“能演示、能部署、能说服人”的,并不是再多堆一层神经网络,而是把这种后处理细节做扎实。
2.6 选型不是拍脑袋:基线效果必须有证据
一个成熟的项目,算法选型不能只靠主观判断,还需要至少给出能够支撑决策的实验结果。本仓库中的测试集混淆矩阵就提供了一个非常直接的基线证据。

图 3:测试集混淆矩阵示意。图中可以读出 TN=185、FP=15、FN=10、TP=190。
由此可得:
A c c u r a c y = 185 + 190 185 + 15 + 10 + 190 = 93.75 % Accuracy = \frac{185 + 190}{185 + 15 + 10 + 190} = 93.75\% Accuracy=185+15+10+190185+190=93.75%
对于一个以人体关键点为输入、以轻量时序分类器为核心的端侧方案来说,这样的基线结果已经足以说明它不是“为了轻量而轻量”,而是在精度和部署性之间做了一次比较理性的平衡。
3. 工程架构设计:
3.1 本地原型和板端部署,本来就是两个世界
本项目从一开始就存在两类完全不同的需求。
| 阶段 | 目标 | 更优技术选择 |
|---|---|---|
| 本地开发期 | 快速验证算法链路、训练模型、调试参数、做界面原型 | Python |
| 板端部署期 | 压榨性能、降低延迟、稳定接入视频流、对接 ONNX Runtime / NPU | C++ |
这不是“语言之争”,而是任务分工。开发期最重要的是迭代速度,部署期最重要的是执行效率与可控性。
3.2 为什么后端选择 C++
在部署侧,视频处理主链路最终放在 C++ 中,核心原因有四个。
-
视频解码与内存控制更直接。连续视频流处理对内存分配、对象生命周期、缓冲策略都很敏感,C++ 更容易做细粒度控制。
-
更容易榨干 ONNX Runtime 与 NPU 后端性能。板端推理链路往往需要直接和动态库、Execution Provider、编译参数打交道,C++ 生态更加贴近底层。
-
更适合做多线程采集与推理解耦。当“采集速度”和“推理速度”不完全一致时,必须设计更稳的线程模型,避免延迟不断累积。
-
更适合承担事件录像、结果叠加、状态输出等系统级职责。这些事情单看都不难,但叠加起来以后,性能边界会非常明显。
-
通过 ONNX Runtime 接入 ONNX 分类模型与姿态模型。
-
提供了
cannExecution Provider 的配置入口,用于对接板端 NPU。 -
支持最新帧优先的实时流优化思路,尽量控制累积延迟。
-
在识别到事件时自动保存事件片段,兼顾演示效果与证据留存。
3.3 为什么前端仍然保留 Python
看到这里可能会有人问:既然后端都已经上 C++ 了,为什么前端不一起写成 C++?
答案很简单:没有必要。
在这个项目里,前端承担的是管理、展示和交互职责,而不是主推理职责。用 Python 做这件事有非常明显的优势:
- 本地原型阶段可以直接用 Streamlit 快速搭出可交互页面。
- 板端部署阶段可以用 Flask 很快做出一个轻量级 Web 仪表盘。
- Python 的 Web 生态更成熟,开发、修改、调试成本都更低。
也就是说,本项目并不是“前后端分离”的互联网系统意义上的前后端,而是“高性能推理后端 + 高效率展示前端”的职责分离。
3.4 当前部署版的 IPC 实现:
这部分是整个项目里最有工程味道的设计之一。
部署版本并没有让 Python 仪表盘通过 HTTP 轮询去索取整帧图像,也没有把每一帧都通过 WebSocket 做大对象传输。相反,C++ 侧会把缩放后的预览图和状态信息直接写到 /dev/shm 中,然后由 Flask 仪表盘读取并展示。
核心思想可以概括为下面这段伪代码式实现:
cv::Mat smallFrame;
cv::resize(frame, smallFrame, cv::Size(640, 360));
cv::imwrite("/dev/shm/preview.jpg", smallFrame);
std::ofstream statusFile("/dev/shm/status.json");
statusFile << "{\"isFight\": true, \"prob\": 0.82, \"label\": \"fight\"}";
然后 Python 侧的 Flask 服务读取这些内容,把它们转成浏览器可消费的 MJPEG 视频流与状态接口:
@app.route('/video_feed')
def video_feed():
return Response(generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame')
@app.route('/status')
def status():
return jsonify(json.load(open('/dev/shm/status.json', 'r')))
这里需要非常严谨地说明一件事:
/dev/shm本质上是 Linux 的内存文件系统,数据驻留在内存中,而不是机械硬盘或 SSD。- 因此,这种方案确实比常规磁盘文件交换高效得多,也很适合竞赛演示。
- 但如果从最严格的系统定义来说,这还不是“裸 mmap 级别的真正零拷贝共享内存块通信”,因为其中仍然有 JPEG 编码/解码与文件读写动作。
换句话说,当前部署方案已经非常接近“共享内存思路”的轻量实现,足以支撑比赛和展示;而如果后续继续追求极限延迟,还可以再进一步升级到 mmap 或 posix_ipc 形式的真正共享内存块方案。这也正是本系列第五篇要重点展开的内容。
3.5 为什么这种双栈架构是合理的
将整个系统拆成“C++ 负责实时主链路,Python 负责快速展示与管理”,本质上是在做一次工程上的资源最优配置:
- 把最吃性能、最需要确定性的部分交给 C++。
- 把最需要快速迭代、最需要界面表达的部分交给 Python。
- 用低开销的进程间数据交换把二者连接起来。
5. 本篇小结
到这里,第一篇要解决的问题基本已经完整回答:
-
为什么要做端侧暴力行为检测:因为它是一个隐私、延迟、带宽和稳定性共同约束下的系统问题。
-
为什么选 YOLO11n-pose + 轻量时序分类器:因为它在结构化表达、轻量部署和时序判别之间找到了很好的平衡点。
-
为什么工程上要采用 C++ + Python 双栈:因为本地研发与板端部署面对的是完全不同的最优目标,单一语言并不是最优解。
-
Kaggle 数据集:https://www.kaggle.com/datasets/mohamedmustafa/real-life-violence-situations-dataset
-
Ultralytics Pose 文档:https://docs.ultralytics.com/tasks/pose/
-
ONNX Runtime 官方文档:https://onnxruntime.ai/docs/
n 双栈**:因为本地研发与板端部署面对的是完全不同的最优目标,单一语言并不是最优解。
- Kaggle 数据集:https://www.kaggle.com/datasets/mohamedmustafa/real-life-violence-situations-dataset
- Ultralytics Pose 文档:https://docs.ultralytics.com/tasks/pose/
- ONNX Runtime 官方文档:https://onnxruntime.ai/docs/
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)