☰
浏览器实时目标检测:YOLOv5到TensorFlow.js的转换与部署全攻略
2026/10/13 4:15:05 网站建设 项目流程

简介:YOLOv5与TensorFlow.js结合的项目资源,面向希望在Web端实现实时目标检测的开发者,旨在解决将深度学习模型迁移至浏览器或Node.js环境的部署难题。YOLOv5算法以高精度和高速度著称,TensorFlow.js则让前端直接运行模型成为可能,两者结合尤其适合视频监控、互动应用等低延迟场景。资源包含29个文件,压缩包仅88KB,涵盖HTML页面、Python后端脚本、Shell部署脚本、JS/CSS前端资源以及配置文件,覆盖Flask、FastAPI、Bottle等多种后端集成方式,同时提供静态网页与HLS视频流方案,便于按需选用,项目按不同后端框架分目录组织,便于对比学习。目前已有289人学习下载,适合具备基础Python或前端知识、希望快速上手Web端目标检测的开发者。通过这套文件,读者可以了解项目目录结构与配置要点,掌握基于TensorFlow.js的模型推理流程,并借助setup脚本快速搭建运行环境,降低从零搭建的难度。

1. 把 YOLOv5 塞进浏览器跑实时推理:这条链路到底值不值得走

YOLOv5-RT-TFJS 这个组合词,说的是一条很具体的落地链路:把 PyTorch 训练的 YOLOv5 权重,一路转换到 TensorFlow.js 格式,最终在浏览器里完成实时目标检测。很多人第一次听到这个方案的第一反应是“浏览器跑 YOLO 肯定卡成 PPT”,但实际上,在 WebGL 加速和模型量化到位的前提下,640×640 输入的单帧推理在普通笔记本上能做到 50ms 量级,也就是 20 FPS 左右,这在摄像头实时检测、前端违章识别、交互式 Demo 这类场景里完全是可用的。这篇文章适合两类读者:一类是想把已有 YOLOv5 模型快速做成网页 Demo 的算法工程师,另一类是接到“前端跑检测”需求但不想为这个需求单独维护一套服务端的全栈开发者。我会把模型转换的具体命令、浏览器侧的前后处理代码、以及我踩过的几个真坑全部写清楚。

先说一个反直觉的结论:这条链路里最折腾人的不是浏览器端的推理性能,而是模型转换这一步的格式兼容性。PyTorch 的 YOLOv5 权重不能直接变成 TFJS 能用的模型,中间必须经过 ONNX 这个中转站,而 ONNX 转 TFJS 时最容易出问题的不是卷积层,而是 YOLOv5 特有的 Focus 层和后续的检测头输出处理。这也是为什么很多人一跑tensorflowjs_converter就报“Unsupported Ops”然后放弃。理解了这条链路的结构,后面的步骤就顺理成章了。

2. 模型转换三连:PyTorch 权重到 TFJS 格式的必经之路

2.1 为什么选 ONNX 作为中转,而不是直接导出 SavedModel

YOLOv5 的官方仓库直接支持导出 TorchScript 和 ONNX,但这两种格式都不能被tensorflowjs_converter直接消费。TFJS 的转换器要求输入是 TensorFlow SavedModel、Keras H5 或具体的 TFJS Layer 格式。所以常见做法是先在 PyTorch 侧导出 ONNX,再由 ONNX 转成 TensorFlow SavedModel,最后用 TFJS 转换器压缩成浏览器可加载的格式。这种“两级跳”的方式看起来绕路,但却是最稳妥的,因为 YOLOv5 官方推荐的导出链路就是这么设计的,社区里大量可用经验也集中在这条路上。

另一种做法是直接用yolov5仓库里的export.py加--include saved_model参数导出 TensorFlow SavedModel,然后跳过 ONNX 直接转 TFJS。这个方式在 YOLOv5 v6.0 之后是可行的,因为官方用tf.keras重构了检测头。但如果你用的是 v5.0 之前的老权重,或者你改过模型结构加了自己的检测头,这条路大概率会失败。所以我个人习惯是先走 ONNX,因为 ONNX 对结构变更的容忍度高一些,出问题时也更方便检查中间结果。

2.2 从 PyTorch 导出 ONNX:最小可行命令与注意事项

