21、WebRTC与AI结合:人脸检测与背景替换、实时美颜滤镜、语音识别集成

说实话,WebRTC 做到一定程度,你会发现一个瓶颈——画面质量。网络再优化,码率再控制,摄像头怼着脸拍,痘痘、暗光、杂乱的背景,全给对面看去了。这时候,AI 就该上场了。

我个人习惯把 WebRTC + AI 的集成分成三个层次:视觉增强(美颜、背景替换)、听觉增强(语音识别、降噪)、智能交互(表情识别、手势控制)。这一章,咱们就聚焦前两个,把最实用的落地场景讲透。

21.1 人脸检测:一切视觉增强的基础

不管你是做美颜还是背景替换,第一步永远是——找到人脸在哪。没有这一步,后面全是瞎搞。

我早期在项目中踩过一个坑:直接用 Canvas 2D 逐帧分析像素,CPU 直接拉满,手机发烫到能煎鸡蛋。后来换成了 MediaPipe Face Detection,才算是走上正轨。

21.1.1 集成 MediaPipe 人脸检测

MediaPipe 是 Google 开源的跨平台框架,专门为实时视频流设计。它能在浏览器里通过 WebAssembly 跑模型,延迟极低。

// 1. 引入 MediaPipe 库
import { FaceDetection } from '@mediapipe/face_detection';
import { Camera } from '@mediapipe/camera_utils';

// 2. 初始化检测器
const faceDetection = new FaceDetection({
  locateFile: (file) => {
    return `https://cdn.jsdelivr.net/npm/@mediapipe/face_detection/${file}`;
  }
});

faceDetection.setOptions({
  modelSelection: 1,        // 0: 短距离(2m内), 1: 全距离
  minDetectionConfidence: 0.7
});

// 3. 绑定摄像头
const camera = new Camera(videoElement, {
  onFrame: async () => {
    await faceDetection.send({ image: videoElement });
  },
  width: 640,
  height: 480
});
camera.start();

// 4. 获取检测结果
faceDetection.onResults((results) => {
  if (results.detections.length > 0) {
    const bbox = results.detections[0].boundingBox;
    // bbox.xCenter, bbox.yCenter, bbox.width, bbox.height
    drawFaceBox(bbox); // 绘制人脸框
  }
});
💡 我的经验:modelSelection 选 1(全距离)虽然更准,但计算量翻倍。如果你的场景是单人坐姿(比如视频会议),选 0 就够了,帧率能提升 30%。

21.2 背景替换:从抠图到合成

背景替换说白了就是两步:抠出人像 + 合成新背景。难点在于实时性——你不能让用户看到一卡一卡的边缘。

21.2.1 基于 TensorFlow.js 的 BodyPix 方案

BodyPix 是 TensorFlow.js 官方的人像分割模型。它能把视频帧里的每个人体像素标出来,精度不错,但速度一般。

import * as bodyPix from '@tensorflow-models/body-pix';

// 加载模型(建议在页面初始化时做)
const net = await bodyPix.load({
  architecture: 'MobileNetV1',
  outputStride: 16,
  multiplier: 0.75,
  quantBytes: 2
});

// 每一帧处理
async function processFrame(video, canvas) {
  const segmentation = await net.segmentPerson(video, {
    flipHorizontal: false,
    internalResolution: 'medium',
    segmentationThreshold: 0.7
  });

  // 用 Canvas 2D 合成
  const ctx = canvas.getContext('2d');
  ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);

  // 把人像部分画上去
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  const mask = segmentation.data;

  for (let i = 0; i < mask.length; i++) {
    if (mask[i] === 1) {
      // 保留原视频像素
      const videoData = getVideoPixel(video, i);
      data[i * 4] = videoData.r;
      data[i * 4 + 1] = videoData.g;
      data[i * 4 + 2] = videoData.b;
      data[i * 4 + 3] = 255;
    } else {
      // 背景区域透明,显示背景图
      data[i * 4 + 3] = 0;
    }
  }
  ctx.putImageData(imageData, 0, 0);
}
⚠️ 注意:BodyPix 在移动端表现一般。我曾经在 iPhone 12 上测试,720p 分辨率下只能跑到 15fps。如果追求更高帧率,建议用 MediaPipe Selfie Segmentation,它专门为自拍场景优化过。

