本章节介绍一个基于 GK7206 的区域运动检测应用示例 —— rpi-detector。该应用通过板端 MPP 视频管线采集摄像头画面,以 MJPEG 流推送至浏览器,并在浏览器端实现可交互的 ROI(Region of Interest,感兴趣区域)运动检测——用户可拖拽、缩放检测区域,实时监控画面变化。同时板端也基于帧大小变化进行辅助运动检测。
应用源码位于 SDK 目录 app_sample/rpi-detector/,涵盖了视频采集、双通道编码(JPEG 快照 + MJPEG 流)、按需启停的视频管线、以及浏览器端像素级运动检测的完整链路,是一个不依赖 NPU 的纯软件视觉应用参考模板。
| 参数 | 值 |
|---|---|
传感器分辨率 |
根据实际 Sensor 型号(如 2560 × 1440) |
MJPEG 流分辨率 |
640 × 480 |
JPEG 快照分辨率 |
与传感器原始分辨率相同 |
视频帧率 |
30 FPS(由 Sensor 决定) |
Web 服务端口 |
80 |
检测采样分辨率 |
160 × 120(浏览器端降采样) |
默认检测阈值 |
20%(可调 1%~80%) |
ROI 默认区域 |
画面中心 50% × 50% |
NPU 使用 |
不使用(纯软件方案) |
app_sample/rpi-detector/
├── Makefile # 构建脚本
├── src/
│ ├── main.c # 主程序(Web 路由处理、GPIO 初始化)
│ └── rpi_detector_mjpeg.c # 视频管线管理、MJPEG 流推送、运动检测
└── web/
├── index.html # Web 前端页面
├── app.js # 前端逻辑(ROI 控制、像素级运动检测)
└── style.css # 页面样式
| 特性 | rpi-detector | face_recognize |
|---|---|---|
| AI/NPU 推理 | 不使用 | 使用 MTCNN 三阶段网络 |
| 运动检测方式 | 帧间像素差异 + 帧大小变化 | NPU 人脸检测 |
| VENC 通道 | 2 个(JPEG 快照 + MJPEG 流) | 1 个(MJPEG 流) |
| 检测位置 | 浏览器端(Canvas 像素对比) | 板端(NPU 推理线程) |
| 管线生命周期 | 按需启停(跟随浏览器连接) | 启动时创建、退出时销毁 |
| 适用场景 | 入侵检测、安防监控、画面变化告警 | 人脸检测、关键点定位 |
在编译本应用之前,请确保已完成以下准备工作:
out/ 目录进入应用目录,使用 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 的编译规则,自动配置交叉编译器、头文件路径和链接库。无需手动设置编译工具链。
编译成功后,在当前目录下生成可执行文件 rpi_detector。
相关信息
由于应用程序体积较大,需挂载 SD 卡后运行。
mkdir -p /sd_card
mount /dev/mmcblk1p1 /sd_card
使用 SCP 将以下文件传输到开发板:
# 在开发主机上执行
# 1. 传输可执行文件
scp 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/
通过 SSH 登录开发板,执行以下命令:
# 登录开发板
ssh root@<板端IP>
# 进入应用目录
cd /sd_card/
# 添加执行权限(首次)
chmod +x rpi_detector
# 运行(默认阈值 20)
./rpi_detector
# 或指定初始运动检测阈值(1~80)
./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.
在电脑浏览器中打开 http://<板端IP>/,即可看到运动检测界面:
在终端按 Ctrl+C 发送 SIGINT 信号即可正常退出。应用会等待所有 MJPEG 流连接关闭后释放视频管线资源。
应用的整体架构遵循「视频采集 → 双通道编码 → Web 展示 + 浏览器端运动检测」的管线:

本应用采用 板端 + 浏览器端 的双层运动检测架构:
| 层级 | 位置 | 检测方法 | 特点 |
|---|---|---|---|
| 板端检测 | MJPEG 流推送循环 | 帧大小变化 + 动态基线校准 | 粗粒度,抗 JPEG 编码抖动,需连续 3 帧异常 |
| 浏览器端检测 | Canvas 像素比较 | ROI 区域逐像素 RGB 差异 | 细粒度,用户可自定义 ROI 区域,250ms 采样周期 |
这种设计的优势是:无人查看时零资源占用,有人连接时即时启动。
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 供电
当首个浏览器请求 /mjpeg 时,rpi_detector_mjpeg_init_pipeline() 按以下顺序初始化管线:
ochn0:原始分辨率 → JPEG 快照编码ochn1:缩放到 640 × 480 → MJPEG 流编码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 流)
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();
}
板端的运动检测在 MJPEG 流推送循环中执行(update_motion_stats()),基于帧大小变化进行分析:
设计要点:
浏览器端的运动检测在 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% | 默认检测区域 |
浏览器端的 ROI 检测框支持以下交互操作:
localStorage,刷新页面后恢复应用持续缓存最新一帧 JPEG 图像到内存中,通过以下 URL 获取:
/api/snapshot → 返回当前缓存的 JPEG 快照
/snapshot.jpg → 同上(别名)
/api/frame.jpg → 同上(别名)
快照缓存使用互斥锁保护(g_snapshot_lock),确保读写安全。
应用通过 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
}
本节以 rpi-detector 为参考模板,讲解如何开发一个基于 GK7206 的视频采集 + Web 展示应用(不使用 NPU)。
步骤 1: 创建工程 → 复制 Makefile 模板 → 编写源码
步骤 2: GPIO/硬件初始化 → Sensor 供电、引脚配置
步骤 3: 初始化视频管线 → VI + VPSS + VENC 配置
步骤 4: 实现 MJPEG 流推送 → VENC 获取帧 → HTTP 分段推送
步骤 5: 实现业务逻辑 → 运动检测、快照缓存等
步骤 6: Web 前端 → HTML/JS/CSS 界面
参照 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)
参照 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; // 恒定码率
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);
}
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);
}
通过 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; // 未处理,交给默认静态文件服务
}
多个 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);
本应用采用帧大小变化作为板端运动检测信号,这是一种轻量但粗粒度的方法。开发者应注意:
-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;
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 浏览器画面黑屏 | 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 正常加载 |