Skip to content

YOLO前端部署

前端部署对比

前端部署有两种方式:TensorFlow.jsONNX, 相对对比如下:

维度ONNX + ORT Web ✅ (推荐)TensorFlow.js (TF.js) ⚠️ (次选)
官方支持​YOLO可直接导出 ONNX 模型需要转换为 TensorFlow.js 模型
部署方式可用 ONNX Runtime Web(ort)可用 TensorFlow.js 部署
库体积极小(~60KB)较大(TF.js 库 > 200KB)
推理性能速度明显更快(比 TF.js 快 30%–100%)推理速度较慢(Web 端优化一般)
实时性YOLO 网页摄像头场景更稳更快-

因此,YOLO 前端部署推荐使用 ONNX

导出 ONNX 模型

python
model.export(format="onnx", imgsz=640, opset=17, simplify=True)
  • opset=17(推荐!速度 + 兼容平衡)
    • YOLOv8/v10/v11 推荐, ORT Web 完全支持,速度比 12 快 10–20%
    • Ultralytics 官方建议:最高用 20,不要更高版本。 17+ 更高版本部分旧版 ORT Web、移动端可能不兼容

前端示例

  • yolo-demo.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>YOLO 实时检测</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            background: #000;
            color: #fff;
            overflow: hidden;
        }
        #status {
            position: fixed;
            top: 10px;
            left: 0;
            width: 100%;
            text-align: center;
            color: #0f0;
            z-index: 99;
        }
        .box {
            width: 100vw;
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        video, canvas {
            position: absolute;
            max-width: 100%;
            max-height: 100%;
            object-fit: fill;
        }
        canvas {
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="status">模型加载中...</div>
    <div class="box">
        <video id="video" autoplay muted playsinline></video>
        <canvas id="canvas"></canvas>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.0/dist/ort.min.js"></script>
    <script>
        // 模型路径(放入 public/static-pages/)
        const modelPath = "/static-pages/yolo11n.onnx";
        // 模型输入尺寸 640×640
        const imgsz = 640;
        // ONNX 推理会话
        let session = null;

        // COCO 80 类名称
        const classes = ["person","bicycle","car","motorcycle","airplane","bus","train","truck","boat","traffic light","fire hydrant","stop sign","parking meter","bench","bird","cat","dog","horse","sheep","cow","elephant","bear","zebra","giraffe","backpack","umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball","kite","baseball bat","baseball glove","skateboard","surfboard","tennis racket","bottle","wine glass","cup","fork","knife","spoon","bowl","banana","apple","sandwich","orange","broccoli","carrot","hot dog","pizza","donut","cake","chair","couch","potted plant","bed","dining table","toilet","tv","laptop","mouse","remote","keyboard","cell phone","microwave","oven","toaster","sink","refrigerator","book","clock","vase","scissors","teddy bear","hair drier","toothbrush"];

        const video = document.getElementById("video");
        const canvas = document.getElementById("canvas");
        const ctx = canvas.getContext("2d");
        canvas.width = imgsz;
        canvas.height = imgsz;

        // ------------------------------
        // 加载 ONNX 模型
        // ------------------------------
        async function loadModel() {
            try {
                // 创建 ONNX 推理会话
                session = await ort.InferenceSession.create(modelPath);
                document.getElementById("status").innerText = "✅ 模型加载成功";
                await startCamera();
            } catch (e) {
                console.error(e);
                document.getElementById("status").innerText = "❌ 模型加载失败";
            }
        }

        // ------------------------------
        // 打开摄像头(后置)
        // ------------------------------
        async function startCamera() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({
                    video: { facingMode: "environment", width: imgsz, height: imgsz },
                    audio: false
                });
                video.srcObject = stream;
                video.onloadeddata = () => {
                    detectFrame();
                    document.getElementById("status").innerText = "📷 检测中";
                };
            } catch (e) {
                document.getElementById("status").innerText = "❌ 请允许摄像头权限";
            }
        }

        // ------------------------------
        // 图像预处理:归一化 + 格式转换 BCHW
        // ------------------------------
        function preprocess(videoElement) {
            const tempCanvas = document.createElement("canvas");
            tempCanvas.width = imgsz;
            tempCanvas.height = imgsz;
            const tCtx = tempCanvas.getContext("2d");
            tCtx.drawImage(videoElement, 0, 0, imgsz, imgsz);

            // 获取像素数据
            const data = tCtx.getImageData(0, 0, imgsz, imgsz).data;
            // 构建模型输入张量 [1, 3, 640, 640]
            const input = new Float32Array(3 * imgsz * imgsz);

            for (let i = 0; i < imgsz * imgsz; i++) {
                // R / G / B 分别归一化 0~1
                input[i] = data[i * 4] / 255;
                input[i + imgsz * imgsz] = data[i * 4 + 1] / 255;
                input[i + imgsz * imgsz * 2] = data[i * 4 + 2] / 255;
            }

            // 返回 ONNX Runtime 格式张量
            return new ort.Tensor("float32", input, [1, 3, imgsz, imgsz]);
        }

        // ------------------------------
        // 逐帧推理 + 绘制检测框
        // ------------------------------
        async function detectFrame() {
            if (!session) return;

            // 1. 预处理图像
            const tensor = preprocess(video);
            // 2. 模型推理
            const output = await session.run({ images: tensor });
            // 3. 获取输出结果
            const predictions = output.output0.data;

            // 清空上一帧画的框
            ctx.clearRect(0, 0, imgsz, imgsz);
            ctx.strokeStyle = "#0f0";
            ctx.lineWidth = 2;
            ctx.font = "16px Arial";
            ctx.fillStyle = "#0f0";

            // 遍历检测结果:每 6 个值为一组 (x1,y1,x2,y2,conf,cls)
            const confThreshold = 0.4;
            for (let i = 0; i < predictions.length; i += 6) {
                const x1 = predictions[i];
                const y1 = predictions[i+1];
                const x2 = predictions[i+2];
                const y2 = predictions[i+3];
                const conf = predictions[i+4];
                const cls = Math.floor(predictions[i+5]);

                // 过滤低置信度
                if (conf < confThreshold) continue;

                // 画框
                ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
                // 写类别名称
                ctx.fillText(`${classes[cls]} ${(conf*100).toFixed(0)}%`, x1, y1 - 5);
            }

            // 下一帧继续检测
            requestAnimationFrame(detectFrame);
        }

        // 启动程序
        loadModel();
    </script>
</body>
</html>

京ICP备2024093538号-1