Appearance
YOLO前端部署
前端部署对比
前端部署有两种方式:TensorFlow.js 和 ONNX, 相对对比如下:
| 维度 | 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>