01 区域运动检测应用
本章节介绍一个基于 GK7206 的区域运动检测应用示例 —— rpi-detector。该应用通过板端 MPP 视频管线采集摄像头画面,以 MJPEG 流推送至浏览器,并在浏览器端实现可交互的 ROI(Region of Interest,感兴趣区域)运动检测——用户可拖拽、缩放检测区域,实时监控画面变化。同时板端也基于帧大小变化进行辅助运动检测。
应用源码位于 SDK 目录 app_sample/rpi-detector/,涵盖了视频采集、双通道编码(JPEG 快照 + MJPEG 流)、按需启停的视频管线、以及浏览器端像素级运动检测的完整链路,是一个不依赖 NPU 的纯软件视觉应用参考模板。
1 应用概述
1.1 功能特性
- 实时 MJPEG 视频流:浏览器直接查看摄像头实时画面
- 浏览器端 ROI 运动检测:用户可拖拽、缩放检测区域(绿色矩形框),实时对比帧间像素变化
- 板端辅助运动检测:基于 MJPEG 帧大小变化的服务端运动事件检测,带动态基线校准
- JPEG 快照捕获:实时缓存最新帧,支持通过 API 或 URL 获取当前画面快照
- 可调检测阈值:通过 Web UI 滑块实时调整运动检测灵敏度(1%~80%)
- 按需启停管线:视频管线仅在浏览器连接时启动,断开后自动释放硬件资源
- Sensor GPIO 控制:启动时自动配置 Sensor 供电 GPIO
1.2 技术参数
| 参数 | 值 |
|---|---|
| 传感器分辨率 | 根据实际 Sensor 型号(如 2560 × 1440) |
| MJPEG 流分辨率 | 640 × 480 |
| JPEG 快照分辨率 | 与传感器原始分辨率相同 |
| 视频帧率 | 30 FPS(由 Sensor 决定) |
| Web 服务端口 | 80 |
| 检测采样分辨率 | 160 × 120(浏览器端降采样) |
| 默认检测阈值 | 20%(可调 1%~80%) |
| ROI 默认区域 | 画面中心 50% × 50% |
| NPU 使用 | 不使用(纯软件方案) |
1.3 目录结构
app_sample/rpi-detector/
├── Makefile # 构建脚本
├── src/
│ ├── main.c # 主程序(Web 路由处理、GPIO 初始化)
│ └── rpi_detector_mjpeg.c # 视频管线管理、MJPEG 流推送、运动检测
└── web/
├── index.html # Web 前端页面
├── app.js # 前端逻辑(ROI 控制、像素级运动检测)
└── style.css # 页面样式1.4 与人脸检测应用的区别
| 特性 | rpi-detector | face_recognize |
|---|---|---|
| AI/NPU 推理 | 不使用 | 使用 MTCNN 三阶段网络 |
| 运动检测方式 | 帧间像素差异 + 帧大小变化 | NPU 人脸检测 |
| VENC 通道 | 2 个(JPEG 快照 + MJPEG 流) | 1 个(MJPEG 流) |
| 检测位置 | 浏览器端(Canvas 像素对比) | 板端(NPU 推理线程) |
| 管线生命周期 | 按需启停(跟随浏览器连接) | 启动时创建、退出时销毁 |
| 适用场景 | 入侵检测、安防监控、画面变化告警 | 人脸检测、关键点定位 |
2 编译与部署
2.1 前置条件
在编译本应用之前,请确保已完成以下准备工作:
- SDK 环境已搭建:参照 SDK 编译 完成交叉编译工具链和 SDK 配置
- SDK 已完整编译一次:应用依赖 SDK 的公共库和头文件,需先执行完整编译生成
out/目录
2.2 编译应用
进入应用目录,使用 SDK 构建系统编译:
# 进入应用目录
cd <SDK_PATH>/app_sample/rpi-detector
# 编译(SDK Makefile 会自动使用交叉编译工具链)
make clean && make编译原理
Makefile 通过 include $(SDK_DIR)/build/base.mk 和 include $(SAMPLE_DIR)/sample_base.mk 引入 SDK 的编译规则,自动配置交叉编译器、头文件路径和链接库。无需手动设置编译工具链。
编译成功后,在当前目录下生成可执行文件 project_02_rpi_detector。
2.3 部署到板端
相关信息
由于应用程序体积较大,需挂载 SD 卡后运行。
mkdir -p /sd_card
mount /dev/mmcblk1p1 /sd_card使用 SCP 将以下文件传输到开发板:
# 在开发主机上执行
# 1. 传输可执行文件
scp project_02_rpi_detector root@<板端IP>:/sd_card/
# 2. 传输 Web 前端文件
scp web/* root@<板端IP>:/www/前端文件部署
Web 前端文件需放置在板端 Web 服务器的静态文件根目录中(默认为 /www/)。可通过以下命令部署:
ssh root@<板端IP> "mkdir -p /www"
scp web/* root@<板端IP>:/www/2.4 运行应用
通过 SSH 登录开发板,执行以下命令:
# 登录开发板
ssh root@<板端IP>
# 进入应用目录
cd /sd_card/
# 添加执行权限(首次)
chmod +x project_02_rpi_detector
# 运行(默认阈值 20)
./project_02_rpi_detector
# 或指定初始运动检测阈值(1~80)
./project_02_rpi_detector 30启动后,终端会输出以下日志信息:
=== rpi-detector: Motion Detector for GK7206 ===
Web UI available at http://<board-ip>/
Motion threshold: 20
Starting web server... Camera pipeline will start when browser opens /mjpeg.2.5 浏览器访问
在电脑浏览器中打开 http://<板端IP>/,即可看到运动检测界面:
- 左侧:摄像头实时预览画面(MJPEG 流),覆盖一个绿色矩形 ROI 检测框
- ROI 框操作:
- 拖拽框体中心区域 → 移动检测区域
- 拖拽四角圆形手柄 → 缩放检测区域
- 点击「重置检测框」按钮 → 恢复默认位置和大小
- 右侧控制面板:
- 阈值滑块(1%~80%):调整运动检测灵敏度
- 当前变化百分比:实时显示 ROI 区域内像素变化率
- 触发次数:累计运动告警次数
- 最近告警:显示告警历史
- 检测触发:当 ROI 区域内像素变化超过阈值时,检测框会短暂闪红,并记录一次告警
2.6 停止应用
在终端按 Ctrl+C 发送 SIGINT 信号即可正常退出。应用会等待所有 MJPEG 流连接关闭后释放视频管线资源。
3 系统架构
3.1 整体数据流
应用的整体架构遵循「视频采集 → 双通道编码 → Web 展示 + 浏览器端运动检测」的管线:

3.2 双层运动检测机制
本应用采用 板端 + 浏览器端 的双层运动检测架构:
| 层级 | 位置 | 检测方法 | 特点 |
|---|---|---|---|
| 板端检测 | MJPEG 流推送循环 | 帧大小变化 + 动态基线校准 | 粗粒度,抗 JPEG 编码抖动,需连续 3 帧异常 |
| 浏览器端检测 | Canvas 像素比较 | ROI 区域逐像素 RGB 差异 | 细粒度,用户可自定义 ROI 区域,250ms 采样周期 |
这种设计的优势是:无人查看时零资源占用,有人连接时即时启动。
4 内部执行逻辑详解
4.1 启动流程
main() 函数的启动流程分为 4 个阶段:
// 阶段 1:配置 Sensor 供电 GPIO
enable_sensor_gpio(); // xmmm 配置寄存器 + GPIO46 导出、设为输出、拉高
// 阶段 2:设置运动检测阈值(可从命令行参数获取)
detector_set_motion_threshold(initial_threshold);
// 阶段 3:注册 MJPEG 处理器
web_server_set_mjpeg_handler(rpi_detector_mjpeg_send_stream,
rpi_detector_mjpeg_request_stop);
// 阶段 4:启动 Web 服务器(阻塞,等待浏览器连接)
web_server_run(rpi_detector_route_get);
// 退出时清理
rpi_detector_mjpeg_shutdown(); // 等待所有连接关闭,释放管线Sensor GPIO 初始化
应用通过 enable_sensor_gpio() 函数完成两步操作:
- 使用
xmmm工具配置 Sensor 电源管理寄存器(0x100C0044 = 0x00001000) - 通过 sysfs 导出 GPIO46,设为输出模式并拉高电平,为 Sensor 供电
4.2 视频管线初始化
当首个浏览器请求 /mjpeg 时,rpi_detector_mjpeg_init_pipeline() 按以下顺序初始化管线:
系统初始化:配置 VB 内存池
- Pool 0:VI 采集缓冲区(传感器原始分辨率)
- Pool 1:VPSS 全分辨率 / VENC 编码缓冲区
- Pool 2:VPSS 子码流缓冲区(640 × 480)
模块初始化:依次初始化 VI、VPSS、VENC 模块
ISP 初始化:配置图像信号处理参数
VI + ISP 启动:启动视频输入
VPSS 配置:配置两个输出通道
ochn0:原始分辨率 → JPEG 快照编码ochn1:缩放到 640 × 480 → MJPEG 流编码
VENC 配置:双通道编码
chn0:JPEG 模式(全分辨率,用于快照)chn1:MJPEG CBR 模式(640 × 480,用于流推送)
绑定关系:
VI(pipe=0, chn=0) ──bind──→ VPSS(pipe=0, ichn=0)
├→ ochn0 ──bind──→ VENC chn0 (JPEG 快照)
└→ ochn1 ──bind──→ VENC chn1 (MJPEG 流)4.3 MJPEG 流推送
rpi_detector_mjpeg_send_stream() 是 MJPEG 流推送的核心函数,在浏览器请求 /mjpeg 时被调用:
void rpi_detector_mjpeg_send_stream(int client_fd)
{
// 1. 按需初始化管线(首个连接时启动)
rpi_detector_mjpeg_init_pipeline();
g_stream_ref_count++;
// 2. 发送 MJPEG multipart 头
send("HTTP/1.1 200 OK\r\nContent-Type: multipart/x-mixed-replace; boundary=frame\r\n...");
// 3. 循环获取 VENC 帧
while (!g_stream_stop_requested) {
// 3.1 等待 VENC 编码完成
xmedia_venc_select(venc_mask, &timeout);
xmedia_venc_query_status(g_venc_chn, &stat);
xmedia_venc_get_stream(g_venc_chn, &stream, -1);
// 3.2 板端运动检测(帧大小变化分析)
update_motion_stats(&stream, frame_len);
// 3.3 更新快照缓存
update_snapshot_cache(&stream, frame_len);
// 3.4 推送 JPEG 帧到浏览器
send("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: ...\r\n\r\n");
send(jpeg_data);
send("\r\n");
// 3.5 释放帧
xmedia_venc_release_stream(g_venc_chn, &stream);
}
// 4. 引用计数 -1,最后一个连接退出时销毁管线
g_stream_ref_count--;
if (g_stream_ref_count == 0) rpi_detector_mjpeg_deinit_pipeline();
}4.4 板端运动检测算法
板端的运动检测在 MJPEG 流推送循环中执行(update_motion_stats()),基于帧大小变化进行分析:

设计要点:
- 帧大小作为运动信号:MJPEG 编码下,画面内容变化会导致压缩后帧大小显著改变,这是一个无需解码的轻量级检测方法
- 双条件过滤:同时要求帧大小偏离基线 且 相邻帧间有跳变,避免渐变场景的误触发
- 连续帧确认:需连续 3 帧异常才触发告警,避免偶发噪声
- 动态基线:基线通过 EMA 持续缓慢更新,适应光照渐变等缓慢场景变化
4.5 浏览器端 ROI 运动检测
浏览器端的运动检测在 app.js 中实现,使用 Canvas 对 MJPEG 流进行降采样后逐像素比较:
function detectMotionFrame() {
// 1. 将 MJPEG 图像绘制到 160×120 Canvas(降采样)
ctx.drawImage(img, 0, 0, 160, 120);
const frame = ctx.getImageData(0, 0, 160, 120).data;
// 2. 获取 ROI 区域在降采样坐标中的位置
const sampleRoi = getRoiSampleRect(img);
// 3. 逐像素比较 ROI 区域内的 RGB 差异
for (y, x in ROI) {
dr = |frame[i].R - prev[i].R|
dg = |frame[i].G - prev[i].G|
db = |frame[i].B - prev[i].B|
if ((dr + dg + db) / 3 > 42) // 单像素变化阈值
changed++
}
// 4. 计算变化率
ratio = (changed / total_pixels) * 100
// 5. 与用户阈值比较,超过则上报
if (ratio >= motionThreshold)
reportMotion(ratio) // POST /api/motion/trigger
}关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
SAMPLE_WIDTH × SAMPLE_HEIGHT | 160 × 120 | 降采样分辨率,平衡精度与性能 |
PIXEL_DIFF_THRESHOLD | 42 | 单像素 RGB 平均差异阈值 |
DETECTION_INTERVAL_MS | 250 | 检测周期(毫秒) |
TRIGGER_COOLDOWN_MS | 1200 | 触发冷却时间,防止重复告警 |
DEFAULT_ROI | 中心 50% × 50% | 默认检测区域 |
4.6 ROI 交互控制
浏览器端的 ROI 检测框支持以下交互操作:
- 整体拖拽:按住框体中心区域拖动,移动检测区域位置
- 四角缩放:拖拽四角的圆形手柄(nw/ne/sw/se),调整检测区域大小
- 位置持久化:ROI 位置和大小自动保存到
localStorage,刷新页面后恢复 - 最小尺寸限制:ROI 最小为 8% × 8%,防止误操作缩放到不可见
4.7 快照功能
应用持续缓存最新一帧 JPEG 图像到内存中,通过以下 URL 获取:
/api/snapshot → 返回当前缓存的 JPEG 快照
/snapshot.jpg → 同上(别名)
/api/frame.jpg → 同上(别名)快照缓存使用互斥锁保护(g_snapshot_lock),确保读写安全。
4.8 Web API 接口
应用通过 rpi_detector_route_get() 注册了以下自定义 API 路由:
| 路由 | 方法 | 功能 | 参数 |
|---|---|---|---|
/api/status | GET | 获取当前状态(运行状态、帧率、阈值、告警次数等) | — |
/api/motion/threshold?val=N | GET | 设置运动检测阈值 | val: 1~80 |
/api/motion/trigger?ratio=N | GET | 手动记录一次浏览器端运动告警 | ratio: 变化率 |
/api/motion/reset | GET | 重置告警计数 | — |
/api/alerts | GET | 获取告警列表摘要 | — |
/api/snapshot | GET | 获取当前 JPEG 快照 | — |
/snapshot.jpg | GET | 快照别名 | — |
/api/status 返回的 JSON 格式:
{
"running": true,
"fps": 30,
"motion_threshold": 20,
"motion_count": 5,
"last_motion_time": "2026-06-08 14:30:25",
"last_frame_time": "2026-06-08 14:31:02",
"stream_url": "/mjpeg",
"has_frame": true,
"frame_size": 15384,
"frame_mtime": 1749354662
}5 如何编写类似应用
本节以 rpi-detector 为参考模板,讲解如何开发一个基于 GK7206 的视频采集 + Web 展示应用(不使用 NPU)。
5.1 开发步骤总览
步骤 1: 创建工程 → 复制 Makefile 模板 → 编写源码
步骤 2: GPIO/硬件初始化 → Sensor 供电、引脚配置
步骤 3: 初始化视频管线 → VI + VPSS + VENC 配置
步骤 4: 实现 MJPEG 流推送 → VENC 获取帧 → HTTP 分段推送
步骤 5: 实现业务逻辑 → 运动检测、快照缓存等
步骤 6: Web 前端 → HTML/JS/CSS 界面5.2 步骤 1:创建工程
参照 rpi-detector 的目录结构创建新工程:
mkdir -p my_app/src my_app/web编写 Makefile(可直接复制并修改):
ifeq ($(CFG_SDK_EXPORT_FLAG),)
SDK_DIR := $(shell cd $(CURDIR)/../.. && /bin/pwd)
endif
include $(SDK_DIR)/build/base.mk
include $(SAMPLE_DIR)/sample_base.mk
TARGET := my_app # 修改为你的应用名
# 根据需要选择链接库:
# - 视频采集: 不需要额外库 (SAMPLE_LIBS 已包含)
# - IVE/MD: -lxmedia_ive -lxmedia_md (硬件运动检测/图像处理)
# - NPU: -lxmedia_svp -lxmedia_npu
LIBS := $(SAMPLE_LIBS) $(SAMPLE_COMMON_LIB) -lpthread
INCLUDES := $(SAMPLE_INCLUDES)
INCLUDES += -I$(SDK_DIR)/project/common # 如果使用 web_server.c
CFLAGS := $(SAMPLE_CFLAGS) $(LIBS) $(INCLUDES)
SRCS := $(wildcard src/*.c) $(SDK_DIR)/project/common/web_server.c
OBJS := $(patsubst %.c, %.o, $(SRCS))
.PHONY: all clean
all: $(OBJS)
$(AT)$(CC) -o $(TARGET) $^ $(CFLAGS)
%.o : %.c
$(AT)$(CC) -c -o $@ $< $(CFLAGS)
clean:
$(AT)rm -rf $(OBJS) $(TARGET)5.3 步骤 2:视频管线配置要点
参照 rpi_detector_mjpeg_init_pipeline() 的初始化流程,关键配置项:
// 1. 视频参数
video_param.pixel_fmt = XMEDIA_VIDEO_PIXEL_FMT_YVU_SEMIPLANAR_420; // NV21
video_param.data_width = XMEDIA_VIDEO_DATA_WIDTH_8; // 8-bit
// 2. 工作模式(全离线模式,最稳定)
sys_config.sys_conf.pipe_mode[0].vicap_viproc_mode = XMEDIA_WORK_MODE_OFFLINE;
sys_config.sys_conf.pipe_mode[0].viproc_vpss_mode = XMEDIA_WORK_MODE_OFFLINE;
sys_config.sys_conf.pipe_mode[0].gdc_vpss_mode = XMEDIA_WORK_MODE_OFFLINE;
// 3. VPSS 输出通道(根据需要配置 1~N 个)
// ochn0: 全分辨率 → JPEG 快照
// ochn1: 缩放分辨率 → MJPEG 流
// 4. VENC 通道配置
// JPEG 模式(快照):
g_venc_cfg.chn_info[0].payload_type = PT_JPEG;
g_venc_cfg.chn_info[0].support_dcf = XMEDIA_TRUE; // 支持 EXIF
// MJPEG 模式(流):
g_venc_cfg.chn_info[1].payload_type = PT_MJPEG;
g_venc_cfg.chn_info[1].rc_mode = VENC_RC_MODE_MJPEGCBR; // 恒定码率5.4 步骤 3:实现按需管线管理
rpi-detector 的按需启停模式是一个实用的设计模式,核心逻辑:
static volatile int g_stream_ref_count = 0;
static pthread_mutex_t g_stream_lock = PTHREAD_MUTEX_INITIALIZER;
// 客户端连接时调用
void mjpeg_send_stream(int client_fd) {
pthread_mutex_lock(&g_stream_lock);
if (g_stream_ref_count == 0) {
// 首个连接 → 初始化管线
init_pipeline();
}
g_stream_ref_count++;
pthread_mutex_unlock(&g_stream_lock);
// ... MJPEG 流推送循环 ...
pthread_mutex_lock(&g_stream_lock);
g_stream_ref_count--;
if (g_stream_ref_count == 0) {
// 最后一个连接断开 → 释放管线
deinit_pipeline();
}
pthread_mutex_unlock(&g_stream_lock);
}5.5 步骤 4:MJPEG 流推送模板
MJPEG 流推送的标准模式(HTTP multipart/x-mixed-replace):
// 1. 发送 HTTP 响应头
send(client_fd,
"HTTP/1.1 200 OK\r\n"
"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
"Cache-Control: no-store\r\n"
"Connection: close\r\n\r\n");
// 2. 循环获取编码帧并推送
while (!stop_requested) {
// 等待 VENC 编码完成
xmedia_venc_select(venc_mask, &timeout);
xmedia_venc_query_status(venc_chn, &stat);
xmedia_venc_get_stream(venc_chn, &stream, -1);
// 计算帧总长度
size_t frame_len = 0;
for (i = 0; i < stream.pack_count; i++)
frame_len += stream.pack[i].len - stream.pack[i].offset;
// 发送帧头
char header[256];
snprintf(header, sizeof(header),
"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %zu\r\n\r\n",
frame_len);
send(client_fd, header, strlen(header), 0);
// 发送帧数据
for (i = 0; i < stream.pack_count; i++)
send(client_fd, stream.pack[i].vir_addr + stream.pack[i].offset,
stream.pack[i].len - stream.pack[i].offset, 0);
send(client_fd, "\r\n", 2, 0);
// 释放帧
xmedia_venc_release_stream(venc_chn, &stream);
free(stream.pack);
}5.6 步骤 5:自定义 API 路由
通过 web_server_run() 注册自定义路由处理函数:
static int my_route_handler(int client_fd, const char *path)
{
char json_buf[512];
if (strcmp(path, "/api/my-endpoint") == 0) {
snprintf(json_buf, sizeof(json_buf),
"{\"status\":\"ok\",\"data\":%d}\n", my_data);
web_send_json_ok(client_fd, json_buf);
return 0; // 已处理
}
return -1; // 未处理,交给默认静态文件服务
}5.7 关键编程要点
互斥锁保护共享状态
多个 MJPEG 客户端可能并发访问共享数据,需要互斥锁保护:
// 管线启停锁
pthread_mutex_lock(&g_mjpeg_lock);
// 操作 g_stream_ref_count、g_inited 等
pthread_mutex_unlock(&g_mjpeg_lock);
// 快照缓存锁
pthread_mutex_lock(&g_snapshot_lock);
// 读写 g_snapshot_buf、g_snapshot_size
pthread_mutex_unlock(&g_snapshot_lock);帧大小运动检测的局限性
本应用采用帧大小变化作为板端运动检测信号,这是一种轻量但粗粒度的方法。开发者应注意:
- 优势:无需解码 JPEG、无需额外内存、计算量极小
- 局限:无法定位运动区域、对缓慢光照变化敏感、JPEG 编码参数波动可能导致误触发
- 改进方向:如需更精确的板端运动检测,可使用 SDK 提供的 MD(Motion Detect)模块(
-lxmedia_md)或 IVE 模块(-lxmedia_ive)进行硬件加速的帧差分析
错误处理与资源释放
管线初始化使用 goto 链式错误处理,确保任何步骤失败都能正确释放已分配的资源:
ret = sample_comm_isp_init(...); if (ret) goto exit0;
ret = sample_comm_vi_start(...); if (ret) goto exit1;
ret = sample_comm_vpss_start(...); if (ret) goto exit2;
ret = sample_comm_venc_start(...); if (ret) goto exit3;
// ...
return XMEDIA_SUCCESS;
exit3: sample_comm_vpss_stop(...);
exit2: sample_comm_vi_stop(...);
exit1: sample_comm_isp_stop(...);
exit0: sample_comm_isp_exit(...);
rpi_detector_mjpeg_sys_exit();
return ret;5.8 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 浏览器画面黑屏 | Sensor GPIO 未正确初始化 | 检查 enable_sensor_gpio() 返回值,确认 GPIO46 可用 |
| MJPEG 流连接后断开 | VENC 编码失败或超时 | 检查 VENC 配置参数、VB Pool 大小和数量是否足够 |
| 快照返回 404 | 首个 MJPEG 连接前无快照 | 先访问 /mjpeg 启动管线,等待几帧后再请求快照 |
| 运动检测误触发频繁 | JPEG 编码抖动导致帧大小波动 | 增大阈值(如 30~50),或修改 trigger_delta 最小值 |
| 运动检测不触发 | 阈值过高或 ROI 区域过小 | 降低阈值、扩大 ROI 区域、检查画面是否在变化 |
| 多个浏览器连接时管线异常 | 并发访问管线状态未加锁 | 参照 g_mjpeg_lock 和 g_stream_ref_count 的互斥保护 |
| 管线初始化失败(返回非零错误码) | VB 内存不足或模块冲突 | 检查是否有其他程序占用 VI/VPSS/VENC 资源 |
| 浏览器端 ROI 检测框无法拖拽 | CSS 层级冲突或 JS 未加载 | 检查浏览器控制台错误,确认 app.js 正常加载 |
