DAMOYOLO-S模型JavaScript前端调用:浏览器端实时检测Demo

张开发
2026/4/6 9:31:38 15 分钟阅读

分享文章

DAMOYOLO-S模型JavaScript前端调用:浏览器端实时检测Demo
DAMOYOLO-S模型JavaScript前端调用浏览器端实时检测Demo最近在折腾一些前端AI应用发现一个挺有意思的事儿很多厉害的模型比如目标检测非得在服务器上跑用户想体验一下还得上传图片或者依赖网络。我就琢磨能不能让模型直接在浏览器里跑起来打开网页调用摄像头就能实时看到检测效果试了一圈还真让我找到了一个不错的方案——把轻量化的DAMOYOLO-S模型搬到浏览器里。今天就跟大家分享一下怎么用JavaScript配合TensorFlow.js或者ONNX Runtime Web搞一个纯前端的实时摄像头目标检测Demo。用户啥都不用装点开链接就能玩体验还挺流畅的。1. 为什么要在浏览器里跑目标检测你可能觉得目标检测这种“重活”交给后端服务器不是更省心吗确实但前端直接跑模型有几个独特的优势尤其是在做实时交互Demo的时候。首先隐私性是最大的亮点。所有的图像数据都在用户的本地设备上处理压根不会上传到任何服务器。这对于涉及人脸、个人环境的摄像头应用来说用户心理上会感觉安全很多。其次实时性体验更好。因为少了网络来回传输的延迟从摄像头捕获画面到显示出检测框这个链路非常短。只要你的模型和代码优化得当在主流电脑或手机上都能达到不错的帧率感觉会很跟手。再者部署和分享变得极其简单。你不需要租服务器不需要配置复杂的环境。做完之后就是一个静态网页可以放在GitHub Pages、任何对象存储或者你自己的网站上。分享给别人就是一个链接对方点开即用没有任何使用门槛。最后它降低了后端负载和成本。对于展示性、体验性的应用如果每个请求都打到你的服务器去推理流量一大服务器成本和维护压力就上来了。前端化之后计算压力分散到了每个用户的设备上。当然缺点也很明显就是受限于用户设备的性能尤其是CPU和内存模型必须足够轻量化推理速度也要快。这也是为什么我们选择DAMOYOLO-S这类为边缘设备设计的轻量模型。2. 技术选型与准备工作要把一个训练好的模型放到浏览器里跑我们需要解决两个核心问题模型格式转换和浏览器端推理引擎。2.1 模型格式从PyTorch到Web友好格式我们常用的DAMOYOLO模型通常是PyTorch格式.pt或.pth。浏览器不能直接运行这种格式。我们需要把它转换成Web环境支持的格式。主要有两条主流路径TensorFlow.js 路径转换成 TensorFlow.js 格式.json 二进制权重文件。这是TensorFlow.js原生支持的格式集成起来最方便。ONNX Runtime Web 路径先转换成ONNX格式.onnx然后由ONNX Runtime Web在浏览器中加载和运行。ONNX作为一个开放的模型交换格式支持来自多种框架的模型。我个人的选择和建议是ONNX路径。原因有几个ONNX的生态现在非常活跃转换工具成熟ONNX Runtime Web对WebAssembly和WebGL后端支持很好性能表现稳定而且这条路径不依赖于特定的训练框架。本文后续也将以ONNX Runtime Web为主要工具进行讲解。2.2 推理引擎ONNX Runtime Web简介ONNX Runtime Web是一个专门为浏览器和Node.js环境设计的ONNX模型推理库。它通过几种技术来加速推理WebAssembly (WASM)将C编写的推理核心编译成WASM字节码在浏览器中以接近原生的速度运行。这是通用性最好、支持最广的后端。WebGL利用用户的GPU进行并行计算对于适合GPU加速的模型如卷积神经网络能获得显著的性能提升。WebNN(实验性)调用操作系统原生的神经网络API潜力最大但目前浏览器支持度有限。我们的Demo会优先尝试使用WebGL后端如果失败则回退到WASM后端以确保最大的兼容性。2.3 开发环境准备在开始写代码之前我们需要先把模型准备好。第一步安装转换工具我们需要onnx和onnx-simplifier这两个Python包。如果你训练好的DAMOYOLO-S模型是PyTorch的可能还需要torch和torchvision。pip install onnx onnx-simplifier # 如果你从PyTorch模型开始转换 # pip install torch torchvision第二步模型转换与简化这里假设你有一个训练好的DAMOYOLO-S模型文件比如damoyolo-s.pt。你需要编写一个转换脚本将模型导出为ONNX格式。这个过程的核心是提供一个正确的输入张量示例dummy_input并调用torch.onnx.export。一个非常关键的步骤是使用onnx-simplifier对导出的ONNX模型进行优化。它会对计算图进行优化、折叠常量、消除冗余操作能显著减小模型体积并提升推理速度。# convert_to_onnx.py 示例脚本框架 import torch import onnx from onnxsim import simplify # 1. 加载你的PyTorch模型 # model YourDamoyoloSModel() # model.load_state_dict(torch.load(damoyolo-s.pt)) # model.eval() # 2. 准备示例输入根据你的模型输入尺寸例如640x640 dummy_input torch.randn(1, 3, 640, 640) # 3. 导出ONNX模型 onnx_path damoyolo-s.onnx torch.onnx.export( model, dummy_input, onnx_path, input_names[images], output_names[output], # 输出名可能不止一个根据模型调整 opset_version12, # 使用一个较新且稳定的opset版本 dynamic_axes{images: {0: batch_size}}, # 支持动态batch ) # 4. 简化模型 onnx_model onnx.load(onnx_path) model_simp, check simplify(onnx_model) assert check, Simplified ONNX model could not be validated onnx.save(model_simp, damoyolo-s-sim.onnx) print(fModel simplified and saved to damoyolo-s-sim.onnx)转换完成后你会得到一个优化后的damoyolo-s-sim.onnx文件这就是我们要在浏览器中使用的模型。3. 构建前端实时检测Demo现在进入有趣的环节——写前端代码。我们会创建一个简单的HTML页面包含视频元素、画布和状态显示然后用JavaScript把它们串起来。3.1 项目结构与初始化创建一个新的项目文件夹结构如下browser-damoyolo-demo/ ├── index.html ├── script.js ├── style.css └── models/ └── damoyolo-s-sim.onnx (转换好的模型文件)在index.html中我们搭建基础界面!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title浏览器端DAMOYOLO-S实时目标检测/title link relstylesheet hrefstyle.css script srchttps://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js/script /head body div classcontainer h1 DAMOYOLO-S 浏览器实时检测/h1 p模型直接在您的浏览器中运行视频数据不会上传至任何服务器。/p div classvideo-container video idvideoFeed playsinline autoplay muted/video canvas idoutputCanvas/canvas /div div classcontrols button idstartBtn开启摄像头并开始检测/button button idstopBtn disabled停止检测/button label input typecheckbox idshowFps checked 显示FPS /label /div div classstats p状态: span idstatus等待开始.../span/p p推理时间: span idinferenceTime-- ms/span/p p检测数量: span iddetectionCount0/span/p pFPS: span idfpsCounter--/span/p /div div classinfo h3检测类别 (COCO示例)/h3 div idclassLegend/div /div /div script srcscript.js/script /body /htmlstyle.css负责让页面看起来整洁一点主要是让视频和画布重叠以及控制按钮和统计信息的布局。3.2 核心JavaScript逻辑script.js是我们的主战场。代码有点长我们分块来看。第一部分初始化与全局变量// script.js let videoElement document.getElementById(videoFeed); let canvasElement document.getElementById(outputCanvas); let canvasCtx canvasElement.getContext(2d); let startBtn document.getElementById(startBtn); let stopBtn document.getElementById(stopBtn); let session null; // ONNX Runtime会话 let isRunning false; let animationFrameId null; let lastTimestamp 0; let frameCount 0; let fps 0; // 模型参数 (必须与训练/转换时一致) const MODEL_WIDTH 640; const MODEL_HEIGHT 640; const INPUT_NAME images; // 假设你的模型输出是 [batch, num_detections, 6] 格式最后一维是 [x1, y1, x2, y2, conf, class_id] // 具体格式需要根据你的模型输出调整 // COCO类别标签 (示例请替换为你的模型实际类别) const CLASS_NAMES [person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic light, ... ];第二部分加载ONNX模型我们使用ONNX Runtime Web的API来异步加载模型。async function loadModel() { const statusEl document.getElementById(status); statusEl.textContent 正在加载模型...; try { // 注意模型文件需要放在你的服务器或通过合适的路径访问 // 这里假设模型文件在 models/ 目录下 session await ort.InferenceSession.create(./models/damoyolo-s-sim.onnx, { executionProviders: [webgl, wasm], // 优先尝试WebGL失败则用WASM graphOptimizationLevel: all, }); statusEl.textContent 模型加载成功; console.log(ONNX Runtime会话创建成功后端:, session); return true; } catch (error) { statusEl.textContent 模型加载失败: error.message; console.error(加载模型失败:, error); return false; } }第三部分视频流处理与推理这是最核心的函数它负责从视频中抓取一帧预处理后送入模型然后处理输出结果。async function runDetection() { if (!session || !isRunning) return; const startTime performance.now(); // 1. 从video元素中捕获当前帧图像 canvasElement.width videoElement.videoWidth; canvasElement.height videoElement.videoHeight; canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height); // 2. 获取ImageData并预处理 const imageData canvasCtx.getImageData(0, 0, canvasElement.width, canvasElement.height); const inputTensor preprocess(imageData); // 预处理函数见下文 // 3. 执行模型推理 const feeds { [INPUT_NAME]: inputTensor }; const results await session.run(feeds); const output results.output; // 注意这里的‘output’需要与模型导出时的输出名对应 // 4. 后处理解析检测框应用非极大值抑制(NMS) const detections postprocess(output, canvasElement.width, canvasElement.height); // 后处理函数见下文 // 5. 在画布上绘制原视频帧和检测框 canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height); drawDetections(canvasCtx, detections); // 绘制函数见下文 // 6. 更新统计信息 const inferenceTime performance.now() - startTime; document.getElementById(inferenceTime).textContent inferenceTime.toFixed(1); document.getElementById(detectionCount).textContent detections.length; updateFps(); // 7. 循环下一帧 if (isRunning) { animationFrameId requestAnimationFrame(runDetection); } }预处理函数 (preprocess)将ImageData转换为模型需要的归一化张量。function preprocess(imageData) { const { data, width, height } imageData; const imageTensorData new Float32Array(3 * MODEL_WIDTH * MODEL_HEIGHT); // 简化的预处理缩放图像到模型输入尺寸并归一化像素值到[0,1] // 注意更严谨的预处理应包括减均值、除标准差等需与模型训练时一致 const scaleX MODEL_WIDTH / width; const scaleY MODEL_HEIGHT / height; for (let y 0; y MODEL_HEIGHT; y) { for (let x 0; x MODEL_WIDTH; x) { // 双线性插值采样简化版此处为示意实际可使用canvas缩放 const srcX Math.floor(x / scaleX); const srcY Math.floor(y / scaleY); const srcIdx (srcY * width srcX) * 4; const dstIdxR y * MODEL_WIDTH x; const dstIdxG dstIdxR MODEL_WIDTH * MODEL_HEIGHT; const dstIdxB dstIdxG MODEL_WIDTH * MODEL_HEIGHT; imageTensorData[dstIdxR] data[srcIdx] / 255.0; // R imageTensorData[dstIdxG] data[srcIdx 1] / 255.0; // G imageTensorData[dstIdxB] data[srcIdx 2] / 255.0; // B } } // 创建ORT张量形状为 [1, 3, MODEL_HEIGHT, MODEL_WIDTH] return new ort.Tensor(float32, imageTensorData, [1, 3, MODEL_HEIGHT, MODEL_WIDTH]); }后处理与绘制函数解析模型输出的原始数据将其转换为画布上的框和标签。这里涉及非极大值抑制NMS算法代码较长其核心是过滤掉低置信度的预测框并合并重叠框。function postprocess(modelOutput, canvasWidth, canvasHeight) { // 假设modelOutput是一个形状为[1, num_boxes, 6]的张量 const predictions modelOutput.data; const numBoxes modelOutput.dims[1]; let detections []; const confidenceThreshold 0.5; // 置信度阈值 const iouThreshold 0.5; // NMS的IoU阈值 // 1. 收集所有超过置信度阈值的预测框 for (let i 0; i numBoxes; i) { const offset i * 6; const [x1, y1, x2, y2, conf, classId] [ predictions[offset], predictions[offset 1], predictions[offset 2], predictions[offset 3], predictions[offset 4], Math.round(predictions[offset 5]) ]; if (conf confidenceThreshold) { // 将坐标从模型输入尺寸(640x640)映射回原始画布尺寸 const scaleX canvasWidth / MODEL_WIDTH; const scaleY canvasHeight / MODEL_HEIGHT; const box { x1: x1 * scaleX, y1: y1 * scaleY, x2: x2 * scaleX, y2: y2 * scaleY, confidence: conf, class: classId }; detections.push(box); } } // 2. 按置信度排序 detections.sort((a, b) b.confidence - a.confidence); // 3. 简单的非极大值抑制(NMS)实现 const selectedDetections []; while (detections.length 0) { const current detections.shift(); // 取出置信度最高的 selectedDetections.push(current); // 计算当前框与剩余框的IoU移除重叠度过高的框 for (let i detections.length - 1; i 0; i--) { const iou calculateIoU(current, detections[i]); if (iou iouThreshold) { detections.splice(i, 1); } } } return selectedDetections; } function calculateIoU(box1, box2) { // 计算两个矩形框的交并比 const x1 Math.max(box1.x1, box2.x1); const y1 Math.max(box1.y1, box2.y1); const x2 Math.min(box1.x2, box2.x2); const y2 Math.min(box1.y2, box2.y2); const intersection Math.max(0, x2 - x1) * Math.max(0, y2 - y1); const area1 (box1.x2 - box1.x1) * (box1.y2 - box1.y1); const area2 (box2.x2 - box2.x1) * (box2.y2 - box2.y1); const union area1 area2 - intersection; return intersection / union; } function drawDetections(ctx, detections) { detections.forEach(det { const { x1, y1, x2, y2, confidence, class: classId } det; const label ${CLASS_NAMES[classId] || classId}: ${(confidence * 100).toFixed(1)}%; // 画框 ctx.strokeStyle #00FF00; ctx.lineWidth 2; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); // 画标签背景 ctx.fillStyle #00FF00; const textWidth ctx.measureText(label).width; ctx.fillRect(x1, y1 - 20, textWidth 10, 20); // 画标签文字 ctx.fillStyle #000; ctx.font 16px Arial; ctx.fillText(label, x1 5, y1 - 5); }); }第四部分控制逻辑与FPS计算最后我们把按钮事件和辅助函数串联起来。async function startDetection() { if (isRunning) return; // 请求摄像头权限 const stream await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: user } }); videoElement.srcObject stream; await videoElement.play(); // 加载模型如果尚未加载 if (!session) { const loaded await loadModel(); if (!loaded) return; } isRunning true; startBtn.disabled true; stopBtn.disabled false; document.getElementById(status).textContent 检测运行中...; // 开始检测循环 animationFrameId requestAnimationFrame(runDetection); } function stopDetection() { isRunning false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId null; } if (videoElement.srcObject) { videoElement.srcObject.getTracks().forEach(track track.stop()); videoElement.srcObject null; } startBtn.disabled false; stopBtn.disabled true; document.getElementById(status).textContent 已停止; canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); } function updateFps() { frameCount; const now performance.now(); if (now lastTimestamp 1000) { // 每秒计算一次 fps Math.round((frameCount * 1000) / (now - lastTimestamp)); lastTimestamp now; frameCount 0; if (document.getElementById(showFps).checked) { document.getElementById(fpsCounter).textContent fps; } } } // 事件监听 startBtn.addEventListener(click, startDetection); stopBtn.addEventListener(click, stopDetection); // 页面加载时预加载模型可选 window.addEventListener(load, () { loadModel(); // 预加载提升首次启动体验 });4. 性能优化关键点代码跑起来后你可能会发现帧率不够理想。浏览器的计算资源有限优化至关重要。1. 模型层面优化选择轻量模型DAMOYOLO-S本身就是一个好的起点。还可以探索更小的变体或使用剪枝、量化后的模型。模型量化将模型权重从FP32转换为INT8可以大幅减少模型体积和内存占用并提升在支持量化推理的后端如某些WASM优化上的速度。ONNX Runtime支持训练后量化。2. 推理引擎优化后端选择确保executionProviders顺序为[webgl, wasm]优先使用GPU加速。使用Web Workers将模型加载和推理过程放到Web Worker中避免阻塞主线程防止页面卡顿保持UI流畅。开启推理会话优化创建会话时设置graphOptimizationLevel: all。3. 前端代码优化降低输入分辨率如果实时性要求高于精度可以降低从摄像头捕获并送入模型的分辨率如从640x640降到320x320。这能显著减少计算量。跳帧处理对于性能较弱的设备可以不用每帧都检测比如每3帧处理1帧。优化预处理/后处理preprocess和postprocess函数中的循环是性能热点。可以尝试使用WebGL或WebAssembly自己写更高效的图像处理。使用ImageBitmapAPI或createImageBitmap来处理视频帧它们通常比直接操作ImageData更快。检查NMS的实现效率对于大量检测框简单的循环可能较慢。内存管理及时释放不再需要的Tensor和ImageData对象避免内存泄漏。4. 用户体验优化添加加载指示器模型加载可能需要几秒到十几秒显示进度条或提示文字。错误处理妥善处理摄像头权限被拒绝、模型加载失败、推理错误等情况给用户明确的反馈。响应式设计确保Canvas和视频元素在不同屏幕尺寸下都能正确显示。5. 总结把这个DAMOYOLO-S模型搬到浏览器里跑一遍感觉前端AI应用的潜力真的挺大。整个过程下来最关键的几步其实就是模型转换、选择合适的推理引擎比如ONNX Runtime Web以及处理好前后端分离的预处理和后处理逻辑。实际体验中在配备普通独显的电脑上经过优化的Demo达到20-30 FPS的实时检测是很有希望的。当然在低端设备或手机上可能需要通过降低分辨率、跳帧等策略来保证流畅度。这种纯前端的方案特别适合做产品原型、技术演示、教育工具或者对隐私要求高的轻量级应用。如果你也想试试建议先从简化版开始比如用固定的图片做检测跑通整个流程。然后再逐步加入摄像头流、优化性能。遇到问题多看看ONNX Runtime Web的文档和社区里面有很多宝贵的经验。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

更多文章