DAMOYOLO-S开发入门:JavaScript前端实现实时视频检测与可视化

张开发
2026/4/7 6:17:41 15 分钟阅读

分享文章

DAMOYOLO-S开发入门:JavaScript前端实现实时视频检测与可视化
DAMOYOLO-S开发入门JavaScript前端实现实时视频检测与可视化你是不是也想过能不能在浏览器里直接调用一个强大的目标检测模型让摄像头看到的画面实时出现识别框比如让电脑自动识别你桌上的水杯、手机甚至是你家宠物猫的动作。今天我们就来动手实现这个听起来很酷的功能。我们将使用一个名为DAMOYOLO-S的轻量级但性能不俗的目标检测模型。你不用被“模型部署”、“深度学习”这些词吓到我们的重点在前端。简单来说就是写一个网页它能打开你的摄像头把拍到的画面一帧一帧地发给后端的AI模型去分析然后把分析结果比如“这是一个杯子坐标在这里”拿回来再实时地画在视频画面上。整个过程你只需要会一些基础的JavaScript和一点点Canvas绘图知识。我们会从零开始一步步搭建起这个实时检测系统。学完这篇你不仅能掌握视频流处理、WebSocket通信这些前端硬核技能还能拥有一个属于自己的“智能眼”应用原型。1. 环境准备与项目搭建在开始写代码之前我们需要把“舞台”搭好。这里不需要复杂的深度学习环境配置因为模型推理的重活交给后端服务。我们前端主要负责“采集画面”和“展示结果”。1.1 核心工具与技术栈我们先来快速认识一下这次要用到的几个关键技术点了解它们各自扮演什么角色DAMOYOLO-S这是我们本次项目的“大脑”一个高效的目标检测模型。你不需要深入理解它的内部原理只需要知道它部署在后端服务器上接收图片然后返回图片里都有哪些物体以及它们的位置。后端如何部署它有专门的教程本文我们假设后端服务已经就绪地址是ws://your-backend-server/ws。JavaScript (ES6)毫无疑问这是我们的主要编程语言。我们会用到现代JavaScript的特性来让代码更简洁。HTML5 Video Canvas这是我们的“眼睛”和“画板”。video元素用来捕获和播放摄像头视频流canvas元素则用来在视频画面上绘制检测框和标签。WebSocket这是前后端实时通信的“高速公路”。相比传统的HTTP请求你问我答一次就结束WebSocket建立的是持久连接可以允许后端随时主动向前端推送数据完美契合视频流这种连续、实时的场景。MediaDevices API浏览器提供的接口用于获取摄像头、麦克风等媒体设备。我们将用它来打开用户的摄像头。1.2 创建基础HTML结构首先创建一个名为index.html的文件。这个文件构成了我们应用的骨架。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title实时目标检测 - DAMOYOLO-S/title style body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; padding: 20px; background-color: #f5f5f5; } .container { display: flex; flex-direction: column; align-items: center; max-width: 900px; width: 100%; } #videoContainer { position: relative; width: 100%; max-width: 640px; margin-bottom: 20px; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } #videoElement { display: block; width: 100%; background-color: #000; } #canvasOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* 确保画布不干扰视频操作 */ } .controls { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } button { padding: 10px 20px; border: none; border-radius: 5px; background-color: #007bff; color: white; font-size: 16px; cursor: pointer; transition: background-color 0.2s; } button:hover { background-color: #0056b3; } button:disabled { background-color: #cccccc; cursor: not-allowed; } #status { margin-top: 15px; padding: 10px; border-radius: 5px; min-width: 200px; text-align: center; } .status-connected { background-color: #d4edda; color: #155724; } .status-disconnected { background-color: #f8d7da; color: #721c24; } /style /head body div classcontainer h1 实时目标检测演示/h1 p使用DAMOYOLO-S模型实时识别摄像头画面中的物体。/p div idvideoContainer !-- 视频元素用于显示摄像头流 -- video idvideoElement autoplay playsinline/video !-- 画布覆盖层用于绘制检测框 -- canvas idcanvasOverlay/canvas /div div classcontrols button idstartBtn开始检测/button button idstopBtn disabled停止检测/button button idchangeCameraBtn切换摄像头/button /div div idstatus classstatus-disconnected状态未连接/div /div script srcmain.js/script /body /html这段代码创建了一个简洁的界面。核心是那个#videoContainer它里面叠放着一个video和一个canvas。视频负责显示画布负责在上面画画检测框。下面的按钮用来控制流程状态栏显示连接情况。2. 核心JavaScript逻辑实现接下来是重头戏我们在项目根目录创建一个main.js文件。所有的交互和业务逻辑都将在这里实现。2.1 初始化与DOM元素获取首先我们把需要用到的HTML元素和关键变量都准备好。// main.js // 获取DOM元素 const videoElement document.getElementById(videoElement); const canvasOverlay document.getElementById(canvasOverlay); const ctx canvasOverlay.getContext(2d); // 获取Canvas绘图上下文 const startBtn document.getElementById(startBtn); const stopBtn document.getElementById(stopBtn); const changeCameraBtn document.getElementById(changeCameraBtn); const statusDiv document.getElementById(status); // 全局变量 let mediaStream null; // 存储摄像头流 let socket null; // WebSocket连接对象 let animationId null; // requestAnimationFrame的ID用于控制循环 let currentDeviceId null; // 当前使用的摄像头设备ID let availableDevices []; // 可用的视频输入设备列表 // 后端WebSocket服务地址请根据你的实际后端地址修改 const WS_SERVER_URL ws://localhost:8000/ws; // 示例地址2.2 访问用户摄像头我们需要请求用户的摄像头权限并成功获取视频流。/** * 请求摄像头访问权限并启动视频流 * param {string} deviceId - 指定的摄像头设备ID如果为undefined则使用默认摄像头 */ async function startCamera(deviceId) { try { // 如果已有流先停止其所有轨道 if (mediaStream) { mediaStream.getTracks().forEach(track track.stop()); } const constraints { video: { width: { ideal: 640 }, // 设置理想宽度 height: { ideal: 480 }, // 设置理想高度 frameRate: { ideal: 15 } // 设置理想帧率平衡性能与流畅度 } }; // 如果指定了设备ID则添加到约束条件中 if (deviceId) { constraints.video.deviceId { exact: deviceId }; } // 获取媒体流 mediaStream await navigator.mediaDevices.getUserMedia(constraints); videoElement.srcObject mediaStream; // 等待视频元数据加载完成确保视频尺寸可用 await new Promise((resolve) { videoElement.onloadedmetadata () { resolve(); }; }); // 设置Canvas尺寸与视频尺寸一致 canvasOverlay.width videoElement.videoWidth; canvasOverlay.height videoElement.videoHeight; updateStatus(摄像头已就绪, connected); console.log(摄像头启动成功视频尺寸:, canvasOverlay.width, x, canvasOverlay.height); } catch (error) { console.error(访问摄像头出错:, error); updateStatus(摄像头错误: ${error.name}, disconnected); alert(无法访问摄像头${error.message}); } } /** * 获取可用的视频输入设备摄像头列表 */ async function getVideoDevices() { try { // 首先获取一次媒体设备权限确保能列出设备在某些浏览器中需要 await navigator.mediaDevices.getUserMedia({ video: true }); const devices await navigator.mediaDevices.enumerateDevices(); availableDevices devices.filter(device device.kind videoinput); console.log(找到摄像头设备:, availableDevices); } catch (error) { console.error(枚举设备失败:, error); } }2.3 建立WebSocket连接与通信这是前后端实时交互的桥梁。/** * 建立WebSocket连接到后端检测服务 */ function connectWebSocket() { if (socket socket.readyState WebSocket.OPEN) { console.log(WebSocket已连接); return; } socket new WebSocket(WS_SERVER_URL); socket.onopen function() { console.log(WebSocket连接已建立); updateStatus(已连接到检测服务, connected); startBtn.disabled false; stopBtn.disabled true; // 连接刚建立时停止按钮应禁用 }; socket.onmessage function(event) { // 接收后端返回的检测结果 try { const detections JSON.parse(event.data); // 将检测结果绘制到Canvas上 drawDetections(detections); } catch (error) { console.error(解析检测结果失败:, error, event.data); } }; socket.onerror function(error) { console.error(WebSocket错误:, error); updateStatus(连接出错, disconnected); }; socket.onclose function() { console.log(WebSocket连接已关闭); updateStatus(连接已断开, disconnected); startBtn.disabled false; stopBtn.disabled true; // 如果正在动画循环中则停止 if (animationId) { cancelAnimationFrame(animationId); animationId null; } }; }2.4 捕获视频帧并发送我们需要定时从视频中抓取图片发送给后端。/** * 开始捕获视频帧并通过WebSocket发送 */ function startDetection() { if (!socket || socket.readyState ! WebSocket.OPEN) { alert(请先确保WebSocket连接正常); return; } if (!mediaStream) { alert(请先启动摄像头); return; } updateStatus(检测进行中..., connected); startBtn.disabled true; stopBtn.disabled false; // 清除上一轮绘制 ctx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height); // 使用 requestAnimationFrame 控制发送频率实现简单的节流 let lastSendTime 0; const SEND_INTERVAL 100; // 发送间隔毫秒约10FPS可根据性能调整 function sendFrame() { const now Date.now(); if (now - lastSendTime SEND_INTERVAL) { // 1. 将当前视频帧绘制到一个离屏Canvas上 const offscreenCanvas document.createElement(canvas); offscreenCanvas.width videoElement.videoWidth; offscreenCanvas.height videoElement.videoHeight; const offscreenCtx offscreenCanvas.getContext(2d); offscreenCtx.drawImage(videoElement, 0, 0); // 2. 将Canvas转换为Base64格式的JPEG图片压缩以减少数据传输量 const imageData offscreenCanvas.toDataURL(image/jpeg, 0.8); // 0.8为图片质量 // 3. 通过WebSocket发送图片数据 const message JSON.stringify({ image: imageData }); if (socket.readyState WebSocket.OPEN) { socket.send(message); } lastSendTime now; } // 循环调用自身 animationId requestAnimationFrame(sendFrame); } // 启动发送循环 animationId requestAnimationFrame(sendFrame); } /** * 停止检测 */ function stopDetection() { if (animationId) { cancelAnimationFrame(animationId); animationId null; } // 清空画布 ctx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height); updateStatus(检测已停止, connected); startBtn.disabled false; stopBtn.disabled true; }2.5 在Canvas上绘制检测结果收到后端返回的坐标和标签后我们需要把它们画出来。/** * 在Canvas上绘制检测框和标签 * param {Array} detections - 检测结果数组每个元素包含bbox, label, confidence */ function drawDetections(detections) { // 每次绘制前清空上一帧的画布 ctx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height); if (!detections || detections.length 0) { return; // 没有检测到物体 } // 设置绘图样式 ctx.lineWidth 2; ctx.font 16px Arial; ctx.textBaseline top; detections.forEach(det { // det 格式假设为: { bbox: [x1, y1, x2, y2], label: person, confidence: 0.95 } const [x1, y1, x2, y2] det.bbox; const label det.label; const score det.confidence; // 为不同类别随机颜色或固定映射 const hue (label.length * 50) % 360; // 简单根据标签名生成色相 ctx.strokeStyle hsl(${hue}, 100%, 50%); ctx.fillStyle hsl(${hue}, 100%, 50%, 0.2); // 绘制矩形框 ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); ctx.fillRect(x1, y1, x2 - x1, y2 - y1); // 绘制标签背景 const text ${label} ${(score * 100).toFixed(1)}%; const textWidth ctx.measureText(text).width; ctx.fillStyle hsl(${hue}, 100%, 50%); ctx.fillRect(x1, y1 - 20, textWidth 10, 20); // 绘制标签文字 ctx.fillStyle white; ctx.fillText(text, x1 5, y1 - 16); }); }2.6 整合与事件绑定最后我们把所有功能串联起来并绑定到按钮的点击事件上。/** * 更新状态显示 */ function updateStatus(message, type) { statusDiv.textContent 状态${message}; statusDiv.className status-${type}; } /** * 切换摄像头 */ async function switchCamera() { if (availableDevices.length 2) { alert(未找到其他摄像头设备。); return; } // 找到当前设备的下一个设备 const currentIndex availableDevices.findIndex(device device.deviceId currentDeviceId); const nextIndex (currentIndex 1) % availableDevices.length; const nextDevice availableDevices[nextIndex]; currentDeviceId nextDevice.deviceId; console.log(切换到摄像头: ${nextDevice.label || 设备 ${nextIndex 1}}); await startCamera(currentDeviceId); // 如果正在检测需要重启检测循环以使用新摄像头 if (animationId) { stopDetection(); startDetection(); } } /** * 页面加载完成后的初始化 */ async function init() { updateStatus(初始化中..., disconnected); // 1. 获取可用摄像头列表 await getVideoDevices(); // 2. 启动默认摄像头 await startCamera(); if (availableDevices.length 0) { currentDeviceId availableDevices[0].deviceId; } // 3. 建立WebSocket连接 connectWebSocket(); // 绑定按钮事件 startBtn.addEventListener(click, startDetection); stopBtn.addEventListener(click, stopDetection); changeCameraBtn.addEventListener(click, switchCamera); // 页面关闭前关闭连接和摄像头 window.addEventListener(beforeunload, () { stopDetection(); if (socket) { socket.close(); } if (mediaStream) { mediaStream.getTracks().forEach(track track.stop()); } }); } // 启动整个应用 init();3. 运行与调试现在所有代码都准备好了。你需要一个后端服务来接收图片并返回DAMOYOLO-S的检测结果。后端可以用PythonFastAPI/Flask WebSockets等任何你熟悉的语言实现它需要做两件事1. 提供WebSocket端点2. 接收Base64图片调用DAMOYOLO-S推理返回JSON格式的检测结果。假设你的后端服务已经在localhost:8000运行。将main.js中的WS_SERVER_URL改为你的后端地址例如ws://localhost:8000/ws。直接在浏览器中打开index.html文件可能需要通过HTTP服务器打开如使用Live Server插件以避免某些API的本地文件限制。点击“开始检测”。浏览器会请求摄像头权限同意后视频画面出现。同时前端会开始向后端发送视频帧。如果一切正常你将在视频画面上看到实时绘制的检测框和标签。4. 性能调优与实用技巧在实际使用中你可能会遇到性能或体验问题。这里有几个小技巧降低发送频率SEND_INTERVAL常量控制发送帧的间隔。增加这个值如改为200毫秒可以显著降低网络和CPU负载但会降低实时性。你需要根据实际场景权衡。降低图片分辨率在startCamera函数的constraints中降低width和height的ideal值如320x240可以大幅减少每帧图片的数据量提升传输和推理速度。压缩图片质量toDataURL(image/jpeg, quality)中的quality参数0到1之间控制JPEG压缩质量。降低质量如0.7能减少数据大小但可能会影响检测精度。使用离屏Canvas我们已经用了这个技巧。将视频帧先画到一个看不见的Canvas上再进行转换和发送是标准做法。处理延迟与丢帧在弱网络环境下可以考虑在前端实现一个简单的帧缓冲区管理如果发现发送队列过长可以丢弃一些中间帧只发送最新的帧确保显示的检测结果不过时。5. 总结走完这一趟你应该已经亲手搭建了一个在浏览器中运行的实时目标检测应用。我们从前端视角串联起了摄像头访问、实时视频流处理、WebSocket双向通信、Canvas动态绘图这几个核心环节。整个过程的关键在于理解数据流从摄像头的原始视频流到我们截取的单帧图片经过Base64编码和网络传输在后端AI模型里转一圈变成结构化的数据再传回前端最终通过Canvas渲染成你看到的那些方框和文字。每一步的格式转换和通信都需要仔细处理。这个项目是一个很好的起点。你可以基于它做很多扩展比如检测特定类别的物体只画出入侵区域的人、将检测结果保存下来、或者加入声音提示。前端与AI的结合能打开很多有趣应用的大门。动手试试调整参数看看效果如何变化这才是学习最快的方式。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

更多文章