一、WebRTC 与 AI 的“化学反应”

说实话,我第一次把 TensorFlow.js 塞进 WebRTC 管道时,心里也没底。浏览器里跑 AI 模型?还要实时处理视频流?听起来就像让自行车跑高速。但后来我发现,这其实是现代实时通信的必然趋势。

为什么这么说?你想想看,传统的 WebRTC 只是负责“传画面”,但画面里有什么、能不能做点智能处理,它不管。而 AI 的加入,相当于给视频流装上了一双“眼睛”和“大脑”。

我个人习惯把这种结合分成三个层次:

  • 基础层:AI 分析视频帧,提取特征(人脸、手势等)
  • 增强层:基于分析结果,实时修改视频流(滤镜、特效)
  • 交互层:AI 识别结果反过来控制 WebRTC 行为(比如静音、切换摄像头)

今天我们就重点讲前两层。嗯,先看一张整体架构图,帮你建立全局认知。

WebRTC + AI 实时处理架构 📷 摄像头 MediaStream 核心处理模块 Canvas 捕获帧 TensorFlow.js 推理 滤镜/特效合成 输出流 处理流程:摄像头 → 原始流 → Canvas 逐帧分析 → AI 推理 → 合成输出 人脸检测 blazeface 模型 实时关键点定位 手势识别 handpose 模型 21个关键点追踪 AI 滤镜 基于人脸关键点 实时像素级处理

二、TensorFlow.js 接入:从零到一

2.1 加载模型的最佳实践

我在项目中踩过最大的坑,就是模型加载时机。一开始我直接在页面加载时就开始下载模型,结果用户打开页面要等 5 秒才能看到画面。后来我改成“懒加载”——等 WebRTC 连接建立后再加载模型。

核心思路:模型加载不要阻塞视频流的启动。

// 我推荐的加载方式
async function loadModels() {
  // 先启动摄像头,让用户看到画面
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  videoElement.srcObject = stream;
  
  // 后台悄悄加载模型
  const model = await blazeface.load();
  const handModel = await handpose.load();
  
  return { model, handModel };
}

小技巧:可以用 requestIdleCallback 在浏览器空闲时加载模型,不影响主线程的渲染性能。

2.2 视频帧捕获:Canvas 是关键

WebRTC 的 MediaStream 不能直接喂给 TensorFlow.js。你需要一个中间人——Canvas。说白了,就是把视频的每一帧“截图”到 Canvas 上,然后让 AI 模型分析这张图。

function captureFrame(video, canvas) {
  const ctx = canvas.getContext('2d');
  // 我习惯用 2x 缩放,提高检测精度
  canvas.width = video.videoWidth * 2;
  canvas.height = video.videoHeight * 2;
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  return canvas;
}

// 在 requestAnimationFrame 循环中调用
async function detectionLoop() {
  const canvas = captureFrame(videoElement, tempCanvas);
  const predictions = await model.estimateFaces(canvas);
  // 处理预测结果...
  requestAnimationFrame(detectionLoop);
}

注意:Canvas 的尺寸不要太大。我曾经设成 1920x1080,结果手机端直接卡死。建议控制在 640x480 以内,精度完全够用。

三、实时人脸检测:不只是画个框

3.1 BlazeFace 模型解析

Google 的 BlazeFace 模型,说白了就是为实时场景量身定做的。它能在 30fps 下稳定检测人脸,而且模型只有几百 KB。我对比过 OpenCV 的 Haar Cascade,BlazeFace 在侧脸、遮挡场景下表现好太多了。

特性 BlazeFace 传统 Haar Cascade
模型大小 ~500KB ~1MB
检测速度 30fps+ 15-20fps
侧脸检测 优秀 较差
关键点输出 6个关键点

3.2 关键点应用:动态特效

有了人脸关键点,你能做的事情就多了。比如根据眼睛位置加眼镜、根据嘴巴位置做表情特效。我记得有一次做项目,客户要求“用户张嘴时自动静音”,就是靠检测嘴巴关键点的距离实现的。

// 检测嘴巴是否张开
function isMouthOpen(landmarks) {
  // landmarks[0]: 鼻子, landmarks[1]: 左眼, landmarks[2]: 右眼
  // landmarks[3]: 左嘴角, landmarks[4]: 右嘴角
  const mouthTop = landmarks[3];  // 上嘴唇
  const mouthBottom = landmarks[4]; // 下嘴唇
  const distance = Math.abs(mouthTop[1] - mouthBottom[1]);
  
  // 阈值需要根据实际画面调整
  return distance > 20; 
}