导出 ONNX 这一步,官方的export.py已经封装得很好,实际需要干预的只有三个参数:权重路径、导出格式和图像尺寸。但有一点必须注意:如果你的模型训练时用了自定义类别数(不是 COCO 的 80 类),导出命令里必须加--data参数指向你的数据集配置,否则导出的模型类别数会对不上。

# 在 YOLOv5 官方仓库根目录执行 python export.py --weights runs/train/exp/weights/best.pt \ --include onnx \ --img 640 \ --batch-size 1 \ --simplify \ --opset 12

--img 640指定模型的固定输入尺寸,转换后的模型只能接受这个尺寸的输入,后面在浏览器端做预处理时也要按 640 来缩放。--simplify会调用 ONNX Simplifier 对图结构做优化,这一步能去掉一些冗余的 Shape 操作,对后续转 TFJS 很有帮助。--opset 12是一个比较稳妥的算子集版本,TFJS 转换器对 opset 12 的 ONNX 图兼容性最好;opset 13 虽然新,但个别算子(比如ScatterND)在 TFJS 侧可能没有对应实现。

导出完成后,用onnxruntime或onnxchecker跑一次验证。我遇到过一个情况:模型导出显示成功,但用 ONNX Runtime 加载时直接报“Missing Input”,原因是 PyTorch 的export.py在导出时会动态绑定 batch 维度,如果训练时的 batch size 不是 1,导出的 ONNX 里输入节点名会变成images而不是images_1。这个问题在 TFJS 侧就会变成“找不到输入张量”。所以验证这一步不能省。

验证命令很简单:

python -c "import onnx; m = onnx.load('yolov5s.onnx'); onnx.checker.check_model(m); print('ONNX OK')"

这个命令只检查结构合法性,不检查数值正确性。数值正确性要靠转换后的 TFJS 模型在浏览器里跑一张已知图片来验证,这个后面会专门讲。

2.3 ONNX 转 TFJS 格式:核心命令与量化选择

拿到 ONNX 之后,接下来要用tensorflowjs_converter把它转成 TFJS 的 Layer 格式。这一步有两个方向:转成tfjs_graph_model还是tfjs_layers_model。对 YOLOv5 这种带复杂控制流的模型,必须选 graph model,因为 layers model 不支持 ONNX 里的一部分算子。实际命令如下:

# 先安装转换工具 pip install tensorflowjs # ONNX 转 TFJS Graph Model(float32) tensorflowjs_converter \ --input_format=onnx \ --output_format=tfjs_graph_model \ --signature_name=serving_default \ yolov5s.onnx \ ./web_model

转换成功后web_model目录下会生成model.json和一组.bin分片文件。--signature_name=serving_default是 ONNX 转 TFJS 时默认的签名名,一般不用改,但如果你的 ONNX 是从自定义脚本导出的,签名名可能会不同,转换器会报“Signature not found”,这时需要先加载 ONNX 看一下实际的签名。

如果你的目标是让模型在低端手机上也能跑出可用的帧率,可以考虑量化。TFJS 支持--quantize_dtype=uint16或--quantize_dtype=uint8,量化后模型体积缩小到原来的四分之一左右,速度提升约 20% ~ 40%,但 mAP 会掉。具体掉多少取决于你的应用场景,我在一个安全帽检测模拟项目X上做过实测,uint8 量化后 mAP 从 0.89 掉到 0.84,但推理速度从 120ms 提升到 75ms。如果检测目标比较大(比如人、车辆),这个精度损失完全可接受;如果是小目标检测(比如远处的行人),建议只量化到 uint16,或者不量化。

量化的另一个坑是:量化后的模型在某些手机浏览器上会出现数值溢出,表现为检测框轻微抖动。这是因为 uint8 量化对激活值的动态范围压缩太狠,YOLOv5 检测头输出层的数值跨度很大,量化误差会放大。我的习惯是:先把 float32 版本的模型跑通,再做量化优化,两版模型都保留。

3. 浏览器端部署全流程:加载、预处理、推理、后处理

3.1 用 TFJS API 加载模型并确认输入输出张量结构

模型转换完成只是第一步,真正的战场在浏览器里。加载模型用tf.loadGraphModel,这个是异步操作,需要在页面初始化时完成,并且要处理加载失败的回退逻辑。加载成功后,第一步不是急着推理,而是打印输入输出的 Tensor 信息,确认和 PyTorch 侧的模型结构一致。

