清晨的超市里,一位老练的果品分拣员站在传送带前,眼睛快速扫过每一个经过的苹果——不仅要判断“这是不是苹果”(分类),还要在瞬间喊出“这个苹果在左侧第三个格子,有点碰伤”(定位检测)。他不需要把每个水果拿起来仔细端详,靠的是多年训练出的条件反射:眼睛捕捉、大脑识别、手指挥动,一气呵成。 浏览器端的图像分类与目标检测,干的正是这档子事。只不过“分拣员”换成了浏览器里跑着的 JavaScript 代码,“传送带”换成了摄像头实时采集的视频流,“喊话”换成了在画布上画出一个个检测框。传统上,这种“视觉识别”被认为是后端服务器的专利——毕竟训练一个模型动辄需要 GPU 集群,推理一次也要消耗不少算力。但 TensorFlow.js 和 ONNX.js 等工具的出现,让浏览器这个“轻量级选手”也能扛起视觉识别的大旗。 这就像分拣员不是非要坐在中央控制室里操作超级计算机,而是直接在传送带旁支起一个小摊位,现场分拣、现场贴标。省去了“把水果送到中央仓库、等结果传回来”的漫长往返,代价则是摊位的空间有限、人手有限——你需要精打细算地安排每一步,才能在保持流畅的同时不把摊位挤爆。 这篇文章,就是带你走进这个“浏览器分拣台”,看看端侧的图像分类与目标检测究竟如何运转:如何从摄像头取流、如何批量处理推理请求、如何把检测结果画到屏幕上、又如何在一个低端手机上依然保住 30 帧的流畅体验。你会发现,这些问题的答案,本质上是一个“如何在有限资源下做最优调度”的故事。
掌握浏览器端图像分类与目标检测,意味着你能将 AI 能力直接嵌入 Web 产品,无需后端即可实现实时视觉交互——这是前端工程师迈向 AI 应用开发的关键一步,也是边缘计算浪潮中极具价值的技能储备。
可视化演示
展示端侧推理流水线、帧率优化策略对比及目标检测框渲染后处理流程。切换模式查看不同环节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
摄像头取流
分拣员上岗的第一件事是什么?不是练识别能力,而是先找到“眼睛”——架好摄像头,对准传送带。在浏览器里,这个“架摄像头”的动作就是 getUserMedia API。它像一把钥匙,打开设备的摄像头权限门,然后源源不断地把视频帧“传送”到浏览器里。但这里有个细节:摄像头给的是“视频流”,而 AI 模型吃的是“图片帧”——这就好比传送带上的水果是一个接一个连成线的,分拣员必须一张张“定格”来看。
于是,开发者需要从视频流中“抽帧”。最常见的做法是把视频帧绘制到一个隐藏的 <canvas> 上,再从画布里读出像素数据(ImageData),转成 TensorFlow.js 的张量。这中间有一个微妙的权衡:分辨率越高,模型看得越清楚,但推理耗时越长、内存占用越大。你不可能把 4K 的每一帧都塞给模型——就像分拣员不可能把传送带上的每个水果都拿起来翻个面端详。
不妨定义取流到画面帧的转换过程为一个简单映射。设摄像头输入帧的原始分辨率宽度为 \( W_0 \)、高度为 \( H_0 \),模型期望输入宽度为 \( W_m \)、高度为 \( H_m \),则缩放系数为:
除了分辨率,还有权限处理这个“看门”环节。用户拒绝授权、设备没有摄像头、浏览器不支持 getUserMedia……每一种异常都需要优雅降级,而不是让页面白屏。这就像分拣台前拉了根警戒线,你得先确认分拣员有资格站到这个位置。
回扣思路:取流是整个流程的“眼睛”,但眼睛也不是越亮越好——你要在“看得清”和“看得快”之间找到平衡点,这正是后续所有优化的起点。
- 模型输入宽度比计算宽度方向上的缩放需求
- 模型输入高度比计算高度方向上的缩放需求
- 取最小值选择两个比值中较小的,确保画面完全纳入且不变形
推理批处理
分拣员的瓶颈不在眼睛,而在大脑——每识别一个水果都需要几十毫秒的思考时间。如果传送带上的水果一个接一个地来,一个一个地思考,很快就会堆积如山。聪明的分拣员会怎么做?他会攒几个再一起处理:这一眼扫过去,同时看清三个苹果、两个梨、一个橙子。这就是“批处理”(batch processing)。
在浏览器推理中,批处理的典型做法是把多帧图像堆叠成一个高维张量,一次性交给模型推理。假设每一帧被缩放为 \( 224 \times 224 \times 3 \) 的 RGB 张量,现在把 \( b \) 帧合并为一个批次,那么这个批次张量的形状就是:
批处理能大幅提升吞吐量,但并非越大越好。批大小增加,单次推理的延迟会上升——分拣员一次看 100 个水果,思考时间可能比看 10 个水果长好几倍。在实时交互场景中,你需要找到“吞吐量最大但单帧延迟仍可接受”的平衡点。这就是批大小权衡的本质。
异步队列是批处理的另一半。浏览器里,模型的推理是异步的:你发出一帧,等待结果返回,期间并不阻塞主线程。就像一个好脾气的分拣员,把看不完的水果先放到一边,等手头这批处理完了再继续——tf.nextFrame() 和 Promise 机制保证了主流程不会因为等待推理而卡死。
回扣思路:批处理是“摊位的批量接单智慧”——不是一次接待一个客人,而是把相近时间到达的订单攒起来一起做,用略微增加的单单等待时间,换来整体接待能力的跃升。
[b, 224, 224, 3] 可以理解为把 b 个“水果照片”摞成一叠,变成一个四维数据块。模型一次处理这一叠,相当于分拣员一眼扫过 b 个水果。- 帧数维度批大小 \( b \),表示一次处理多少帧
- 空间维度每帧的高度和宽度
- 通道维度RGB 三个颜色通道
检测框渲染
分拣员识别出水果之后,不能只在脑子里记着,得喊出来、贴上标签、甚至指给你看。在浏览器端,模型输出的“指给你看”就是检测框(bounding box)——一组 [x, y, width, height] 坐标加上类别和置信度。渲染这些框的工作,落到了 Canvas 2D API 身上。
渲染不是简单地画个方框。模型可能输出几百个候选框,其中大部分是重复的或者低置信度的噪声。你需要先把置信度低于阈值的框丢弃,再用非极大值抑制(NMS)去掉重叠的框。所谓阈值筛选,就好比分拣员只报告“90% 确定是苹果”的结果,对“50% 可能是苹果”的模糊判断保持沉默。
假设模型对某个候选框输出的原始置信度为 \( c \),阈值设定为 \( \theta \; (0 < \theta < 1) \),则该框被保留的条件为:
渲染与推理解耦是保持流畅的关键。推理在后台进行,渲染在主线程绘制,两者不该互相阻塞。常见的做法是:推理结果放入一个“结果缓冲区”,渲染循环每次从缓冲区取最新结果绘制,即使推理掉了帧,画面也不会卡顿——就像分拣员的大脑在思考时,嘴巴已经在说上一个结果了,两者各干各的,井水不犯河水。
回扣思路:检测框是分拣员“说出口的话”。话说得太多(阈值太低)会吵死人,话说得太少(阈值太高)又会漏报。恰到好处的阈值 + 流畅的“嘴皮子”(渲染解耦),才是好的表达。
- 模型置信度模型输出对该框内物体属于某类别的概率
- 判定阈值设定的最低置信度门槛
- 保留条件置信度不低于阈值时,候选框被保留并渲染
帧率优化
分拣员最怕什么?传送带跑得比他的思考速度快。一旦来不及处理,水果就会堆积、掉落、甚至堵住传送带。浏览器的“传送带”就是摄像头的帧率——假设摄像头以 30 FPS 输出帧,而模型推理一次需要 100ms(即 10 FPS),那么推理速度跟不上了,必须想办法。
第一个办法是“跳帧”。就像分拣员不会对每一个经过的水果都伸手去拿,而是有选择性地每隔几帧取一个来识别。定义推理频率 \( f_{\text{infer}} \) 和摄像头帧率 \( f_{\text{cam}} \),则跳帧间隔 \( k \) 为:
第二个办法是“降采样”。把送入模型的图像尺寸从 \( 640 \times 640 \) 降到 \( 320 \times 320 \),计算量大约会降到原来的四分之一(因为卷积操作的参数量与输入像素数成正比)。代价是精度下降——小图上小物体会更难被检测到。这就像分拣员离传送带远一点看,能看清大西瓜,但可能看不清小草莓。
第三个办法是“频率分离”。渲染保持全帧率(比如 30 FPS),而推理则按较低频率运行(比如 10 FPS),用最新一次的推理结果反复渲染多帧。这种“渲染快、推理慢”的策略,保证了画面流畅感和检测框的连续性——就像分拣员的眼睛持续盯着传送带(渲染),大脑每隔一小会才重新评估一次当前的局面(推理),中间用“上一轮判断”来撑着。
回扣思路:帧率优化是分拣台上的节拍器——不是让每个动作都快,而是让“看”和“说”合拍,用最小的算力代价换来视觉上最流畅的体验。
- 摄像头帧率视频源每秒输出的帧数
- 推理频率模型每秒能完成的推理次数
- 向上取整保证跳帧间隔为整数,且推理量不超过能力上限
低端设备适配
不是每个分拣员都站在最新款的传送带前。很多用户手里的设备是几年前的千元机,浏览器的性能捉襟见肘。这时候,你不能让应用直接卡死或闪退,而要像一位经验丰富的分拣班长,给每个摊位配置适合的方案。
第一个适配手段是“模型量化”。把模型权重从 32 位浮点数压缩到 8 位整数,模型体积大约缩小到原来的四分之一,推理速度提升明显,但精度会有轻微下降。量化后的模型就像一个“速记版”的分拣手册——字迹潦草了一些,但关键信息都在,查起信息来反而更快。
假设原始浮点权重的数值范围为 \( [r_{\min}, r_{\max}] \),将其均匀映射到 8 位整数范围 \( [0, 255] \),则量化缩放因子为:
第二个适配手段是“WebGL 回退”。TensorFlow.js 默认用 WebGL 做 GPU 加速,但低端设备的 GPU 可能不支持某些特性,或者驱动有 bug。这时候需要检测环境,回退到 CPU(WASM)后端,虽然慢一些,但稳定。这就像分拣台上准备了手动的弹簧秤,电子秤坏了也能称重,只是慢点。
第三个适配手段是“动态降级”。通过监控推理耗时,动态调整输入分辨率和推理频率:如果检测到推理时间超过阈值,自动降低分辨率或增大跳帧间隔。这是“自适应节拍器”——设备性能好,就调高节奏;设备卡顿,就自动放慢。用户不会看到明显的卡死,只会发现精度稍微下降了一点。
第四个是“内存监控”。浏览器环境下,模型推理的中间张量可能占用大量内存,低端设备尤其容易 OOM(内存溢出)。定期调用 tf.memory() 检查张量数量,及时释放不再使用的中间结果,就像分拣台边有个垃圾桶,果皮果核随手丢,保持台面整洁。
回扣思路:低端设备适配的哲学是“小灶台也能做大锅饭”——通过量化瘦身、环境回退、动态降级和内存管理,让每一份算力都花在刀刃上。
- 权重范围原始浮点权重的最小值和最大值
- 量化级数8 位整数可以表示的离散级数
- 缩放因子每一级对应的浮点数值跨度
方案选型对比
当你要为项目选择“浏览器分拣台”的搭建方案时,市场上主要有两个主流的“装备包”:TensorFlow.js 和 ONNX.js。它们都能在浏览器里跑神经网络,但是侧重点不同。
TensorFlow.js 是 TensorFlow 生态的浏览器延伸。它的最大优势是“亲儿子”待遇:你可以直接加载 Keras 训练的模型,也可以使用它提供的各种预训练模型(如 COCO-SSD、MobileNet)。如果你已经在用 TensorFlow 生态,TensorFlow.js 是最顺手的方案——就像你一直用某个品牌的厨具,新出的便携款自然上手最快。
ONNX.js 则是一个“翻译官”式的方案。ONNX(Open Neural Network Exchange)是一个开放的模型交换格式,PyTorch、TensorFlow、Keras 等框架训练出来的模型都能导出成 ONNX,然后由 ONNX.js 在浏览器里加载运行。它的核心价值是“跨框架互操作”——你可以在 PyTorch 里训练,在浏览器里部署,而不必拘泥于某个特定生态。
两个方案都对模型大小敏感。以 MobileNetV2 为例,原始浮点模型约 14MB,量化为 8 位后约 3.5MB。是选择体积大、精度高的模型,还是体积小、精度略降的模型?要考虑用户首次加载的等待时间。定义加载时间预算 \( T_{\text{budget}} \) 和模型大小 \( M \),在带宽为 \( B \) 的网络下,可加载的最大模型为:
场景适配是选型的最终判据。如果你的应用是“快速原型、要在浏览器里做实验、团队熟悉 JS”——TensorFlow.js 是首选。如果你的应用是“已经在 PyTorch 里训练好了模型、想直接搬到浏览器且不想重新训练”——ONNX.js 更合适。另外还有一个隐形的选型标准:模型是否需要在服务端复用。如果有服务端推理需求,ONNX 的“一次导出、多处运行”优势会更加明显。
回扣思路:选型没有银弹,就像分拣员选工具——有人喜欢弹簧秤(TensorFlow.js),因为熟悉顺手;有人喜欢电子秤(ONNX.js),因为通用性强。关键是搞清楚自己的传送带有多宽、水果种类有多少、分拣精度要求多高,再决定买哪套装备。
- 带宽网络每秒能传输的数据量
- 时间预算可接受的模型加载时间上限
- 最大模型体积两者相乘得到模型大小上限
AI 应用场景
场景一:实时口罩检测的 Web 应用
阶段:浏览器端目标检测全流程(取流 → 推理 → 渲染 → 优化)
解决的问题:传统做法是把摄像头视频流上传到服务器进行口罩识别,存在两大痛点:一是视频上传消耗大量带宽,延迟高;二是涉及用户隐私,很多用户不愿意把摄像头画面传到云端。浏览器端推理把整个流程放在本地,画面不需要离开设备,既保护隐私又降低延迟。
具体案例:某办公楼的访客系统在浏览器中运行 TensorFlow.js + COCO-SSD 模型,访客在平板上打开网页,摄像头自动取流,模型实时检测人脸区域并判断是否佩戴口罩。检测到未佩戴口罩时会显示红色检测框并发出提醒。由于采用了跳帧策略(每 3 帧推理一次)和 WebGL 加速,普通 Android 平板上也能达到 20 FPS 的流畅体验。
场景二:商品瑕疵检测的在线演示系统
阶段:图像分类 + 检测框渲染
解决的问题:电商平台需要向卖家展示 AI 质检的能力,但部署一套完整的服务端推理系统成本高、周期长。浏览器端推理让销售团队可以在任何电脑上打开网页,直接用本地摄像头演示商品瑕疵检测,无需后端支持。
具体案例:某服装电商平台用 ONNX.js 加载 PyTorch 训练的布料瑕疵检测模型,销售人员在客户电脑上打开演示网页,对准一块布料,模型实时标出破洞、污渍、色差等瑕疵位置。模型经过 INT8 量化后体积从 25MB 压缩到 6MB,加载时间从 8 秒降到 2 秒。
Python 演示
# -*- coding: utf-8 -*-"""浏览器端目标检测核心逻辑演示(批处理+NMS)"""import numpy as np class FrameBatcher: """模拟浏览器端帧批处理器""" def __init__(self, batch_size=4, conf_thres=0.6, iou_thres=0.5): self.batch_size = batch_size self.conf_thres = conf_thres self.iou_thres = iou_thres self.frame_queue = [] self.result_cache = [] def add_frame(self, frame_id, boxes, scores): """模拟加入一帧的原始检测结果""" self.frame_queue.append((frame_id, boxes, scores)) if len(self.frame_queue) >= self.batch_size: return self.flush() return None def flush(self): """批量处理队列中的帧""" if not self.frame_queue: return None batch = self.frame_queue[:] self.frame_queue = [] return self._batch_infer(batch) def _batch_infer(self, batch): """对一批帧执行推理和NMS""" all_results = [] for frame_id, boxes, scores in batch: keep = self._nms(boxes, scores) for idx in keep: all_results.append({'frame': frame_id, 'box': boxes[idx], 'score': scores[idx]}) return all_results def _nms(self, boxes, scores): """非极大值抑制:去除重叠候选框""" order = np.argsort(scores)[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) if order.size == 1: break ious = self._compute_iou(boxes[i], boxes[order[1:]]) order = order[1:][ious <= self.iou_thres] return keep def _compute_iou(self, box_a, box_b_list): """计算两个框的IoU重叠度""" x1 = np.maximum(box_a[0], box_b_list[:, 0]) y1 = np.maximum(box_a[1], box_b_list[:, 1]) x2 = np.minimum(box_a[2], box_b_list[:, 2]) y2 = np.minimum(box_a[3], box_b_list[:, 3]) inter_area = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) box_a_area = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) box_b_area = (box_b_list[:, 2] - box_b_list[:, 0]) * (box_b_list[:, 3] - box_b_list[:, 1]) union_area = box_a_area + box_b_area - inter_area return inter_area / np.maximum(union_area, 1e-6) if __name__ == '__main__': batcher = FrameBatcher(batch_size=4, conf_thres=0.6, iou_thres=0.5) # 模拟4帧图像的原始检测输出 frames = [ (1, np.array([[10, 10, 60, 60], [12, 12, 58, 58], [100, 100, 150, 150]]), np.array([0.9, 0.7, 0.8])), (2, np.array([[20, 20, 80, 80], [100, 100, 160, 160]]), np.array([0.85, 0.75])), (3, np.array([[30, 30, 70, 70], [200, 200, 300, 300]]), np.array([0.65, 0.95])), (4, np.array([[15, 15, 65, 65], [50, 50, 120, 120]]), np.array([0.55, 0.88])), ] batcher.result_cache = [] for frame in frames: res = batcher.add_frame(frame[0], frame[1], frame[2]) if res: batcher.result_cache.extend(res) print('批处理推理结果(已过滤低置信度并做NMS):') for r in batcher.result_cache: print(' 帧%d: 框=%s 置信度=%.2f' % (r['frame'], r['box'].tolist(), r['score']))
class FrameBatcher帧批处理器类,对应浏览器端把多帧攒成一批的核心逻辑self.frame_queue = []帧队列实例属性,存放等待批处理的帧add_frame()加入一帧原始检测结果,积累到 batch_size 时触发批处理_batch_infer()对一批帧执行推理后处理,对应 TensorFlow.js 的一次批量推理_nms()非极大值抑制,把重叠的候选框合并成最终结果order = np.argsort(scores)[::-1]按置信度从高到低排序,优先保留最有信心的框_compute_iou()计算两个框的交并比,衡量框的重叠程度ious <= self.iou_thres只保留与当前框重叠较小的框,丢弃几乎重复的框np.maximum(union_area, 1e-6)防止除零,保证数值稳定性
常见误区
误区我以为浏览器端 AI 就是把 Python 的模型推理代码用 JS 重写一遍,逻辑一模一样,只是换个语言。结果发现连“模型文件怎么加载”都搞不定,更别提 GPU 加速了。
正解浏览器端推理的复杂之处不在于“换语言”,而在于运行环境的天壤之别。Python 版模型跑在服务器上,有无限的 CPU/内存/GPU 资源;浏览器端跑在用户设备上,需要处理 WebGL 加速、内存限制、模型加载延迟、主线程阻塞等一系列问题。这就像同样是做饭,在中央厨房跟在小推车上完全不是一回事——前者可以随意摆开阵势,后者得想着怎么在方寸之间周转。
为何常见:因为大多数人接触 AI 都是从 Python 开始的,天然会认为“换个语言就行”,忽略了浏览器这个“边缘设备”本身的资源约束。
误区我觉得取流帧率拉到 60 FPS 肯定体验最好,结果页面卡成幻灯片。后来才发现,取流帧率只是“传送带速度”,真正决定体验的是推理帧率和渲染帧率的配合。
正解摄像头帧率高,意味着每一帧都要绘制到 Canvas、要做预处理,消耗的是 CPU/GPU 资源。如果模型推理只能跑 10 FPS,你喂给它 60 FPS 的帧也只会浪费计算资源,还可能因为过载导致推理延迟反而增大。好的做法是:渲染保持流畅(比如 30 FPS),推理按需降低频率(比如 10 FPS),并用跳帧策略让两者协调。
为何常见:人们直观地认为“更高 = 更好”,忽略了系统是一个整体,瓶颈在推理而不是取流。就像传送带跑得再快,分拣员来不及处理,只会造成堆积。
误区我以为阈值设低一点就能“捕捉”到所有目标,结果画面里全是密密麻麻的方框,连墙上的一团影子都被框出来了,根本没法用。
正解目标检测模型的原始输出包含大量候选框,其中很多是高度重叠或低置信度的噪声。阈值的作用是“宁缺毋滥”——设得低,召回率上去了但精确率惨不忍睹;设得高,精确率上去了但可能漏检。实际应用中需要根据场景调参:安全监控场景宁可误报也怕漏报,阈值可以低一些;商品计数场景则要求精确,阈值需要调高。
为何常见:不熟悉检测模型输出机制的人,会把原始输出当成“最终结果”,不知道候选框只是模型的“草稿”,还需要筛选和去重。
误区我看它们都是“浏览器跑 AI 的库”,以为差不多,随便选一个就行。结果按 TensorFlow.js 的教程做了半天,才发现同事给的模型是 ONNX 格式,根本加载不了。
正解TensorFlow.js 是“TensorFlow 生态的浏览器延伸”,它擅长加载 TensorFlow / Keras 生态的模型;ONNX.js 则是“跨框架的翻译官”,接收的是开放格式 ONNX 模型。两者在模型来源、加载方式、支持的算子层面都有差异。选型时应该看你的模型从哪来、要部署到哪去,而不是“哪个热门用哪个”。
为何常见:初学者看到它们都能“在浏览器里跑模型”,就自然地画上了等号,忽略了模型格式这个关键差异。就像看到两个人都能“切菜”,就以为他们用的是同一把刀。
误区我一开始就强制 WebGL 后端,觉得 GPU 肯定比 CPU 快。结果在用户的老款电脑上一测试,反而比 CPU 模式还慢,甚至直接崩溃。
正解WebGL 加速的实际效果高度依赖设备 GPU 的能力和驱动质量。对于小型模型(如 MobileNet),CPU(WASM)后端可能已经足够快,而且更稳定;WebGL 的优势主要体现在大型模型的并行计算上。更合理的做法是运行时检测设备能力,动态选择后端,而不是写死一种。就像不是所有路都适合开跑车,山路可能还骑摩托更快。
为何常见:人们容易迷信“GPU 一定快”的直觉,忽略了 WebGL 本身的调用开销、纹理上传成本和低端 GPU 的兼容性问题。
面试题
Q1. 在浏览器中,哪个 API 用于获取摄像头视频流?
- A.
getUserMedia← 正确答案 - B.
getVideoStream - C.
openCamera - D.
mediaCapture
答案:A。getUserMedia 是 WebRTC 体系中用于获取摄像头和麦克风流的标准 API。它返回一个 Promise,解析为 MediaStream 对象。其他选项是干扰项——getVideoStream、openCamera、mediaCapture 都不是浏览器标准 API。这就像打开摄像头权限门只有一把官方配的钥匙,其他都是仿造的。
Q2. 把摄像头视频帧送入 TensorFlow.js 模型前,通常需要做什么?
- A.把视频帧绘制到 Canvas 再提取像素,转换成张量← 正确答案
- B.直接传入 HTMLVideoElement 对象
- C.将视频流压缩为 JPEG 后传入
- D.等待摄像头自动转换格式
答案:A。tf.browser.fromPixels 可以从 Canvas 或 Video 元素直接提取像素并转换为张量,但 Video 元素内部也需要经过绘制逻辑。最标准和可控的做法是先把帧绘制到 Canvas,再提取像素数据转成张量。模型吃的是“图片帧数据”,不是视频流本身,就像分拣员需要“定格”的水果照片,而不是一整段录像。
Q3. 批量推理的批大小从 1 增大到 4,下列哪种说法正确?
- A.单帧延迟一定降低,吞吐量一定提升
- B.吞吐量提升,但单帧延迟可能上升← 正确答案
- C.吞吐量和单帧延迟都一定提升
- D.吞吐量和单帧延迟都不变
答案:B。批处理的核心权衡是“用单帧延迟的小幅增加换取吞吐量的大幅提升”。批大小为 1 时每帧单独推理;批大小为 4 时需要等 4 帧凑齐,单帧延迟可能因排队而增加,但总体吞吐量(每秒处理的帧数)通常会提升。就像分拣员攒 4 个水果一起看,单个水果的等待时间略长,但整体效率更高。
Q4. 模型输出 200 个候选框,置信度阈值 0.7,大约保留了 30 个,接下来应该做什么?
- A.直接把这 30 个框全部画出来
- B.用非极大值抑制(NMS)去除重叠框← 正确答案
- C.把置信度阈值再调低到 0.3
- D.把 30 个框平均合并成一个
答案:B。阈值筛选之后,剩下的候选框往往包含大量高度重叠的重复框——同一个物体可能被框了 5 次。NMS 的作用是:按置信度排序,保留最高分的框,去掉与它重叠度(IoU)超过阈值的其他框,然后重复这个过程。直接画出来会看到一堆重叠方框,调低阈值更糟糕,平均合并则没有依据。
Q5. 摄像头输出 30 FPS,模型推理能力为 8 FPS,合适的跳帧间隔是多少?
- A.2 帧
- B.3 帧
- C.4 帧← 正确答案
- D.5 帧
答案:C。跳帧间隔 \( k = \lceil 30 / 8 \rceil = \lceil 3.75 \rceil = 4 \)。意思是每 4 帧取 1 帧送入模型,实际推理频率约为 \( 30/4 = 7.5 \) FPS,略低于模型能力上限,留有余量。取 3 帧则为 10 FPS,可能超出模型能力;取 5 帧则为 6 FPS,精度浪费。这个计算正是“分拣员跟不上传送带时,有节奏地跳过一部分水果”的数学表达。
Q6. 以下哪些是低端设备适配的常见手段?
- A.将模型从 FP32 量化为 INT8← 正确答案
- B.检测到 WebGL 不可用或异常时回退到 CPU← 正确答案
- C.动态降低输入图像分辨率← 正确答案
- D.将摄像头帧率调高到 60 FPS
答案:ABC。模型量化(A)能直接减少模型体积和计算量;WebGL 回退(B)保证老设备上也能稳定运行;动态降分辨率(C)是“性能自适应”的常见策略。而调高摄像头帧率(D)不仅不帮助低端设备,反而会加重 CPU/GPU 负担。D 选项的分拣员“传送带加速”方向完全反了。
Q7. TensorFlow.js 使用 WebGL 后端做 GPU 加速时,下列哪个问题可能导致它比 CPU 后端还慢?
- A.模型参数量超过 100 万
- B.多次 GPU 和 CPU 之间的张量拷贝← 正确答案
- C.浏览器窗口尺寸不是整数
- D.Canvas 的宽度大于高度
答案:B。WebGL 加速需要把数据从 CPU 内存上传到 GPU 纹理,推理结果还要拷回 CPU 层。如果代码中频繁在 GPU 和 CPU 之间交换数据(比如每帧都做 dataSync() 或反复转换张量),传输开销会超过 GPU 并行计算带来的收益,导致整体反而更慢。这就像跑车每次提速都要先花时间从车库挪到赛道,来回折腾反而没小电驴快。
Q8. 关于 TensorFlow.js 和 ONNX.js 的对比,下列说法正确的有?
- A.TensorFlow.js 可以直接加载 Keras 训练的模型← 正确答案
- B.ONNX.js 可以加载 PyTorch 导出的 ONNX 模型← 正确答案
- C.两者都支持 GPU 加速← 正确答案
- D.ONNX.js 必须搭配 TensorFlow.js 才能运行
答案:ABC。TensorFlow.js 是 TensorFlow 生态的浏览器端运行时,能加载 Keras 的 .h5 或 TF SavedModel 转出的模型(A 对)。ONNX.js 加载开放格式 ONNX 模型,PyTorch 可以通过 torch.onnx.export 导出(B 对)。两者都支持 WebGL 作为后端(C 对)。ONNX.js 独立运行,不依赖 TensorFlow.js(D 错)。它们的关系更像是“两种不同菜系”,而不是“主菜和配菜”。
Q9. 在帧率优化中,“渲染与推理解耦”的含义是什么?
- A.渲染和推理同时进行,互不阻塞← 正确答案
- B.只渲染当前推理的帧,其他帧丢弃
- C.渲染由 GPU 完成,推理由 CPU 完成
- D.推理结果只保存一帧就立即覆盖
答案:A。渲染与推理解耦的核心是:推理在异步后台进行,结果写入缓冲区;渲染循环以全帧率(如 30 FPS)持续绘制,每次从缓冲区取“最新可用”的检测框。即使推理只完成了最近一次(如 8 FPS),渲染也不会等待推理结果,所以画面依然流畅。这是“分拣员嘴巴不停、大脑偶尔重新评估”的工程化实现。
Q10. 某浏览器端检测应用的模型大小为 30MB,网络带宽平均 1MB/s,加载预算 5 秒。下列哪些措施可能让它在预算内加载?
- A.量化模型到 INT8,使体积降到 8MB← 正确答案
- B.使用 HTTP 缓存减少二次访问加载← 正确答案
- C.将带宽预估改为理论峰值 30MB/s
- D.模型分片懒加载,先加载首屏所需部分← 正确答案
答案:ABD。量化(A)直接把 30MB 压到 8MB 左右,在 5 秒内绰绰有余。HTTP 缓存(B)对首次加载无效,但能大幅加速二次访问,属于合理优化。分片懒加载(D)把模型按需加载,首屏只需加载部分权重。C 选项是自欺欺人——理论峰值带宽不等于实际可用带宽,就像传送带标称速度不等于实际分拣速度,不能用理想值当设计依据。