四、实时手势识别:从手到指令

4.1 Handpose 模型的使用

Handpose 模型能输出 21 个手部关键点。我刚开始用的时候觉得“21 个点能干嘛”?后来发现,通过计算点与点之间的角度和距离,可以识别出各种手势。

举个例子,识别“比心”手势:

  • 大拇指指尖和食指指尖靠近
  • 其他手指弯曲
  • 手腕位置相对稳定
function detectHeartGesture(landmarks) {
  const thumbTip = landmarks[4];   // 大拇指尖
  const indexTip = landmarks[8];   // 食指尖
  
  const distance = Math.sqrt(
    Math.pow(thumbTip[0] - indexTip[0], 2) +
    Math.pow(thumbTip[1] - indexTip[1], 2)
  );
  
  // 距离小于阈值,且其他手指弯曲
  return distance < 30 && areOtherFingersBent(landmarks);
}

避坑指南:我曾经把阈值设得太死,结果用户稍微偏一点角度就识别失败。后来我改用“相对比例”而不是“绝对距离”,鲁棒性好了很多。

4.2 手势控制 WebRTC

手势识别的终极目标,是控制 WebRTC 的行为。比如:

  • 握拳 → 静音麦克风
  • 张开手掌 → 取消静音
  • 比心 → 开启美颜滤镜
  • 挥手 → 切换摄像头
// 手势到指令的映射
function handleGesture(gesture) {
  switch(gesture) {
    case 'fist':
      audioTrack.enabled = false;
      break;
    case 'palm':
      audioTrack.enabled = true;
      break;
    case 'heart':
      enableBeautyFilter();
      break;
    case 'wave':
      switchCamera();
      break;
  }
}

五、AI 滤镜应用:实时像素级处理

5.1 基于人脸关键点的滤镜

传统的滤镜是整张图统一处理,而 AI 滤镜可以做到“因人而异”。比如:

  • 根据眼睛位置加眼影
  • 根据脸型轮廓瘦脸
  • 根据肤色自动调色

我做过一个项目,需要给视频中的人“戴”虚拟口罩。关键就是利用人脸关键点定位嘴巴和鼻子,然后在这个区域叠加口罩图片。

function applyMaskFilter(canvas, landmarks) {
  const ctx = canvas.getContext('2d');
  const nose = landmarks[0];
  const leftMouth = landmarks[3];
  const rightMouth = landmarks[4];
  
  // 计算口罩位置和大小
  const maskWidth = Math.abs(rightMouth[0] - leftMouth[0]) * 1.5;
  const maskHeight = maskWidth * 0.6;
  const maskX = nose[0] - maskWidth / 2;
  const maskY = nose[1] - maskHeight * 0.2;
  
  // 绘制口罩(这里用半透明矩形代替实际图片)
  ctx.fillStyle = 'rgba(100, 150, 200, 0.7)';
  ctx.beginPath();
  ctx.ellipse(maskX + maskWidth/2, maskY + maskHeight/2, 
              maskWidth/2, maskHeight/2, 0, 0, Math.PI * 2);
  ctx.fill();
}

5.2 性能优化:别让滤镜拖垮帧率

滤镜处理是最吃性能的环节。我踩过的坑:

  • 不要每帧都重新绘制整个 Canvas,用离屏 Canvas 缓存背景
  • 滤镜计算用 Web Worker 分离到后台线程
  • 降低滤镜分辨率,最后再缩放到原始尺寸

性能红线:如果帧率掉到 15fps 以下,用户会明显感觉到卡顿。这时候宁可降低滤镜效果,也要保证流畅度。

六、完整流程串联

最后,我把整个流程串起来。嗯,其实就三步:

  1. 捕获:从 WebRTC 的 MediaStream 中获取视频帧,绘制到 Canvas
  2. 推理:用 TensorFlow.js 模型分析 Canvas 图像,得到人脸/手势关键点
  3. 合成:基于关键点信息,在 Canvas 上叠加滤镜/特效,输出到新的 MediaStream

说白了,就是把 AI 模型当作一个“智能滤镜引擎”,插在摄像头和 WebRTC 发送端之间。我在实际项目中,这个架构支撑了 30 路并发视频流的实时处理,帧率稳定在 24fps 以上。

记住一点:AI 是工具,WebRTC 是管道。两者结合得好,才能做出真正“聪明”的实时通信应用。