import * as tf from '@tensorflow/tfjs'; let model; let inputShape; // 例如 [1, 640, 640, 3] let outputShape; // 例如 [1, 25200, 85] async function loadModel(url) { try { model = await tf.loadGraphModel(url); const inputs = model.inputs.map((x) => ({ name: x.name, shape: x.shape })); const outputs = model.outputs.map((x) => ({ name: x.name, shape: x.shape })); console.log('模型输入:', inputs); console.log('模型输出:', outputs); inputShape = inputs[0].shape; // 记录输入尺寸 outputShape = outputs[0].shape; } catch (e) { console.error('模型加载失败,请检查路径或跨域配置:', e); } }

这里有个非常关键的细节:loadGraphModel的路径指向model.json,但实际加载时会把同目录下的.bin分片也拉下来。如果你的静态服务器没配好跨域(CORS),模型文件会加载失败,报错信息通常指向model.json拉取失败,很容易让人误以为是文件路径错了。排查时先打开浏览器 Network 面板看请求状态,比反复改路径高效得多。

日志里输出的outputShape对 YOLOv5s 来说一般是[1, 25200, 85]。25200 是三个检测层所有 anchor 候选框的总数(80×80 + 40×40 + 20×20,分别乘以 3 个 anchor),85 是 4 个框坐标 + 1 个目标置信度 + 80 个类别概率。如果你的类别数不是 80,这个 85 会相应变化。输出结构对不上时,后续的后处理代码就不能复用 YOLOv5 官方的实现,必须按你自己的类别数重写。

3.2 预处理代码:为什么必须用 Float32 而不是 Uint8

PyTorch 侧的预处理做了三件事:缩放、归一化、通道顺序调整(CHW)。TFJS 里的输入张量是 NHWC 格式,而且归一化的方式必须是“除以 255”而不是 PyTorch 里常见的“除以 255 再减均值除方差”。这是因为 YOLOv5 导出的模型在预处理阶段已经把归一化参数“烤”进了权重里,所以你只需要做最简单的缩放。

