智能摄像头嵌入式编程:实现本地移动侦测与报警事件快速上传
摄像头“移动侦测”最怕两件事:像素一变就报,手机被树叶晃到轰炸;算法和上传耦合,网络一抖事件丢了。嵌入式端正确做法是采集线程出帧→算法线程抽帧做帧差→事件队列去抖→上报线程发MQTT,本地先判定,网络上只传“事件”,不传视频流。
一、别全分辨率跑算法
1080p逐帧算帧差,MIPS直接爆。工程上抽帧+缩放:
采集走V4L2/MIPICSI,取YUV或MJPEG解码出Y通道
缩到320×240或160×120足够
5~10fps抽帧即可,家门口场景不需要30fps
只处理ROI区域,草坪/树冠直接屏蔽
#define MW 320
#define MH 240
static uint8_t prev[MW*MH];
static uint8_t bg[MW*MH];
int motion_blocks(uint8_t*cur, uint8_t*roi_mask,
int th, int min_area){
int hit=0, pix=0;
for(int y=0;y<MH;y++){
for(int x=0;x<MW;x++){
int i=y*MW+x;
if(roi_mask && !roi_mask[i]) continue;
int d = abs(cur[i]-bg[i]);
if(d>th){
pix++;
int bx=x/16, by=y/16;
hit |= (1<<(by*20+bx)%32); // 粗略块标记
}
}
}
return pix>=min_area ? pix : 0;
}
帧差阈值一般15~30,最小面积按场景标,门口人体几百像素起步。
二、背景建模别用“首帧定死”
纯帧差遇光照渐变会漏。嵌入式够用的是Surendra式更新:运动区保留旧背景,静止区慢追当前帧。
void bg_update(uint8_t*cur, uint8_t*mask, int alpha){
for(int i=0;i<MW*MH;i++){
if(mask[i]) continue; // 运动像素不更新
bg[i] = (bg[i]*(256-alpha) + cur[i]*alpha)>>8;
}
}
alpha取8~16,背景几秒跟一次,既能抗光漂移,又不会把慢速移动目标融进背景。
三、去抖:连续帧+冷却时间
单帧像素变化=飞虫、红外亮点、窗帘晃。 必须做两层过滤:
typedef struct {
int confirm_cnt;
uint32_t last_alarm_ms;
} motion_fsm_t;
int motion_decide(motion_fsm_t*m, int pix,
uint32_t now, int need_frames){
if(pix>0){
m->confirm_cnt++;
}else{
m->confirm_cnt=0;
}
if(m->confirm_cnt>=need_frames){ // 连续3帧
if(now-m->last_alarm_ms > COOLDOWN_MS){ // 冷却10s
m->last_alarm_ms = now;
m->confirm_cnt=0;
return 1;
}
}
return 0;
}
规则:
连续2~3帧确认,过滤瞬时干扰
同场景冷却10~30s,避免一个人走过去报5条
ROI分区独立计数,院子里树动不影响门口报警
高配才上人形分类;基础IPC用ROI+面积+连续帧,误报能压掉大半。
四、事件队列解耦算法与上传
算法线程别直接调mqtt_publish,网络阻塞会拖慢侦测。统一发事件:
typedef struct {
uint32_t seq;
uint32_t ts;
int pix;
uint8_t zone;
uint8_t snap; // 是否带抓拍
} alarm_evt_t;
QueueHandle_t evt_q;
void algo_task(void*p){
for(;;){
uint8_t*frame = get_small_y();
int pix = motion_blocks(frame, roi, TH, MIN_AREA);
if(motion_decide(&fsm, pix, now(), 3)){
alarm_evt_t e = {
.seq = ++g_seq,
.ts = rtc_sec(),
.pix = pix,
.zone= 0
};
xQueueSend(evt_q, &e, 0);
snap_request(); // 异步抓一帧JPEG
}
bg_update(frame, cur_mask, 12);
vTaskDelay(pdMS_TO_TICKS(100)); // 10fps
}
}
五、上报线程:QoS1+断网补传
报警事件不能丢,用MQTT QoS1;网络断了进本地环形缓冲,恢复后按seq补。
#define EVT_SLOTS 32
static alarm_evt_t ring[EVT_SLOTS];
void report_task(void*p){
alarm_evt_t e;
for(;;){
xQueueReceive(evt_q,&e,portMAX_DELAY);
char buf[96];
snprintf(buf,sizeof(buf),
"{\"seq\":%u,\"zone\":%d,\"pix\":%d,\"ts\":%u}",
e.seq,e.zone,e.pix,e.ts);
if(mqtt_connected()){
mqtt_publish_qos1("cam/01/motion", buf);
flush_ring(); // 补发断网期事件
}else{
ring[e.seq%EVT_SLOTS]=e; // 先缓存
}
}
}
重连别死循环秒重连,指数退避1/2/4/8…最大60s:
int backoff = 1000;
void on_disconnect(){
vTaskDelay(backoff);
mqtt_reconnect();
backoff = backoff*2>60000?60000:backoff*2;
}
补传限速,别一次性灌几十条把新事件堵住;报警类优先,状态类可丢。
六、快速上传的几个细节
抓拍异步:算法只发事件,JPEG抓拍由单独DMA/编码任务出,不占侦测周期
本地先响:蜂鸣器/GPIO联动在事件入队瞬间做,不等云端回执
LWT配置:设备掉线Broker发遗嘱,平台知道离线
KeepAlive适配:弱网RTT大时心跳别30s硬卡,否则误断连
时间戳对齐:RTC或NTP对时,补传事件别用到达时间
区域独立ROI掩码:门口/围墙开侦测,草坪/天空关掉
七、落坑清单
全分辨率30fps跑帧差→CPU满载掉帧
单帧变化直接报警→飞虫树叶狂推
算法线程里阻塞发MQTT→侦测卡死
断网事件不缓存→丢报警
重连固定1s→基站弱信号雪崩
没冷却时间→一人走过报5条
背景首帧写死→白天黄昏全误报
ROI不屏蔽绿植→户外必炸
只发像素变化不上区域→分不清人和猫
八、落地顺序
摄像头出Y帧,缩到320×240
ROI掩码配置(门口/围墙)
帧差+背景慢更新
运动像素统计+最小面积
连续帧确认+冷却时间
事件结构体入队列
上报线程MQTT QoS1
断网环形缓冲+退避重连+补传
抓拍异步+本地GPIO联动
本地移动侦测的本质不是“检测得有多聪明”,是先别被像素噪声骗;快速上传的本质不是“每次都立马发”,是事件进队列、网络层异步兜底。嵌入式摄像头稳不稳,就看算法任务和上报任务有没有切开——耦合在一起的项目,网络一抖要么丢报警要么卡侦测。基础帧差+ROI+连续帧确认,再加事件队列解耦,家用/商铺IPC够用;要再降误报才上人形分类,别一上来就怼模型。





