
狗一听见门响就蹿上沙发回来一看坐垫上留个坑——这事儿我盯着摄像头画面折腾了大概两周最后落地的东西比想象中简单一个纯前端页面打开摄像头浏览器实时目标检测在本地跑识别到狗压进沙发那块区域就响一声。全程不上传任何画面老笔记本上也跑得动。这套方案适合两类人一类是家里有狗、想低成本做个电子狗哨的养宠人另一类是刚接触前端视觉、想找个有真实约束条件的项目练手的人——它涉及模型加载、视频流接入、坐标换算、状态机去抖、长时间运行稳定性麻雀虽小五脏俱全。下面我把整个拆解过程、踩过的坑和能直接抄的代码都摊开讲。1. 需求拆解这件事到底难在哪1.1 把防狗上沙发翻译成技术语言很多人一听这需求第一反应是不就是个目标检测吗找个模型框出来不完了。真做起来你会发现检测到狗和狗上了沙发是两个完全不同难度的问题。前者是通用的单帧分类定位后者是一个带空间关系、时间连续性、并且需要控制误报的业务判定。我把需求拆成四层每一层都有独立的失败模式必须分开处理不然出了问题根本不知道该改哪。第一层是画面获取得在浏览器里拿到稳定的摄像头流解决权限、分辨率协商、前后摄像头切换这些问题。第二层是单帧推理把视频帧喂给模型拿到哪些位置有什么物体、有多确信的结果。第三层是空间判定判断检测框落没落在你划的沙发范围内——这是纯几何计算跟模型没关系但错得最多的往往是它。第四层是时间决策单帧结果抖动极大可能这帧 0.62 下帧 0.41需要一个状态机把零散信号攒成事件再触发提示音。注意不要指望模型直接输出狗在沙发上这种结论。通用检测模型只知道这团像素像狗它不懂什么叫沙发也不懂你家客厅的布局。沙发这个概念必须由你在业务层用人划的区域来定义。这四层里第三层和第四层是我花时间最多的地方。第一版我图省事只要检测框中心点落进矩形区域就报警结果狗从沙发前走过、只露出一半身子的时候疯狂误报后来改成判断框的底部中点近似狗脚着地的位置误报立刻掉了大半。这个细节后面会详细讲它背后其实是一个朴素的物理直觉判定站上去应该看接触面而不是看整体重心。1.2 正确率、延迟、隐私三者怎么取舍任何实时检测系统都在三个指标之间拉扯准确率、响应延迟、资源占用。家用场景的诉求和工业质检完全不同判断标准只有一条——误报和漏报哪个更不能忍。狗上沙发这件事漏报的代价是坐垫上多个坑误报的代价是半夜突然响一声把人吓醒。所以我的默认策略是宁可慢半秒不要乱响宁可检测频率低一点、确认帧数多一点也要把误报压到接近零。响应延迟方面我做了一个粗糙的体验测试从狗屁股挨上坐垫到提示音响起2 秒以内人不会有这玩意儿卡了的感知3 秒以上就会觉得迟钝。这个预算分配下来就是推理间隔 200 毫秒左右连续 3 帧确认理论最快反应时间约 600 毫秒留足了余量。隐私是这套方案最硬的优势。所有像素在整个生命周期里都没离开过浏览器进程——视频流只存在于video元素和 Canvas 里推理结果只有几个坐标和分数。如果你换成把视频推到远端服务去分析那客厅画面等于天天往外传我个人是完全不能接受的。这也是我坚持把它做成纯前端方案的核心原因而不是为了炫技。1.3 整体链路长什么样先把全貌画出来后面每块单独展开。数据从摄像头进来经节流控制后送进模型模型吐出一堆检测框判定模块筛出狗这一类和预先标定好的沙发区域做几何比对命中后交给状态机累积达到稳定条件才触发提示音同时往日志里写一条记录。整个链路里唯一重的环节是模型推理其余全是很轻的几何计算和状态维护。这也决定了优化方向应该优先砸在推理上降低输入尺寸、拉开推理间隔、选轻量模型。我见过有同学花大量时间优化判定逻辑的性能其实那部分开销小到可以忽略方向一开始就偏了。还有一条容易被忽略的约束CPU 和显卡占用不能拉满。这页面是要 7×24 挂着跑的如果推理把笔记本风扇吹得呼呼响用户三天就把它关了。所以后面我会专门讲主动降速的取舍。2. 技术选型为什么把模型塞进浏览器2.1 浏览器端推理的三种可行路线摆在我面前的选择大概有三个方向我实际都试过或评估过。第一种是纯前端跑轻量检测模型用 WebGL 加速模型文件几 MB首屏加载几秒之后完全离线。第二种是前端只负责采集把画面编码后送到后端或边缘设备推理准确率上限更高可以换更重的模型但要维护服务、要处理带宽、要面对画面外传的隐私问题。第三种是趁设备厂商的本地视觉能力但这通常是封闭能力可定制性差也不好跨设备复用。我最后选第一种理由是匹配度家用场景的类别极其有限只需要区分狗和不是狗空间范围固定就一个沙发对绝对精度的要求远低于对跑得动、不出事、不联网的要求。在这种约束下用一个大模型把 80 类都识别到很高的 mAP属于过度设计。方案首屏/接入成本准确率上限隐私长期运维适合场景浏览器本地推理低加载即用中画面不出设备无家用、单场景、类别少推流到服务端推理高要部署高画面外传有多点位、需要统一管理厂商封闭能力中不可控一般较好依赖厂商快速验证表格里准确率上限那一栏是相对的。浏览器本地方案的准确率天花板确实低一些但在我这个场景里真正拉低准确率的不是模型规模而是光照、遮挡和画面质量——这三样换成多大的模型都救不回来。与其纠结模型不如先把摄像头位置摆好、把补光做上收益大得多。2.2 轻量检测模型的选型逻辑浏览器里能用的现成检测模型不算多主流是两类一类是通用目标检测比如基于 MobileNet 骨干的 SSD 系列开箱即用能识别常见类别其中就包含狗另一类是姿态/关键点模型输出骨架而非框判断是否站上去更精细但类别覆盖窄需要额外训练。我选了前者因为它的类别表里天然带狗这一项不需要自己准备数据集就能跑通全流程。选型时我主要看三个参数输入分辨率、骨干网络、权重体积。输入分辨率直接决定推理耗时300×300 左右的输入在移动端芯片上单帧大约几十毫秒够用骨干网络越深越准也越慢家用场景用最轻的那档即可因为目标一只成年犬在画面里占比通常不小不需要强特征提取能力。权重体积决定首屏体验几 MB 的量级在普通宽带上 2 到 3 秒能下完可以接受配合浏览器缓存第二次打开几乎瞬开。提示模型加载一定要做失败兜底。我遇到过网络策略较严的环境里模型文件下不下来的情况页面直接白屏。后来加了超时和错误提示至少能告诉用户模型没加载成功而不是让人对着空页面猜。另外一个我认为必须提前想清楚的问题是要不要自己训练模型。如果你的狗是深色长毛、经常缩成一团、或者沙发本身是毛绒材质导致边界模糊通用模型的召回率会明显下降。这时候的解法不是换更大的模型而是用迁移学习在通用模型基础上补几百张自家场景的图微调。这个工作量不小我建议先用通用模型跑一周统计漏报都发生在什么条件下再决定要不要投入。2.3 一句话说清架构这套东西的架构可以压缩成一句话一个页面、两条循环、一个区域。页面就是单个 HTML包含视频元素、用于可视化的 Canvas、和几个控制按钮两条循环分别是视频播放浏览器内部驱动和检测循环我用时间戳节流主动控制一个区域是用户手动标定的沙发多边形。没有服务端没有数据库没有消息队列配置全部存在本地。这种极简形态不是偷懒而是刻意为之。家用工具的失败模式通常是复杂度导致的不可维护加一个后端就多一个可能挂掉的组件。我宁可把能力边界缩到最小也要保证它能长期无人值守地跑。3. 核心原理从检测框到狗在沙发上3.1 模型输出的到底是什么先说清楚模型吐出来的东西长什么样不然后面所有判定都是空中楼阁。一次检测调用返回的是一个数组每个元素包含三部分边界框、类别名、置信度。边界框的格式是一个四元组代表左上角横纵坐标加上宽高——注意是宽高不是右下角坐标这个格式差异是新手最容易写错的地方写错了框会整体偏移一个身位。置信度是一个 0 到 1 之间的数表示模型有多确信这个位置有这个东西。它不是概率意义上的绝对准确率只是一个相对排序的分数。实践中你会发现同一只狗在不同帧之间的分数波动很大起身时 0.7趴着不动时掉到 0.4这不是模型坏了而是目标姿态和遮挡变化导致的正常现象。// 一次调用的返回结构示意 [ { bbox: [312, 118, 186, 240], class: dog, score: 0.71 }, { bbox: [40, 300, 90, 120], class: couch, score: 0.55 } ]有意思的是部分通用检测模型的类别表里确实包含沙发类。我一开始很兴奋想着能不能直接用检测框和沙发框求交集省掉手动标定。实测下来放弃了这个念头沙发这类大件家具的检测稳定性远不如狗稍微被抱枕遮一下就检不出来而且只要有一帧丢了沙发框整个判定就断了。手动划一次区域稳定性高出一个数量级。3.2 沙发布防区怎么划才靠谱区域标定这件事看起来只是点几下鼠标实际上决定了后面所有判定的准确率上限。我试过三种划法最后留下的是多边形。矩形最简单但沙发通常有扶手和靠背矩形会把沙发前方的地板也圈进去狗路过就误报。多边形可以贴着扶手内沿描一圈把坐垫可站立面精确框出来。标定的操作流程我做成页面先截取一帧画面当底图用户在上面依次点击若干顶点最后闭合点位存进本地存储。这里有个细节必须处理——标定用的底图和实际推理的坐标系必须一致。如果视频元素被 CSS 拉伸过而底图是按原始分辨率截的那画出来的区域和真实画面对不上判定会全错。我的做法是统一用视频的原始内部分辨率作为坐标系显示时再按比例缩放。区域数据我建议存成归一化坐标0 到 1 之间的小数而不是像素值。原因很实在换了摄像头、改了分辨率像素坐标就废了归一化坐标乘以当前宽高就能自动适配同一个区域配置可以跨设备复用。这个习惯在图像类项目里通用早建立早省事。3.3 从单帧到事件去抖与状态机单帧判定必须加状态机这是整个项目里我觉得最有工程味的一段。原始信号是这样的帧 1 命中、帧 2 未命中、帧 3 命中、帧 4 命中……如果每帧命中就响你会被提示音淹没。所以需要一个类似硬件的去抖逻辑连续命中 N 帧才认为状态切换为上沙发连续未命中 M 帧才认为离开。我把 N 设成 3M 设成 8。为什么离开的阈值要比进入的大因为狗趴着不动的时候模型偶尔会掉一两帧如果 M 也设成 3状态会疯狂在在和不在之间翻转触发一堆无意义的日志。离开判定松一点宁可晚点认为它走了也不要中途抖动。再叠加一层冷却时间一次触发之后30 秒内不再重复触发。这一层防的是狗赖在沙发上不走的情况——状态一直保持在命中态但从业务上讲你不需要它每 3 帧就提醒你一次。冷却时间我做成可配置实际用下来 30 秒到 1 分钟比较舒服。// 状态机核心逻辑去掉无关分支 const state { hits: 0, misses: 0, inside: false, lastAlert: 0 }; const ENTER_N 3, EXIT_M 8, COOLDOWN_MS 30000; function tick(hit, now) { if (hit) { state.hits; state.misses 0; if (!state.inside state.hits ENTER_N) { state.inside true; if (now - state.lastAlert COOLDOWN_MS) { state.lastAlert now; fireAlert(); } } } else { state.misses; state.hits 0; if (state.inside state.misses EXIT_M) { state.inside false; } } }这段代码短但每一行都有存在的理由。state.hits和state.misses互斥清零保证计数语义只有一种解释inside是唯一的业务状态报警只发生在它的上升沿lastAlert做冷却。很多同类项目写成每帧独立判断 时间窗口过滤逻辑散在各处加一个新条件就要改三处很难维护。3.4 判定站上去用哪个参考点这一节单独拎出来讲因为我在这上面栽过跟头。最初我用检测框中心点判断是否在区域内问题很明显狗站在沙发前的地板上头探过来闻坐垫框的中心点可能已经越过了沙发边界于是误报。反过来狗趴在沙发边缘只有前半身压在坐垫上中心点还在外面于是漏报。第二种尝试是框与区域的重叠面积占比即检测框有多大比例落在区域内。这个比中心点好但阈值很难定狗整个趴在沙发上时占比接近 1只探个头时占比 0.05中间一大段是模糊地带。最终我用的是框底部边线中点也就是取检测框左右两边的中间、下边线的那个点。理由很朴素这个点近似对应目标与地面或家具面的接触位置。狗从沙发前走过接触点在沙发前方的地板上判定不命中狗跳上去接触点进入坐垫区域命中。实测这个方案的误报率比中心点低了一个量级代码还更简单。// 接触点判定 面积过滤 function isOnCouch(det, roiPoly, minAreaRatio) { const [x, y, w, h] det.bbox; // 过滤过小的框压掉远处背景里的误检 if ((w * h) / (frameW * frameH) minAreaRatio) return false; const footPoint { x: x w / 2, y: y h }; return pointInPolygon(footPoint, roiPoly); }minAreaRatio这个过滤项也很有用。画面里偶尔会出现远处电视里一只狗、墙上挂画上的狗、或者一个小摆件被误检它们的共同特征是框很小。设一个面积下限我用的 1.5% 画面面积一股脑全过滤掉副作用极小因为要真的上沙发狗在画面里不可能太小。4. 动手实现一个能跑的最小闭环4.1 页面骨架与依赖引入页面结构我保持到最简一个视频元素不显示只作为数据源、一个叠加在上面的 Canvas用来画框和区域、一排按钮启动、标定、静音。刻意不在视频元素上做任何 CSS 缩放之外的装饰就是为了减少坐标换算的心智负担。依赖通过模块打包引入不用全局脚本标签避免版本混乱。核心就两个包张量运算框架和目标检测模型。import * as tf from tensorflow/tfjs; import * as cocoSsd from tensorflow-models/coco-ssd; // 优先使用 WebGL 后端失败则回落到 CPU await tf.ready(); console.log(backend:, tf.getBackend()); const model await cocoSsd.load({ base: lite_mobilenet_v2 });lite_mobilenet_v2这一档是我反复对比后定下来的。换成更重的档位准确率提升在肉眼可辨的范围内很小但单帧耗时几乎翻倍在老旧设备上直接掉到个位数帧率。家用场景的目标足够大轻量档完全够。关于后端tf.ready()之后打印一下当前后端是个好习惯。如果拿到的是 CPU 后端说明 WebGL 没起来页面会慢到不可用这时候应该在界面上明确提示而不是让用户以为这东西就是这么卡。我遇到过某些远程桌面环境里 WebGL 被禁用就是靠这个日志定位的。4.2 摄像头接入的坑都在约束参数里取流本身几行代码但参数写不好会带来一堆后续麻烦。我用的约束是分辨率给一个理想值 640×480帧率不强制面向环境摄像头。640×480 是个甜点再高对检测精度帮助有限模型内部还会缩放但会明显增加解码和绘制的开销。const stream await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 15, max: 20 }, facingMode: environment }, audio: false }); video.srcObject stream; await video.play();这里有几个坑值得单独说。第一ideal不是强约束设备可能给你一个完全不同的分辨率所以后续所有坐标计算都必须读video.videoWidth和videoHeight不能写死 640 和 480。我第一版就是写死的换了个摄像头之后框整体偏移查了半天。第二play()要等某些浏览器上srcObject赋值后立刻调用检测会拿到宽度为 0 的帧模型直接报错。加一个等待元数据就绪的逻辑更稳。第三如果用了前置摄像头并且做了镜像显示画面是左右翻转的但模型推理用的是原始帧这时候你画的框和看到的画面是对不上的需要在绘制阶段单独处理翻转别在判定阶段混着改。注意检测循环启动前一定要检查video.readyState 2否则会频繁触发帧数据无效的异常。这不是理论风险我在低端机上稳定复现过。4.3 推理循环的节流写法循环这块我最开始用requestAnimationFrame直接驱动结果在 60Hz 屏幕上每秒调用 60 次推理机器瞬间烫手帧率还被拖垮。正确做法是用时间戳节流动画帧照常跑用于流畅绘制但推理按固定间隔触发。let lastInfer 0; const INFER_INTERVAL 200; // 毫秒 async function loop(now) { requestAnimationFrame(loop); if (now - lastInfer INFER_INTERVAL) return; lastInfer now; if (video.readyState 2) return; const preds await model.detect(video, 20, 0.35); const dogs preds.filter(p p.class dog p.score 0.45); const hit dogs.some(d isOnCouch(d, roiPoly, 0.015)); tick(hit, performance.now()); draw(preds, dogs, hit); } requestAnimationFrame(loop);几个参数值得解释。model.detect的第二个参数是最大检测数量我设 20因为家里画面里不可能有更多目标设小一点能省一点后处理时间。第三个参数是内部置信度阈值设 0.35 是为了让模型多吐一些低分框给我自己筛因为模型的阈值和我的业务阈值不是一回事——我要在狗这个类别上单独用一个更高的阈值0.45。这个双层阈值的设计很实用模型的内部阈值宁可松一点业务阈值严格把关比反过来灵活。INFER_INTERVAL设 200 毫秒也就是每秒 5 次推理。这个频率听起来很低但配合 3 帧确认最慢 600 毫秒就能触发人对这个延迟的感知是可以接受的。我把这个值做成可调实测在性能好的机器上降到 120 毫秒体验更好在老旧设备上提到 300 毫秒也不影响可用性。还有一个隐藏问题await model.detect是异步的如果某次推理特别慢比如系统突然卡了一下可能出现两次推理交叠。虽然实际影响不大但为了保险我加了一个busy标志位正在推理时直接跳过这一轮。4.4 提示音触发与自动播放限制报警用什么方式我试过三种页面弹提示、往手机推送、播声音。前两个都需要额外的服务端或通知权限只有声音是纯前端的所以选了声音。但浏览器有自动播放策略——页面没被用户交互过之前音频播放会被拦截。这意味着你不能指望页面一打开就能响。解法是在用户点击启动按钮的那一刻解锁音频上下文。这个动作本身就是一次用户手势正好满足策略要求。我用的是音频接口实时合成一段短促的提示音不需要额外的音频文件省掉一次网络请求也不会出现音频加载失败导致静默的情况。let audioCtx; function unlockAudio() { if (!audioCtx) audioCtx new (window.AudioContext || window.webkitAudioContext)(); if (audioCtx.state suspended) audioCtx.resume(); } function fireAlert() { if (muted || !audioCtx) return; const osc audioCtx.createOscillator(); const gain audioCtx.createGain(); osc.type sine; osc.frequency.setValueAtTime(880, audioCtx.currentTime); gain.gain.setValueAtTime(0.15, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime 0.35); osc.connect(gain).connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime 0.36); }音量我控制在 0.15 左右实测这个音量在客厅里能听见但不会吓人。频率用 880 Hz 的短音比低频的嗡嗡声更容易被注意到。时长 0.35 秒太短容易被环境噪声盖住太长会烦。4.5 可视化调试层的价值我强烈建议做一个可视化层把模型的原始检测框、筛选后的狗框、以及沙发区域全部画出来颜色区分。理由不是好看而是没有它你根本没法排查。误报的时候你要一眼看出是模型把什么当成了狗、那个框在哪、接触点落在区域的哪一侧。这些信息全靠日志输出根本看不明白。绘制的坐标换算要小心。Canvas 的显示尺寸受 CSS 控制内部尺寸应该设成视频的原始分辨率然后用 CSS 缩放这样drawImage和绘制坐标都在同一个坐标系里不需要做任何缩放计算。如果反过来按显示尺寸设内部尺寸那每一帧都要做一次比例换算容易出错也浪费性能。还有一个实用技巧给命中的框和未命中的框用不同颜色并且在画布角落实时显示当前状态机的计数命中帧数、是否处于 inside 状态。调试期间我几乎全靠这两个数字判断阈值调得合不合适。上线之后可以一键关掉不影响正常使用。5. 调优与长期运行让它在客厅里活过一个星期5.1 性能预算怎么分配先说结论在这个项目里模型推理占了 90% 以上的计算开销其余所有逻辑加起来不到 10%。所以优化必须瞄准推理而不是去抠几何计算的性能。这个判断很重要因为它决定了你该往哪个方向使劲。我实际测过的一组数据老笔记本集显640×480 输入轻量档模型单帧推理在 40 到 70 毫秒之间波动绘制一帧在 2 毫秒以内区域判定和状态机加起来不到 0.1 毫秒。按每秒 5 次推理算CPU 占用大约在 20% 到 25% 之间风扇会有轻微声音但不会持续高转。优化手段效果代价我的取舍降低推理频率到 5fps开销降约 60%响应慢约 200ms采用使用最轻骨干开销降约 40%小目标召回略降采用缩小输入分辨率开销降约 30%远处目标易漏视情况关闭可视化绘制开销降约 2%没法调试仅上线后关后台标签页暂停开销降 100%无必须采用最后一条特别重要。浏览器对后台标签页有节流策略但不同浏览器的策略不一样有的会把定时器压到每分钟一次有的干脆不停。我加了一个基于页面可见性事件的显式暂停页面切到后台就停掉检测循环切回来再恢复。这一条既省电也避免了页面在后台偷偷跑了通宵这种事。5.2 长期运行的两个真实故障第一个故障是日志数组无限增长。我最早把每次检测结果都推进一个数组想着方便事后分析跑了大概六个小时页面就明显变卡内存占用一路往上走。修法很简单给日志数组设一个上限我用的 200 条超出就从头覆盖。这类环形缓冲的思路在任何需要长时间记录的场景里都适用成本极低收益极大。第二个故障是视频流意外中断。可能是摄像头被其他程序占用可能是 USB 松动也可能是系统休眠唤醒。表现是video.readyState一直是 0 或者画面卡住不动但检测循环还在空转。我加了一个简易的健康检查连续若干次推理失败或者连续若干秒画面无变化就重建整个流并提示用户。这个逻辑有点土但确实管用尤其适合无人值守的场景。// 简易健康检查每 30 秒跑一次 setInterval(() { if (video.readyState 2 || video.currentTime lastVideoTime) { stallCount; if (stallCount 2) restartStream(); } else { stallCount 0; } lastVideoTime video.currentTime; }, 30000);5.3 光照、遮挡与摄像头位置这三件事对准确率的影响远比换模型大。我在同一个模型下做过对比白天靠窗自然光检测稳定在 0.7 以上傍晚只开一盏暖色灯分数掉到 0.45 上下还经常漏检。这不是模型的问题是画面信噪比下降导致的必然结果。具体建议摄像头架在能看到坐垫正面的位置略微俯角不要平视正对沙发——平视时狗趴下后被靠背挡住只剩一撮毛露在外面什么模型都检不出来。避开窗户逆光否则狗会变成剪影。夜间要么补一盏小夜灯要么接受夜间基本不可用这个现实普通摄像头在无光环境下拍出来的画面任何模型都无能为力。遮挡方面还有一个容易忽略的细节天气热的时候狗喜欢趴在沙发靠背上只露一个头。这种姿态在画面里占比很小经常被面积过滤误杀。我的处理是把面积下限从 1.5% 调到 1%代价是误检略微上升但靠去抖逻辑兜住了。这种参数的调法没有标准答案只能根据自己的实际画面反复试。5.4 提示策略的人性化设计技术上响一声就完事了但真用起来你会发现提示策略本身也需要设计。我家的情况是狗白天大部分时间都在沙发上趴着如果一直响我三天就想把这东西砸了。所以我加了两个机制冷却时间和分时段静音。冷却时间前面说过解决的是持续命中的重复触发。分时段静音则是按小时设定不响的区间比如夜里十一点到早上七点直接静音只记录不播报。这两个机制加起来实测一周的误报和打扰次数能压到个位数。另外我建议把触发记录展示出来哪怕只是一个列表显示几点几分触发了一次。这玩意儿除了排查问题还有个意外用途你能大概知道狗一天什么时候爱上去。纯粹好玩。6. 常见问题与排查实录下面这些是我和自己、和几个朋友实际遇到过的问题按现象整理成速查表遇到问题先对照一下能省不少时间。现象大概率原因排查动作页面打开一直空白后端没起来或模型下载失败打印当前后端和加载错误检查网络画面正常但从不报警区域没标定好或坐标系错位打开可视化层看框和区域是否对得上框位置整体偏移用了写死分辨率或 CSS 缩放未换算统一以 videoWidth/Height 为基准画面左右和框相反前置摄像头镜像了显示但没镜像坐标在绘制阶段单独处理翻转频繁误报置信度阈值过低或用了中心点判定阈值提到 0.45改用底部中点狗趴着不动就不报了连续帧确认把静态目标滤掉了降低进入阈值或提高推理频率跑几小时后变卡日志或数组无限增长加环形缓冲上限夜里完全不工作无光环境画面不可用补光或接受夜间不启用声音不响音频上下文未解锁在用户点击回调里 resume页面切后台后不恢复可见性事件未正确恢复循环检查事件绑定与状态复位讲几个表里没展开但比较典型的。关于狗趴着不动就不报了这个问题的成因挺有意思狗刚跳上去的时候动作大、姿态舒展模型容易检出趴稳之后缩成一团加上长时间静止模型反而可能连续几帧都没检出。如果你的进入阈值设得太高比如要连续 5 帧很可能永远攒不够。我的做法是把进入阈值压到 3 帧并且推理频率不要太低。关于误报的具体来源我统计过一周的误报大致分布是电视或手机屏幕里的狗占一半以上、墙上的挂画和玩偶、以及狗在沙发前转身时框的形变。前两类用面积过滤能挡掉一部分挡不掉的只能靠位置过滤——屏幕和挂画的位置通常是固定的把它们从区域里排除就行。第三类靠底部中点判定基本解决了。提示如果误报集中在某个固定位置最快的解法不是调模型而是在区域标定时把那个位置直接抠掉。多边形标定的好处就在这里可以画得很细。还有一个我认为值得单独提醒的坑不要用检测框的重叠率IoU去和沙发区域做匹配。我一开始想当然地用了这个方法结果发现沙发是大目标、狗是小目标两者框的尺度差太远IoU 天然就上不去阈值怎么调都不对。正确的做法是用检测框落在区域内的比例或者干脆用点判定语义上才对得上。最后分享一个我在实际使用中觉得最有价值的小改动给判定结果加一个置信度平滑。不是直接用单帧的分数而是维护一个最近 5 帧的分数滑动平均用平均值和阈值比较。这个改动只有几行代码但能明显削掉那些闪一下就没了的抖动让状态切换更稳。尤其是画面有轻微反光或者狗在动的时候效果比单纯调阈值好得多。这个思路可以推广到所有需要把连续检测结果转成离散状态的场景里比如人多的地方统计进出、停车位占用检测本质都是同一个问题。