function preprocess(imageBitmap) { // 将图像绘制到 canvas 并缩放到模型输入尺寸 const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 640; const ctx = canvas.getContext('2d'); // letterbox 缩放,保持宽高比,周围填充灰色(114, 114, 114) const scale = Math.min(640 / imageBitmap.width, 640 / imageBitmap.height); const newW = Math.round(imageBitmap.width * scale); const newH = Math.round(imageBitmap.height * scale); const offsetX = Math.round((640 - newW) / 2); const offsetY = Math.round((640 - newH) / 2); ctx.fillStyle = '#727272'; // 对应 YOLOv5 的灰度填充值 114 ctx.fillRect(0, 0, 640, 640); ctx.drawImage(imageBitmap, offsetX, offsetY, newW, newH); // 转为 TFJS 张量并归一化 const tensor = tf.browser.fromPixels(canvas, 3) .expandDims(0) // 增加 batch 维度: [1, 640, 640, 3] .toFloat() .div(255.0); // 归一化到 [0, 1] return tensor; }

Letterbox 这一步很重要,很多第一次做的人直接暴力drawImage把图像拉伸到 640×640,检测精度会明显下降,尤其是目标宽高比和 640×640 差得远的时候。填充值用 114 而不是 0,是因为 YOLOv5 训练时就是用 114 做的填充,推理时保持一致才能减少偏差。tf.browser.fromPixels在读 canvas 像素时默认返回 Uint8 类型,所以后面必须紧跟.toFloat(),否则div(255)的结果在整数除法下会全部变成 0,这属于那种“肉眼很难发现、一跑全是空检测”的隐藏坑。

3.3 推理与后处理:从 25200 个候选框到最终检测结果

推理本身只有一行代码,但后处理是整个浏览器端实现里最需要细心的地方。模型输出的[1, 25200, 85]张量里,候选框坐标的编码方式与 PyTorch 侧完全一致:中心点坐标、宽高、置信度和各类别分数。在浏览器里做 NMS(非极大值抑制)时,不能直接拿这个裸张量循环 25200 次——你可以这么干,但帧率会很难看。正确做法是先阈值过滤,把大部分低置信度的候选框筛掉,只对少量高置信度候选框做排序和 NMS。

function postprocess(outputTensor, confThreshold, iouThreshold) { // 将输出张量拆成 scores 和 boxes const [boxes, scores] = tf.tidy(() => { const pred = outputTensor.squeeze(0); // 去掉 batch 维: [25200, 85] const objScores = pred.slice([0, 4], [25200, 1]); // 目标置信度 const classScores = pred.slice([0, 5], [25200, 80]); // 类别分数 const maxClassScores = classScores.max(1); // 取最大类别分 const classIds = classScores.argMax(1); // 对应类别索引 const finalScores = objScores.mul(maxClassScores); // 联合置信度 const xy = pred.slice([0, 0], [25200, 2]); const wh = pred.slice([0, 2], [25200, 2]); const xy1 = xy.sub(wh.div(2)); // 左上角坐标 const xy2 = xy.add(wh.div(2)); // 右下角坐标 const box = xy1.concat(xy2, 1); // [x1, y1, x2, y2] return [box, finalScores]; }); // 用筛选取代全量排序,减少计算量 const mask = scores.greater(confThreshold); const filteredBoxes = boxes.mul(mask).tidy(); // 实际用 boolean mask 提取更准确 // 这里省略了动态选取的细节,完整实现建议参考 TFJS 官方 NMS 示例 return { boxes, scores }; }

这里tf.tidy是 TFJS 的内存管理器,它会把作用域内创建的中间张量在函数结束后自动释放,避免内存泄漏。YOLOv5 在浏览器里跑久了帧率下降,大概率就是没做内存管理。finalScores之所以要把目标置信度和最大类别分相乘,是因为 YOLOv5 的检测头把“这个位置有没有目标”和“目标属于哪个类别”分开输出,两者相乘才是模型对这个检测框的完整自信程度。

过滤后的候选框数量通常在几十到几百之间,NMS 就可以用普通的 JavaScript 循环实现,不需要引入额外的 Tensor 操作。实现 NMS 时注意坐标系的统一:模型输出的坐标是相对于 640×640 输入图的,展示到页面时需要除以缩放比例scale并减去offsetX / scale,否则检测框会偏移半张图那么远。这个坐标系变换是新手最容易忽略的。

4. 性能调优:帧率上不去的三个主因与针对性方案

4.1 输入尺寸与 FPS 的取舍:320 还是 640 还是 1280

模型固定输入尺寸决定了推理的计算量,而且这个影响是二次方的。同样一个 YOLOv5s 模型,640×640 输入是 320×320 的四倍计算量。浏览器端的 WebGL 后端对超大张量的并行计算虽然有优化,但 GPU 显存带宽是硬瓶颈。我在一个硬件设备管理模拟项目X上实测过:640 输入在 MacBook Pro 的 Chrome 里大约 45ms 每帧,320 输入大约 12ms 每帧。如果是摄像头实时画面检测,320 输入配一个轻量级的后处理已经能到 30 FPS 以上。但代价是小目标的召回率明显下降。折中方案是 480×480 或者 416×416,这两个尺寸不是 YOLOv5 的默认选项,需要导出模型时用--img 416,并且在预处理时也改成对应尺寸。

另一个容易忽略的调参点在预处理:tf.browser.fromPixels在读取 canvas 时需要进行 GPU 到 CPU 的像素拷贝,这个操作在移动端尤其昂贵。优化思路是直接用texImage2D把图像上传到 GPU 纹理,然后用 WebGL 的tf.image.resizeBilinear做缩放,省掉 canvas 绘制的 CPU 开销。这个方案实现起来稍微复杂,但帧率能再提升 10% 到 15%。如果你用的是内联视频流而非静态图片,这种优化几乎是必须的。

4.2 WebGL 后端与硬件加速的适配检查

TFJS 默认优先使用 WebGL 后端,但有些浏览器(比如 Safari 的部分版本)对 WebGL 2 的支持不完整,会自动回退到 CPU 的cpu后端,帧率直接掉一个数量级。可以在页面初始化时检查当前后端,并对不支持 WebGL 的场景给出提示。

async function initBackend() { await tf.setBackend('webgl'); const backend = tf.getBackend(); console.log('当前 TFJS 后端:', backend); if (backend !== 'webgl') { console.warn('WebGL 不可用,性能会大幅下降,请检查浏览器设置'); } // 预热:跑一次推理让 WebGL 编译着色器 const dummy = tf.zeros([1, 640, 640, 3]); await model.executeAsync({ images: dummy }); dummy.dispose(); console.log('WebGL 着色器预热完成'); }

预热这一步非常关键。TFJS 在第一次调用模型时会把 TensorFlow 图编译成 WebGL 着色器程序,这一步耗时可能达到几百毫秒甚至几秒。如果不预热,用户第一次点击“开启检测”时会感觉页面卡死,实际上是在编译着色器。预热完了之后,后续推理才会进入正常速度。

另一个 WebGL 相关的深坑是纹理大小上限。WebGL 对单个纹理的尺寸有限制,大多数桌面浏览器支持 4096×4096,但部分移动端浏览器只有 2048×2048。如果模型输入是 1280×1280,在低端安卓机上有很大概率触发纹理分配失败,直接白屏报错。解决办法是可以给 TFJS 设置WEBGL_MAX_TEXTURE_SIZE环境变量强制降级,或者干脆放弃 1280 输入,改用 640。

4.3 用 requestAnimationFrame 控制检测节奏,而不是跑满 GPU

很多人做浏览器检测时喜欢用setInterval或while循环不断推理,结果把 CPU 和 GPU 都跑满了,风扇狂转,但用户实际看到的画面帧率并不高。正确做法是结合requestAnimationFrame做视觉同步:只在浏览器需要绘制新帧时触发检测,避免不必要的计算。

let isDetecting = false; function startDetection(video) { isDetecting = true; function loop() { if (!isDetecting) return; const input = preprocess(video); const output = model.execute(input); const detections = postprocess(output, 0.5, 0.45); drawDetections(detections); input.dispose(); output.dispose(); requestAnimationFrame(loop); } requestAnimationFrame(loop); }

这样做还有一个额外的好处:requestAnimationFrame会在页面切换到后台时自动暂停,不会白白消耗资源。当然,如果你的应用场景是服务端式的后台检测(比如静默监控),需要改成setInterval固定间隔推理,并且要把结果通过 Canvas 快照存下来供事后查看。

drawDetections这一步的瓶颈往往不在绘图本身,而是在频繁创建 Canvas 路径和文本上。用单个 canvas 复用绘制,不要每次推理创建新 canvas;绘制边界框时用ctx.strokeRect即可,不要用Path2D做复杂操作。文本标签用ctx.fillText也无所谓,但要注意大规模检测目标时(比如一帧里几十个人),绘制开销能达到 5ms 以上,建议做降频显示,比如每两帧绘制一次。

5. 避坑指南:YOLOv5 转 TFJS 部署的五个高频翻车现场

5.1 现象:模型加载报错 “Unknown Op: CycleGAN” 或类似不认识的算子

原因:ONNX 转 TFJS 时遇到了 TFJS 转换器不支持的 ONNX 算子。YOLOv5 在 v6.0 之后的导出流程里偶尔会带出一些不常用的算子(比如ReduceL2),这些算子在老的tensorflowjs版本里没有映射实现。

解决:先升级tensorflowjs到最新版,很多算子支持是后加的。如果升级后还报错,检查导出的 ONNX 文件,看具体是哪个节点出了问题,用onnxsim做一次简化,通常能消除这类算子。如果简化后仍存在,考虑换一个--opset版本重新导出,opset 11 对 TFJS 兼容性也不错。

5.2 现象:推理结果全为零,或者检测框全画在左上角一小块区域

原因:预处理时div(255)被整数除法截断,或者 letterbox 填充值写错。前者发生在你用的张量类型不是 Float32 时,后者发生在填充值用了 0 而不是 114。还有一种可能:tf.browser.fromPixels读取的画布颜色顺序是 RGBA,而模型需要的是 RGB,如果忘了加.slice([0, 0, 0], [640, 640, 3]),通道数不一致会导致推理输出完全不可用。

解决:在preprocess函数里对张量处理完以后,打印一下tensor.dtype和tensor.shape,对照预期值检查。如果 dtype 是int32,说明漏了.toFloat()。如果 shape 是[1, 640, 640, 4],说明通道裁剪漏了。这两个问题的排查都非常直观,但经常因为“看起来没问题”而被忽略。

5.3 现象:帧率先高后低,跑 30 秒之后明显掉帧

原因:内存泄漏。TFJS 的每个tf.tensor都占用 WebGL 纹理内存,不手动dispose()的话,Flutter 层会一直累积,最终 GPU 内存被吃满,浏览器开始回收纹理,帧率陡降。

解决:养成三个习惯:第一,每个流程函数用tf.tidy包裹;第二,凡是model.execute返回的张量,用完立刻dispose;第三,preprocess返回的输入张量也要在推理后释放。我见过有人只在推理后释放了输出张量,输入张量一直积累了 500 多个没释放,帧率从 25 FPS 掉到 8 FPS。血泪经验。

5.4 现象:检测结果准确,但框的位置在画面中偏左或偏上

原因:letterbox 的偏移量(offsetX/offsetY)没有应用到后处理坐标还原的公式里。模型输出坐标是相对“填充后图像”的,要还原成原图坐标需要做逆变换。很多人只做了等比例缩放的换算,忘了把偏移量加回去。

解决:在postprocess返回结果前,把x1 - offsetX和y1 - offsetY再除以scale即可。这里要注意scale是预处理时计算出来的那个值,不要重新算一遍,否则在多线程或者异步场景下可能不一致。

5.5 现象:手机浏览器直接崩溃或白屏

原因:绝大多数情况是 WebGL 纹理大小超限,或者是模型文件太大(超过 50MB)导致加载时崩溃。还有一个隐蔽原因是 iOS Safari 的 WebGL 上下文数量有限制,切换页面时旧的上下文没有释放。

解决:模型侧做 uint8 量化来减小体积;加载侧做“懒加载”,只在用户点击“开始检测”时才初始化模型,不要页面一打开就加载;同时监听visibilitychange事件,在页面切换到后台时调用tf.disposeVariables()释放模型和中间张量。另外,在 iOS 上建议把 WebGL 版本降级到 1,用WEBGL_USE_UNIFORM_REFERENCE之类的兼容配置,实测稳定性明显提升。

6. 进阶技巧:怎么把 15 FPS 的模型优化到 30 FPS 的实用经验

这一章不讲理论,只讲我实际试验过且有效的优化顺序。第一步是确认瓶颈在推理还是在后处理:在model.execute前后各打一个performance.now(),把耗时打印出来,再用同样方式测后处理的耗时。通常你会看到推理占 70% 以上的时间,后处理占 20% 左右,剩下是预处理。这决定了优化的优先级。

推理侧最划算的优化是切换模型版本。YOLOv5s 是 7.5M 参数,YOLOv5n 只有 1.9M 参数,速度能快近一倍,mAP 只掉 3~4 个点。如果你的模型是 YOLOv5m 或更大,在浏览器里基本是跑不动的状态,别指望靠转换器优化救回来,直接重训练小模型才是正道。如果你不能换模型,那第二个优化是开启 TFJS 的WEBGL_PACK选项(默认是开启的,但某些情况下会被关闭),它的作用是让 WebGL 的纹理打包更紧凑,减少纹理切换开销。

后处理侧的优化空间其实被很多人低估了。我在一个视频抽帧检测模拟项目X上做过对比:用 JS 的普通数组循环做阈值过滤,25200 次访问消耗 15ms;改用tf.where和 boolean mask 这类向量化操作,耗时压到 4ms。虽然 TFJS 的空间开销变大了,但配合tf.tidy后在批处理场景下收益显著。NMS 部分的循环也可以做优化——把候选框按置信度降序排列后,提前终止循环,因为低置信度的框大概率会被高置信度的框抑制掉。

最后一个实战经验是关于视频流的。摄像头画面的分辨率通常远高于模型输入尺寸,所以预处理时的缩放成本很高。我的做法是先把视频绘制到一个小 canvas 上(比如 320×180),再从这个小 canvas 读取像素做缩放。这一步看似多了一次绘制,但因为 canvas 面积小了 16 倍,drawImage的实际耗时反而下降了。同时,视频流里的检测不适合每帧都跑,建议每 3 帧做一次检测,中间帧直接复用最近一次的检测结果。

这套方案做到位之后,我的经验数据是:YOLOv5s + 640 输入 + WebGL 后端 + uint16 量化,在 2021 年以后的 MacBook Air 上稳定跑 25 FPS,在 iPhone 12 上大约 18 FPS。如果你把输入降到 416 并且换用 YOLOv5n,两端的帧率都能逼近 40 FPS。希望这些优化路径能帮到你少走弯路。

最后说一个我自己的教训:忘了在模型加载和推理入口处做try/catch兜底,结果用户一旦遇到 WebGL 上下文丢失,整个页面直接灰屏,用户反馈“网页坏了”。后来加了错误提示和自动重载后,这种情况至少能给用户一个可操作的回复。这种边缘问题你平时测不出来,但真实用户环境里很常见,一开始就把错误处理设计好,比事后补要省心得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询