守望 Sentinel:只在我不在的时候,才让摄像头替我看着
一个跑在自己电脑上的小哨兵:手机还连着、屏幕还亮着,它就不打扰你;只有你真的离开之后,画面里出现人形,它才会连拍、挑出最清晰的那几张,作为一条 Telegram 相册发给你。
项目已 MIT 开源:fengye1003/watch-sentinel

一、为什么想做这个
我有一个很具体的场景:笔记本放在桌上,人离开房间,但不想为了"看一眼"去装一整套云摄像头。
市面上的方案大致三类:
- 云摄像头 / 家用监控:要买设备、要订阅云存储,画面还要经过别人的服务器。我要的只是"我不在的时候,有人进了这个房间,让我知道",不需要 7×24 录像。
- 本地 NVR(比如 Frigate):能力很强,但要 Docker、要模型、要一块常开的录像盘,对一台偶尔用电池的笔记本来说太重。
- 自己写一个:听起来最麻烦,其实需求很窄 —— 只需要回答两个问题:"我走了吗?" 和 "画面里是不是有人?"
真正让我下决心的是第二个问题的一个细节:手机连着电脑的时候,我人就在旁边。这时候摄像头一直录、一直告警,是最烦的。所以第一优先级不是"识别得多准",而是什么时候不该工作。
于是有了这个项目,我给它起名「守望」(Watch Sentinel)。
二、先设计"不工作"的条件
整个系统的门禁是一条链,只有全部通过,摄像头才真正开始判断:
手机蓝牙仍连接 ──▶ 待机(人在旁边)
屏幕亮度不是最低 且 未锁屏 ──▶ 待机(人还在电脑前)
两个条件都满足"人不在",才进入检测。
为什么用蓝牙,而不是 Wi-Fi / 定位
- 手机连不连电脑,是本地、即时、不依赖网络的事实;
- 蓝牙 API 在 Windows 上可以直接枚举"已配对 / 已连接",单次查询 1–3 ms;
- 不需要手机装任何 App、不需要定位权限。
这里踩了一个很典型的坑:Get-PnpDevice -Class Bluetooth 里 Status=OK 根本不代表设备连着。我的三副耳机、两个手柄、手机,全都显示 OK,但实际一个都没连。真正的连接状态得走 Win32:
BluetoothGetDeviceInfo(radio, info) # info.fConnected / info.fRemembered
fRemembered 是"配过对",fConnected 是"现在连着",两者是独立的事实。用 ctypes 直接调 bthprops.cpl,比每次起 PowerShell 快两个数量级。
设备名还做了归一化:去掉空格、.、·、-、_ 再 casefold,所以 Pixel.hello 和 Pixel·hello 都能命中同一个目标。
为什么加"亮度 + 锁屏"
蓝牙只能回答"手机在不在",回答不了"人是不是还坐在电脑前"。而屏幕亮度和锁屏状态是两个几乎零成本的代理信号:
- 屏幕亮度不是最低 → 大概率还在用;
- 已锁屏 → 大概率离开了。
亮度走 WMI 的 WmiMonitorBrightness(CurrentBrightness,0–100),锁屏走 WTSQuerySessionInformation 的 SessionFlags,再 OR 上 LogonUI.exe 是否存在做双保险。一次查询约 8 ms,2 秒刷新一次。
后来还发现一个有意思的边界:Windows 关显示器通常不改亮度值。所以"屏幕自动熄灭但没锁屏"这条规则按字面会认为人还在。真正稳定触发的是"手动拉到最低"或"Win+L"。要更严谨,可以再加显示器电源状态 —— 但那是另一个故事了。
三、检测:为什么不用 YOLO
需求是"识别类似人体的东西,尤其是人脸",同时明确要求低耗高效。
YOLOv8n 在纯 CPU 上一帧 60–120 ms,还要拖 torch / onnxruntime 几百 MB。对一台要长期挂着、偶尔用电池的笔记本来说,这是负分。
最后选了三层很便宜的组合:
| 判据 | 实现 | 单帧成本 |
|---|---|---|
| 画面变化 | 当前帧 vs N 秒前,灰度差 > 25 的像素占比 | ~1 ms |
| 人脸 | OpenCV YuNet(FaceDetectorYN,模型 232 KB) |
5–15 ms |
| 人形轮廓 | 帧差最大连通块:面积 ≥ 8%、高宽比 ≥ 1.05 | ~1 ms |
640×480 下整条分析 8–20 ms。速度判据则用相邻两拍的变化比例除以时间,单位是 %/s。
这里还有一个隐藏的坑:OpenCV 的 DNN 读模型不吃中文路径。项目如果放在含中文的目录里,FaceDetectorYN.create(path) 会报 Can't read ONNX file,看起来像模型坏了、版本不兼容,其实只是路径编码。解法是把模型复制到纯 ASCII 的 %LOCALAPPDATA%\sentinel\models\ 再加载。
另外,OpenCV 5.0 已经移除了 CascadeClassifier 和 HOGDescriptor,所以老的 Haar 方案也不通 —— YuNet 反而是最稳的选择。
四、报警照片为什么总是糊的,以及连拍
第一版发出去的照片经常是这样:人在动,快门刚好抓在 Motion Blur 最严重的一帧,糊成一团。
解决思路很朴素:多拍几张,挑最清晰的。
- 人形触发时,按固定间隔(默认 150 ms)连拍 N 张(默认 6 张);
- 每张算一次 Laplacian 方差:
cv2.Laplacian(gray, CV_64F).var(); - 取分数最高的 m 张(默认 3 张),逐张重新做人脸检测并标注;
- 通过 Telegram 的
sendMediaGroup作为一条相册发出,第一张带 caption,其余不带。
为什么用 Laplacian 方差而不是别的:它是纯 CPU、单张 < 3 ms,对"运动模糊/失焦"这种最影响观感的退化最敏感。实测一张锐利棋盘 39931、高斯模糊后 1.8,差四个数量级,判别力足够。
Telegram 的细节:sendMediaGroup 最少 2 张、最多 10 张;caption 只能挂在第一张;只有 1 张时必须降级成 sendPhoto,否则 400。
连拍会短暂阻塞检测循环,所以代码里给总时长封了 8 秒上限。默认 0.9 秒,可以接受。
五、面板与登录
面板是 Material Design 3 风格,原生 HTML/CSS/JS,无 CDN、离线可用。所有阈值、连拍参数、摄像头参数、Telegram 配置都能改,改动即时保存。
考虑到它可能会被放到 tailnet / 局域网里,登录保护直接复用了我平时用的 TOTP:RFC 6238,SHA-1、6 位、30 秒、±1 步。密钥可以指向 DSH Web 面板的 state.json,手机里已有的验证器条目不用重扫。
会话是一个 HMAC 签名的 sentinel_auth cookie(HttpOnly、SameSite=Lax、默认 30 天)。同一 IP 60 秒内错 5 次返回 429。
六、把 AI Agent 也当成用户
这个项目从第一天就是"给自己用的工具",所以部署文档写成了给 agent 照着执行的 runbook,而不是给人看的安装说明:
仓库里的 docs/DEPLOY-FOR-AGENTS.md 包含:
- 环境自检(摄像头、蓝牙、Python 版本);
- 装依赖、下载模型;
- 配 Telegram(token 与 chat id);
- 启动与健康检查;
- 配置目标手机;
- 三步验收(演练 / 模拟告警 / 门禁状态);
- 6 个会伪装成"程序坏了"的坑;
- 回滚。
最短路径只有四行:
git clone https://github.com/fengye1003/watch-sentinel.git
cd watch-sentinel
python -m pip install -r requirements.txt
python scripts\fetch_model.py
python app.py
验收时 agent 应该确认:/api/health 返回 200、bt.found=true、/api/drill 能识别到人脸、/api/telegram/simulate 后用户在 TG 收到一条多图相册。
七、几个值得记下来的工程坑
- PnP 的 Status 不代表连接,蓝牙必须用
fConnected。 - OpenCV 模型路径不能有中文,否则报错会误导你怀疑模型。
- dxva2 的亮度在一些 HDR 屏上直接失败(
ERROR_GEN_FAILURE),WMI 才稳。 - WMI 的 COM 对象有线程亲和,跨线程复用会报"已为另一线程整理的接口",要按线程隔离。
- 摄像头后端 MSMF 偶发"能打开但读不到帧",默认 DSHOW,失败自动回退。
- 任务计划程序 +
powershell -WindowStyle Hidden会闪黑窗:控制台在参数生效前就建好了。正确做法是用wscript.exe跑一个 VBS 壳,Run(cmd, 0, False),窗口完全不出现。
最后一条是给"开机自启"的,也是这个项目里唯一一个"用户一眼就能看见"的 bug。
八、可复现流程与验收清单
部署:
git clone https://github.com/fengye1003/watch-sentinel.git
cd watch-sentinel
python -m pip install -r requirements.txt
python scripts\fetch_model.py
.\start.ps1
验收:
- [ ]
GET /api/health→{"ok": true} - [ ] 面板能打开,蓝牙列表里能找到手机
- [ ]
/api/drill10 秒内识别到人脸 - [ ]
/api/telegram/simulate后 TG 收到一条多图相册 - [ ] 手机连上电脑时,门禁显示「已连接,哨兵待机」
- [ ] 锁屏/熄屏后,门禁显示「已锁屏,哨兵生效」
- [ ] 开机自启的两个计划任务都是
Ready
回滚:
.\uninstall-autostart.ps1
.\stop.ps1
九、开源信息
- 仓库:fengye1003/watch-sentinel
- License:MIT
- 作者:星澄(设计与实现)、幻愿(需求与验收)
它不是"更便宜的监控",而是一个很小的立场:只有当我不在的时候,它才有权看。 如果这个立场对你有用,欢迎拿去改成你自己的门禁条件 —— 蓝牙、亮度、锁屏,或者任何更能代表"你不在"的信号。
评论(0)
暂无评论