21.2.2 用 WebGL 加速合成

纯 CPU 做像素操作太慢了。我建议把合成逻辑搬到 WebGL 着色器 里。这样 GPU 并行处理,帧率直接翻倍。

// 顶点着色器
const vertexShader = `
  attribute vec2 a_position;
  attribute vec2 a_texCoord;
  varying vec2 v_texCoord;
  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
    v_texCoord = a_texCoord;
  }
`;

// 片元着色器(核心合成逻辑)
const fragmentShader = `
  precision mediump float;
  uniform sampler2D u_video;
  uniform sampler2D u_background;
  uniform sampler2D u_mask;
  varying vec2 v_texCoord;

  void main() {
    vec4 videoColor = texture2D(u_video, v_texCoord);
    vec4 bgColor = texture2D(u_background, v_texCoord);
    float mask = texture2D(u_mask, v_texCoord).r;

    // mask 接近 1 表示人像,接近 0 表示背景
    gl_FragColor = mix(bgColor, videoColor, mask);
  }
`;

嗯,这里要注意:mask 纹理需要每帧更新。你可以把 MediaPipe 输出的分割结果,用 texImage2D 上传到 GPU。这样 CPU 只负责跑模型,GPU 负责合成,各司其职。

21.3 实时美颜滤镜:不只是磨皮

美颜滤镜听起来高大上,其实核心就三个操作:磨皮(模糊)、美白(调色)、瘦脸(变形)。

21.3.1 磨皮:双边滤波

普通高斯模糊会把边缘也糊掉,人脸轮廓都没了。双边滤波(Bilateral Filter)能保留边缘,只平滑皮肤区域。

// 简化版双边滤波(片元着色器)
const bilateralShader = `
  precision mediump float;
  uniform sampler2D u_image;
  uniform vec2 u_texelSize;
  uniform float u_sigma;

  void main() {
    vec2 uv = v_texCoord;
    vec3 center = texture2D(u_image, uv).rgb;
    vec3 sum = vec3(0.0);
    float totalWeight = 0.0;

    // 采样周围 5x5 像素
    for (int x = -2; x <= 2; x++) {
      for (int y = -2; y <= 2; y++) {
        vec2 offset = vec2(float(x), float(y)) * u_texelSize;
        vec3 sample = texture2D(u_image, uv + offset).rgb;

        // 空间权重
        float spatialWeight = exp(-float(x*x + y*y) / (2.0 * u_sigma * u_sigma));
        // 颜色权重(边缘保留关键)
        float colorWeight = exp(-distance(sample, center) / (2.0 * 0.1 * 0.1));

        float weight = spatialWeight * colorWeight;
        sum += sample * weight;
        totalWeight += weight;
      }
    }
    gl_FragColor = vec4(sum / totalWeight, 1.0);
  }
`;
🔑 关键参数:u_sigma 控制模糊程度。我一般设 2.0~3.0,太大会让画面像油画。另外,采样半径不要超过 5x5,否则移动端扛不住。

21.3.2 美白:简单调色

美白其实就是一个颜色矩阵变换。把 RGB 往亮色调,同时降低黄色分量。

// 美白着色器
const whiteningShader = `
  precision mediump float;
  uniform sampler2D u_image;
  uniform float u_intensity; // 0.0 ~ 1.0

  void main() {
    vec4 color = texture2D(u_image, v_texCoord);
    // 提高亮度
    vec3 bright = color.rgb * (1.0 + u_intensity * 0.3);
    // 降低黄色(减少红色和绿色中的蓝色补色)
    bright.b = mix(color.b, bright.b, 0.8);
    // 增加蓝色调,让皮肤更冷白
    bright.b = min(bright.b + u_intensity * 0.05, 1.0);
    gl_FragColor = vec4(bright, color.a);
  }
`;

我个人习惯把美白强度控制在 0.3~0.5 之间。太强了会显得假白,像戴了面具。你想想看,用户开视频会议,对面看到一张惨白的脸,多吓人。

21.4 语音识别集成:让视频通话“听得懂”

语音识别和 WebRTC 结合,最常见的场景是实时字幕语音指令。比如开会时自动生成字幕,或者说“静音”就自动关麦。

21.4.1 获取音频流并送入识别引擎

