16、智能视频分析:YOLO物体检测、运动检测、区域入侵告警、WebRTC流中嵌入分析结果
这一章,我们来聊聊怎么让WebRTC流“看懂”画面。
说白了,就是给视频流装上大脑。让它不仅能传输画面,还能告诉你画面里有什么、发生了什么、有没有异常。我最早接触这个方向是在一个安防项目里,客户要求实时检测仓库里有没有人闯入禁区。那时候我就在想,能不能把AI检测和WebRTC揉到一起?后来发现,不仅能,而且效果还不错。
16.1 整体架构:AI分析引擎如何接入WebRTC管道
先看一张图,帮你快速建立整体认知。
整个流程其实不复杂。摄像头采集画面,通过WebRTC发送端推到SFU。SFU一方面转发给接收端,另一方面把视频帧交给AI分析引擎。分析完的结果,再叠加回视频流里。接收端看到的,就是带标注的智能画面了。
核心思路:AI分析不一定要在发送端做,也不一定要在接收端做。放在SFU侧,一次分析,多人共享,效率最高。我在项目中就吃过亏——最开始在每台接收端都跑YOLO,结果CPU直接拉满。后来改到SFU侧,问题迎刃而解。
16.2 YOLO物体检测:从帧提取到推理输出
YOLO,全称You Only Look Once。这名字起得好,看一眼就完事。它把目标检测当成回归问题,一次前向传播就能输出所有物体的位置和类别。
在WebRTC场景里,我们怎么用YOLO?
第一步,从视频流里取帧。WebRTC的VideoTrack本质上是一帧一帧的VideoFrame。我们可以通过track.onframe或者ReadableStream拿到原始帧数据。
// 从WebRTC轨道中提取视频帧
const videoTrack = pc.getReceivers()[0].track;
const processor = new MediaStreamTrackProcessor({ track: videoTrack });
const reader = processor.readable.getReader();
async function processFrames() {
while (true) {
const { done, value: frame } = await reader.read();
if (done) break;
// 将VideoFrame转换为Canvas可用的格式
const canvas = document.createElement('canvas');
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(frame, 0, 0);
// 送入YOLO模型推理
const detections = await runYOLO(canvas);
// 处理检测结果...
frame.close();
}
}
processFrames();
第二步,把Canvas图像传给YOLO模型。我习惯用ONNX Runtime Web或者TensorFlow.js来跑模型。YOLOv8的ONNX模型大概30MB,在浏览器里跑一次推理大约50-100ms,基本能跟上实时视频。
小技巧:不要每帧都推理。视频一般是30fps,但YOLO跑30次/秒太费资源。我一般设置跳帧策略——每3帧取1帧做检测,或者根据场景动态调整。静止画面可以降到1fps,运动剧烈时再升到10fps。
16.3 运动检测:简单高效的背景差分法
YOLO虽然强,但也不是万能的。有些场景不需要知道“是什么”,只需要知道“有没有动”。比如监控画面里,只要有人走过就触发告警。这时候运动检测比YOLO更轻量、更快速。
运动检测的核心算法是背景差分。说白了,就是拿当前帧减去背景帧,差值大的区域就是运动区域。
// 运动检测:背景差分法
class MotionDetector {
constructor(width, height, threshold = 30) {
this.width = width;
this.height = height;
this.threshold = threshold;
this.background = null;
}
// 更新背景(每隔N帧更新一次)
updateBackground(currentFrame) {
this.background = currentFrame.slice(); // 深拷贝
}
// 检测运动区域
detect(currentFrame) {
if (!this.background) {
this.updateBackground(currentFrame);
return [];
}
const diff = new Uint8Array(this.width * this.height);
let motionPixels = 0;
for (let i = 0; i < currentFrame.length; i++) {
const delta = Math.abs(currentFrame[i] - this.background[i]);
diff[i] = delta > this.threshold ? 255 : 0;
if (diff[i] === 255) motionPixels++;
}
// 计算运动比例
const motionRatio = motionPixels / (this.width * this.height);
// 动态更新背景(慢速适应光照变化)
for (let i = 0; i < currentFrame.length; i++) {
this.background[i] = this.background[i] * 0.95 + currentFrame[i] * 0.05;
}
return {
motionRatio,
motionMask: diff,
isMotion: motionRatio > 0.01 // 超过1%像素变化视为运动
};
}
}
这段代码看着简单,但实际效果很好。我曾在一个人脸门禁项目里用这个算法做活体检测的预处理——如果有人拿着照片晃动,运动检测能快速识别出异常。
注意:背景差分法对光照变化很敏感。如果场景里有灯开关、窗帘飘动,很容易误报。我的经验是:加一个光照补偿因子,或者用帧间差分法(连续两帧相减)代替背景差分。
16.4 区域入侵告警:划定禁区,智能触发
有了YOLO的检测框,有了运动检测的蒙版,接下来就可以做区域入侵告警了。
区域入侵的逻辑其实就三步:
- 在画面上画一个多边形区域(禁区)
- 判断检测框或多边形是否与禁区相交
- 如果相交,触发告警
判断两个多边形是否相交,可以用分离轴定理(SAT),也可以用更简单的“点是否在多边形内”算法。我一般用射线法——从检测框中心点发出一条射线,看与禁区边界的交点数量。奇数就在内部,偶数就在外部。
// 射线法判断点是否在多边形内
function isPointInPolygon(point, polygon) {
const [x, y] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i];
const [xj, yj] = polygon[j];
if ((yi > y) !== (yj > y) &&
x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
inside = !inside;
}
}
return inside;
}
// 入侵检测逻辑
function checkIntrusion(detections, restrictedZone) {
const alerts = [];
for (const det of detections) {
const centerX = det.x + det.width / 2;
const centerY = det.y + det.height / 2;
if (isPointInPolygon([centerX, centerY], restrictedZone)) {
alerts.push({
label: det.label,
confidence: det.confidence,
message: `检测到 ${det.label} 进入禁区`
});
}
}
return alerts;
}
实际项目经验:有一次我在做周界安防,客户要求“人不能进,但猫狗可以进”。怎么办?很简单,YOLO检测出“人”类别才触发告警,检测出“猫”“狗”就忽略。这就是AI检测比纯运动检测更智能的地方——它能区分“谁”在动。
16.5 在WebRTC流中嵌入分析结果
分析结果出来了,怎么让接收端看到?
有三种主流方式:
| 方式 | 原理 | 延迟 | 适用场景 |
|---|---|---|---|
| Canvas叠加 | 在接收端用Canvas绘制检测框和文字 | 低(<50ms) | 浏览器端查看 |
| 数据通道传输 | 通过WebRTC DataChannel发送JSON结果 | 低(<30ms) | 需要二次处理的场景 |
| 视频帧直接绘制 | 在SFU侧将结果绘制到视频帧上再转发 | 中(<100ms) | 所有接收端统一显示 |
我个人最推荐第二种——数据通道传输。为什么?因为它把“显示”和“分析”解耦了。接收端拿到JSON数据后,可以自由选择怎么渲染。想画框就画框,想显示文字就显示文字,甚至可以用语音播报。
// 通过DataChannel发送检测结果
const dataChannel = pc.createDataChannel('ai-analysis');
dataChannel.onopen = () => {
// 每帧检测完成后发送结果
setInterval(async () => {
const frame = await captureFrame();
const detections = await runYOLO(frame);
const alerts = checkIntrusion(detections, restrictedZone);
dataChannel.send(JSON.stringify({
timestamp: Date.now(),
detections: detections.map(d => ({
label: d.label,
confidence: d.confidence,
bbox: [d.x, d.y, d.width, d.height]
})),
alerts: alerts
}));
}, 100); // 10fps
};
// 接收端解析并渲染
dataChannel.onmessage = (event) => {
const data = JSON.parse(event.data);
// 在Canvas上绘制检测框
drawDetections(data.detections);
// 显示告警信息
if (data.alerts.length > 0) {
showAlerts(data.alerts);
}
};
避坑指南:我曾经在数据通道里发送过大的JSON(比如把整张图片的像素数据都塞进去),结果导致通道拥塞,视频也跟着卡顿。记住,数据通道只传元数据,不要传原始图像。检测框坐标、类别、置信度,这些就够了。
16.6 性能优化:别让AI拖垮WebRTC
AI分析很吃资源。如果处理不好,WebRTC的实时性会大打折扣。这里分享几个我踩过的坑和优化方案。
1. 推理频率控制
不要每帧都推理。我一般用动态帧率:
- 画面静止时:1fps
- 有轻微运动时:5fps
- 有大量运动时:10fps
这样平均推理负载能降低60%以上。
2. 模型量化
YOLOv8的FP32模型大概120MB,量化成INT8后只有30MB,推理速度提升3倍,精度只下降1-2%。在浏览器里跑,量化几乎是必选项。
3. 使用Web Workers
把AI推理放到Worker线程里,避免阻塞主线程的渲染和WebRTC处理。我习惯开2-3个Worker,轮流处理帧,形成流水线。
// 主线程
const worker = new Worker('ai-worker.js');
worker.postMessage({ type: 'init', model: 'yolov8n-int8.onnx' });
// 每帧发送给Worker处理
worker.postMessage({ type: 'infer', frame: canvasData }, [canvasData.buffer]);
worker.onmessage = (e) => {
const { detections, alerts } = e.data;
// 更新UI
};
4. 硬件加速
WebGL和WebGPU可以加速模型推理。TensorFlow.js默认就支持WebGL后端。如果用户的设备支持,推理时间能从100ms降到20ms。
一句话总结:AI + WebRTC不是简单的拼凑,而是一场精心设计的协奏。YOLO负责“看懂”,运动检测负责“感知”,区域入侵负责“判断”,数据通道负责“传达”。四者配合好了,你的视频流就有了真正的智能。
好了,这一章的内容就到这里。代码示例你都可以直接拿去用,但记得根据实际场景调整参数。毕竟,每个项目的摄像头角度、光照条件、检测目标都不一样。多调试,多测试,你也能做出自己的智能视频分析系统。