实时口罩检测-通用模型WebRTC视频流接入:浏览器端实时检测
实时口罩检测-通用模型WebRTC视频流接入:浏览器端实时检测
1. 引言:从静态图片到实时视频流的跨越
想象一下,在一个公共场所的入口,摄像头需要实时判断每一位进入者是否佩戴了口罩。传统的做法可能是:拍一张照片,上传到服务器,等待模型分析,再把结果返回。这个过程可能需要几秒钟,甚至更长。在需要快速通行或实时监控的场景下,这种延迟是无法接受的。
今天,我们要解决的问题就是:如何让口罩检测模型“跑”在浏览器里,直接处理摄像头视频流,实现毫秒级的实时检测?
本文将带你一步步实现这个目标。我们将基于一个已经部署好的“实时口罩检测-通用”模型服务,利用WebRTC技术,在浏览器端直接捕获摄像头视频流,并调用模型进行实时分析。整个过程无需将视频帧上传到远程服务器,所有计算都在本地(或靠近用户的边缘)完成,从而实现真正的低延迟、高隐私性的实时检测应用。
你将学到什么:
- 理解WebRTC如何捕获和处理浏览器摄像头视频流。
- 学会将Gradio部署的模型服务与前端页面集成。
- 掌握在浏览器中实现实时视频流目标检测的核心逻辑。
- 获得一套完整的、可运行的代码,快速搭建你自己的实时检测应用。
前置条件:
- 一个已经通过ModelScope和Gradio部署好的“实时口罩检测-通用”模型服务(访问地址假设为
http://your-model-service.com)。 - 基础的HTML、JavaScript知识。
- 一个支持WebRTC的现代浏览器(如Chrome, Edge, Firefox)。
2. 核心技术与原理简介
在开始动手之前,我们先花几分钟了解一下背后的“武器库”。这能帮助你在遇到问题时,知道该从哪里寻找答案。
2.1 模型基石:DAMO-YOLO
我们使用的“实时口罩检测-通用”模型,其核心是DAMO-YOLO-S。你可以把它理解为一个速度与精度平衡得非常好的“视觉侦察兵”。
- 它厉害在哪? 相比大家熟悉的YOLOv5、YOLOv7等,DAMO-YOLO在保持极快推理速度的同时,检测精度更高。这意味着它既能“看得准”(准确区分戴口罩和没戴口罩的人脸),又能“反应快”(每秒能处理很多帧图像),这正是实时应用最需要的特性。
- 设计巧思:大脖子,小脑袋。 它的网络结构主要分三部分:骨干网络(Backbone)、颈部网络(Neck)和检测头(Head)。DAMO-YOLO采用了一种“大脖子、小脑袋”的设计。简单说,就是花更多精力在“颈部”去充分融合图像浅层的细节信息(比如边缘、轮廓)和深层的语义信息(比如“这是一个人脸”),最后用一个轻量级的“头”做出判断。这种设计让模型在复杂场景下也能表现稳定。
2.2 实时传输引擎:WebRTC
WebRTC 是一套免费的、开放的、内置于浏览器的实时通信技术。我们主要利用它的两个超能力:
getUserMediaAPI: 让你在网页中一行代码就能获取用户摄像头和麦克风的访问权限,并得到实时的视频/音频流。- 低延迟传输: WebRTC专为实时性优化,能够将媒体流以极低的延迟进行处理和(如果需要)传输。
在我们的场景中,我们主要使用第一个能力来捕获摄像头视频流。
2.3 桥梁:Gradio模型接口
我们的模型已经通过 Gradio 部署成了Web服务。Gradio会为我们的模型生成一个友好的网页界面和一套标准的HTTP API。这意味着,我们可以通过向一个特定的URL发送图片数据,来获取模型的检测结果。这为我们从浏览器调用模型提供了可能。
技术栈全景图:
[浏览器摄像头] --(WebRTC)--> [视频流] --(Canvas截帧)--> [图片帧] --(HTTP请求)--> [Gradio模型服务] --(JSON响应)--> [检测结果] --(Canvas绘图)--> [浏览器标注显示]
整个过程形成了一个高效的闭环。
3. 实战:构建浏览器端实时检测系统
理论说得差不多了,现在我们来动手搭建。请跟随以下步骤,你将得到一个完整的实时检测页面。
3.1 创建基础HTML页面
首先,创建一个名为 realtime_mask_detection.html 的文件,并填入以下骨架代码。它定义了视频显示、画布(用于绘图)和控制按钮。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器实时口罩检测</title>
<style>
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #f5f5f5;
}
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
max-width: 1200px;
margin-bottom: 20px;
}
.video-box, .result-box {
border: 2px solid #ccc;
border-radius: 8px;
padding: 10px;
background: white;
text-align: center;
}
video, canvas {
width: 640px;
height: 480px;
background-color: #000;
border-radius: 4px;
}
.controls {
margin: 20px 0;
}
button {
padding: 12px 24px;
margin: 0 10px;
font-size: 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
}
#startBtn {
background-color: #4CAF50;
color: white;
}
#startBtn:hover {
background-color: #45a049;
}
#stopBtn {
background-color: #f44336;
color: white;
}
#stopBtn:hover {
background-color: #da190b;
}
#status {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
min-height: 24px;
}
.status-ready {
color: #2196F3;
}
.status-running {
color: #4CAF50;
}
.status-error {
color: #f44336;
}
.legend {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 10px;
}
.legend-item {
display: flex;
align-items: center;
}
.color-box {
width: 20px;
height: 20px;
margin-right: 8px;
border: 1px solid #333;
}
.mask-color {
background-color: rgba(0, 255, 0, 0.5); /* 绿色,半透明 */
}
.no-mask-color {
background-color: rgba(255, 0, 0, 0.5); /* 红色,半透明 */
}
</style>
</head>
<body>
<h1>🎥 浏览器实时口罩检测演示</h1>
<p>基于 DAMO-YOLO 模型与 WebRTC 技术实现。请允许浏览器访问您的摄像头。</p>
<div class="container">
<div class="video-box">
<h3>实时摄像头画面</h3>
<video id="videoElement" playsinline autoplay muted></video>
<!-- playsinline 用于移动端自动播放,muted 是Chrome等浏览器自动播放策略要求 -->
</div>
<div class="result-box">
<h3>实时检测结果</h3>
<canvas id="canvasElement"></canvas>
<div class="legend">
<div class="legend-item"><div class="color-box mask-color"></div> 已佩戴口罩</div>
<div class="legend-item"><div class="color-box no-mask-color"></div> 未佩戴口罩</div>
</div>
</div>
</div>
<div class="controls">
<button id="startBtn">开始实时检测</button>
<button id="stopBtn" disabled>停止检测</button>
</div>
<div id="status" class="status-ready">状态:等待开始...</div>
<!-- 引入主逻辑脚本 -->
<script src="realtime_mask.js"></script>
</body>
</html>
3.2 编写核心JavaScript逻辑
接下来,创建 realtime_mask.js 文件。这是整个应用的大脑,负责控制视频流、调用模型和绘制结果。
// realtime_mask.js
// 配置:你的Gradio模型服务地址
const MODEL_API_URL = 'http://your-model-service.com/run/predict'; // 请替换为你的实际地址
// 获取DOM元素
const videoElement = document.getElementById('videoElement');
const canvasElement = document.getElementById('canvasElement');
const ctx = canvasElement.getContext('2d');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const statusDiv = document.getElementById('status');
// 全局变量
let stream = null;
let animationId = null;
let isRunning = false;
// 1. 初始化:请求摄像头权限并设置视频流
async function initCamera() {
updateStatus('正在请求摄像头权限...', 'status-ready');
try {
// 获取用户媒体(摄像头视频),约束条件指定视频分辨率等
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user' // 'user' 前置摄像头,'environment' 后置
},
audio: false // 我们不需要音频
});
videoElement.srcObject = stream;
updateStatus('摄像头已就绪。点击“开始实时检测”。', 'status-ready');
startBtn.disabled = false;
} catch (err) {
console.error('获取摄像头失败:', err);
updateStatus(`错误:无法访问摄像头。请确保已授予权限。 (${err.name})`, 'status-error');
startBtn.disabled = true;
}
}
// 2. 调用模型API进行检测
async function detectMask(imageDataUrl) {
// 将DataURL转换为Blob,以便通过FormData发送
const blob = await (await fetch(imageDataUrl)).blob();
const formData = new FormData();
formData.append('data', blob, 'frame.jpg'); // 'data' 是Gradio接口常见的参数名,请根据你的接口调整
try {
const response = await fetch(MODEL_API_URL, {
method: 'POST',
body: formData,
// 注意:如果Gradio服务开启了CORS,可能需要处理跨域。本地开发或同源部署可避免。
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Gradio的预测接口通常返回一个JSON,结构如 { data: [...] }
// 具体结构需要查看你模型的实际返回。假设返回一个数组,每个元素是 [x1, y1, x2, y2, confidence, class_id]
return result.data || [];
} catch (error) {
console.error('模型调用失败:', error);
updateStatus(`检测请求失败: ${error.message}`, 'status-error');
return [];
}
}
// 3. 从视频帧中捕获图像并处理
function captureAndProcessFrame() {
if (!isRunning || !stream) return;
// 确保画布尺寸与视频一致
if (canvasElement.width !== videoElement.videoWidth || canvasElement.height !== videoElement.videoHeight) {
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
}
// 1) 将当前视频帧绘制到画布上
ctx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
// 2) 从画布获取图像数据(DataURL格式)
const imageDataUrl = canvasElement.toDataURL('image/jpeg', 0.8); // 压缩质量0.8,平衡速度与质量
// 3) 调用检测函数
detectMask(imageDataUrl).then(detections => {
// 4) 在画布上绘制检测结果
drawDetections(detections);
}).catch(err => {
console.error('处理帧时出错:', err);
});
// 5) 循环下一帧,实现“实时”
animationId = requestAnimationFrame(captureAndProcessFrame);
}
// 4. 在画布上绘制检测框和标签
function drawDetections(detections) {
// 先清除上一帧的绘制(除了原始图像)
// 我们不清除整个画布,因为上面已经有视频帧了。我们只清除之前绘制的矩形和文字。
// 更简单的方法是:在绘制新框之前,重新绘制一遍视频帧。但为了效率,我们采用“脏矩形”思想简化处理。
// 这里为了简单,我们直接在新的视频帧上绘制,覆盖旧的。
// 实际上,我们在 captureAndProcessFrame 的第一步已经用视频帧覆盖了整个画布。
if (!detections || detections.length === 0) return;
const labelMap = {
1: { name: 'facemask', color: 'rgba(0, 255, 0, 0.5)', textColor: 'green' }, // 绿色,半透明
2: { name: 'no facemask', color: 'rgba(255, 0, 0, 0.5)', textColor: 'red' } // 红色,半透明
};
detections.forEach(det => {
// 假设 det 是 [x1, y1, x2, y2, confidence, class_id] 格式
// 具体格式需要根据你模型的实际输出调整
const [x1, y1, x2, y2, conf, clsId] = det;
const labelInfo = labelMap[clsId];
if (!labelInfo) return;
// 绘制矩形框
ctx.strokeStyle = labelInfo.textColor;
ctx.lineWidth = 3;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
// 绘制半透明填充背景
ctx.fillStyle = labelInfo.color;
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
// 绘制标签文本
const label = `${labelInfo.name} ${(conf * 100).toFixed(1)}%`;
ctx.fillStyle = 'white';
ctx.font = 'bold 16px Arial';
const textWidth = ctx.measureText(label).width;
ctx.fillRect(x1, y1 - 20, textWidth + 10, 20); // 文本背景
ctx.fillStyle = 'black';
ctx.fillText(label, x1 + 5, y1 - 5);
});
}
// 5. 更新状态显示
function updateStatus(message, className) {
statusDiv.textContent = `状态:${message}`;
statusDiv.className = className;
}
// 6. 开始检测流程
function startDetection() {
if (isRunning) return;
if (!stream) {
updateStatus('错误:摄像头未就绪。', 'status-error');
return;
}
isRunning = true;
startBtn.disabled = true;
stopBtn.disabled = false;
updateStatus('实时检测运行中...', 'status-running');
// 启动处理循环
captureAndProcessFrame();
}
// 7. 停止检测流程
function stopDetection() {
isRunning = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
startBtn.disabled = false;
stopBtn.disabled = true;
updateStatus('检测已停止。', 'status-ready');
// 可选:清除画布上的检测框
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
ctx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
}
// 8. 绑定按钮事件
startBtn.addEventListener('click', startDetection);
stopBtn.addEventListener('click', stopDetection);
// 9. 页面加载时初始化
window.addEventListener('load', initCamera);
// 10. 页面关闭或刷新时,关闭摄像头流
window.addEventListener('beforeunload', () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
stopDetection();
});
3.3 关键步骤与代码解析
现在,让我们拆解一下上面代码中的几个关键点:
initCamera函数: 这是入口。它使用navigator.mediaDevices.getUserMedia这个WebRTC API来请求摄像头权限。成功后,将获取到的媒体流(stream)赋值给<video>元素的srcObject,视频就开始播放了。captureAndProcessFrame函数: 这是核心循环。- 使用
drawImage将视频当前帧“快照”到<canvas>上。 - 使用
toDataURL将画布内容转换成一张JPEG图片的Base64编码字符串。 - 调用
detectMask函数,将图片数据发送给模型API。 - 收到检测结果后,调用
drawDetections在画布上绘制框和标签。 - 最后,使用
requestAnimationFrame递归调用自己,形成一个动画循环,从而实现逐帧处理。
- 使用
detectMask函数: 负责与后端模型通信。它构造一个包含图片的FormData,通过fetchAPI 发送POST请求到Gradio服务。这里有个关键点:你需要将MODEL_API_URL替换成你实际部署的Gradio应用的预测接口地址。 通常Gradio应用的接口路径是/run/predict。drawDetections函数: 根据模型返回的坐标和类别信息,在画布上绘制矩形框。我们用了两种颜色来区分“戴口罩”(绿色)和“未戴口罩”(红色)。- 资源管理: 在页面关闭时,我们通过事件监听器关闭摄像头轨道并停止检测循环,这是一个好习惯,可以避免资源泄漏。
4. 运行与调试你的应用
- 准备模型服务: 确保你的“实时口罩检测-通用”Gradio应用正在运行,并记下它的访问地址(例如
http://localhost:7860或一个公网地址)。 - 修改配置: 在
realtime_mask.js文件中,将const MODEL_API_URL的值改为你的模型服务地址加上/run/predict。例如:const MODEL_API_URL = 'http://localhost:7860/run/predict';。 - 启动页面: 由于涉及摄像头访问和可能的跨域请求,建议通过一个HTTP服务器来打开HTML文件,而不是直接双击。你可以使用Python快速启动一个:
然后在浏览器中访问# 在HTML文件所在目录执行 python3 -m http.server 8000http://localhost:8000/realtime_mask_detection.html。 - 允许摄像头: 浏览器会弹出权限请求,点击“允许”。
- 开始检测: 点击“开始实时检测”按钮。你应该能看到左侧是摄像头实时画面,右侧画布上会实时画出检测框。
可能遇到的问题与解决思路:
- 跨域错误(CORS): 如果你的前端页面地址(如
http://localhost:8000)和模型API地址(如http://localhost:7860)不同源,浏览器会因安全策略阻止请求。解决方法:- 最佳实践: 将前端页面和后端模型服务部署在同一个域名下。
- 开发调试: 可以暂时使用浏览器插件禁用CORS(不推荐用于生产),或者为Gradio服务配置CORS头(如果Gradio支持)。
- 代理: 使用Nginx等反向代理将前后端API代理到同域下。
- 模型返回格式不匹配: 代码中
drawDetections函数假设了特定的返回格式[x1, y1, x2, y2, conf, clsId]。你需要根据你模型Gradio接口的实际返回JSON结构来调整解析逻辑。打开浏览器开发者工具的“网络”选项卡,查看预测请求的响应体,就能知道确切的格式。 - 性能问题: 逐帧发送高清图片可能会对网络和服务器造成压力。你可以通过调整
toDataURL的图片质量(如0.7)、降低视频采集分辨率、或设置检测频率(例如每3帧处理一次)来优化。
5. 总结与展望
通过本文的实践,我们成功地将一个静态的图片口罩检测模型,升级为了一个基于浏览器和WebRTC的实时视频流检测应用。我们利用了DAMO-YOLO模型的高性能,结合WebRTC的实时捕获能力,在浏览器端完成了视频流的获取、帧的提取、结果的绘制,仅将图像推理任务交给了后端模型服务。
这种架构的优势非常明显:
- 低延迟: 视频捕获和结果显示在本地,只有推理请求有网络往返延迟。
- 隐私性好: 原始视频数据无需上传到远程服务器(除非你需要存储),仅在必要时发送单帧图片用于分析。
- 部署灵活: 前端是纯静态网页,可以放在任何地方;后端模型服务可以独立部署和扩展。
你可以在此基础上进行更多探索:
- 优化性能: 实现前端智能节流,只在画面有显著变化时才发送检测请求。
- 增加功能: 添加检测统计(如当前画面未戴口罩人数)、声音告警、截图保存等功能。
- 模型轻量化: 探索使用ONNX Runtime或TensorFlow.js等工具,尝试将模型直接部署到浏览器中运行,实现完全离线的实时检测,这将彻底消除网络延迟。
实时AI应用正在改变我们与计算机交互的方式。希望这个项目能成为你探索更广阔世界的一块敲门砖。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)