WebRTC 的 MediaStream 可以直接传给 Web Speech API 或第三方服务(如 Google Cloud Speech-to-Text)。

// 获取本地麦克风流
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

// 创建 AudioContext 处理音频
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);

// 创建 Web Speech API 识别器
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = 'zh-CN';
recognition.continuous = true;   // 持续识别
recognition.interimResults = true; // 返回中间结果

// 绑定音频流到识别器(注意:Web Speech API 自动使用默认麦克风)
// 这里需要手动将 stream 连接到识别器
// 实际上 Web Speech API 不支持直接传入 MediaStream,需要 hack
// 更可靠的方式是用 WebSocket 发送音频到后端

// 方案一:直接用浏览器麦克风(简单但不灵活)
recognition.onresult = (event) => {
  for (let i = event.resultIndex; i < event.results.length; i++) {
    const transcript = event.results[i][0].transcript;
    if (event.results[i].isFinal) {
      console.log('最终结果:', transcript);
      // 显示到字幕区域
      showSubtitle(transcript);
    } else {
      console.log('中间结果:', transcript);
    }
  }
};
recognition.start();
⚠️ 坑点:Web Speech API 在 Chrome 上表现不错,但在 Safari 和 Firefox 上支持很差。我曾在项目中遇到 Safari 完全不识别中文的情况。如果你需要跨浏览器兼容,建议用 WebSocket + 后端 ASR 引擎(如 Kaldi、Whisper)。

21.4.2 用 WebSocket 传输音频到后端

更专业的做法是把 PCM 音频数据通过 WebSocket 发送到服务器,由服务器做语音识别,再把结果推回来。

// 前端:采集音频并发送
const socket = new WebSocket('wss://your-asr-server.com/ws');
const mediaRecorder = new MediaRecorder(stream, {
  mimeType: 'audio/webm;codecs=opus'
});

mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0 && socket.readyState === WebSocket.OPEN) {
    socket.send(event.data);
  }
};

// 每 100ms 采集一次
mediaRecorder.start(100);

// 接收识别结果
socket.onmessage = (event) => {
  const result = JSON.parse(event.data);
  if (result.type === 'final') {
    updateSubtitle(result.text);
  }
};

为什么不用 WebRTC 的 RTCPeerConnection 传音频?因为语音识别需要的是原始音频流,而不是编码后的 RTP 包。你想想看,编码后的 Opus 数据再解码,质量会有损失,影响识别准确率。

21.5 知识体系总览

下面这张图,把本章涉及的核心模块和它们之间的关系梳理清楚了。你可以把它当作一个技术选型参考。

WebRTC + AI 集成架构 WebRTC 视频/音频流 视觉增强模块 人脸检测 背景替换 美颜滤镜 音频增强模块 语音识别 实时字幕 语音指令 增强后的视频/音频流 RTCPeerConnection 发送

21.6 性能优化建议

把 AI 集成到 WebRTC 里,最大的敌人是延迟。视频通话本身就有端到端延迟,再加上 AI 处理,很容易超过 500ms,用户能明显感觉到卡顿。

我总结了几条实战经验:

  • 降低分辨率:AI 处理用 320x240 就够了,输出到 WebRTC 再用 720p。人脸检测不需要高清图。
  • 跳帧处理:不是每一帧都要跑 AI。比如美颜可以每 2 帧处理一次,中间帧直接复用上一帧的结果。人眼基本看不出来。
  • Web Worker 分离:把 AI 模型放到 Web Worker 里跑,避免阻塞主线程的渲染和编码。
  • 硬件加速优先:能用 WebGL 就别用 Canvas 2D,能用 WebGPU 就别用 WebGL。移动端尤其明显。
💡 一个小技巧:如果你同时做美颜和背景替换,可以把人脸检测的结果共享给两个模块。不要跑两次模型,浪费算力。我在项目中就是这么干的,帧率从 18fps 提到了 28fps。

好了,这一章的内容就到这里。WebRTC 和 AI 的结合,说白了就是用算力换体验。你投入多少计算资源,用户就能得到多好的画面和交互。但别忘了,实时通信的第一要务是流畅——再美的滤镜,卡成 PPT 也没人用。


公众号:蓝海资料掘金营,微信deep3321