ESP32-CAM 监控:小试牛刀写一个自助闪光
起因
之前用 ESP32-S3 做了个监控摄像头,效果还行。后来翻抽屉发现还有一块 AI-Thinker 的 ESP32-CAM 开发板——就是那种十几块钱、自带 OV2640 摄像头和 TF 卡槽的经典板子。手上有就不用浪费,再做一个吧:ai-thinker-esp32-cam。
这次还是 ESP-IDF 从零写固件,功能跟上个项目差不多,但针对 AI-Thinker 这块板子做了不少适配。最终实现了这些东西:
- 浏览器打开 IP 看 MJPEG 实时画面
- 动态检测,检测到动静自动拍照
- 暗光环境自动开闪光灯拍照
- 拍照存 TF 卡,支持 WebDAV 上传 NAS
- Web 管理界面,手机上就能配参数
- Prometheus
/metrics端点接监控系统 - WiFi AP/STA 双模式,首次上电手机配网
功能不复杂,但闪光灯那块逻辑花了我好几天。原因很简单:我懒得接光敏传感器。省了几根线,多写了几百行代码。
闪光灯逻辑:不接传感器的代价
需求就一句话,实现折腾好几天
天黑了自动开闪光灯拍照——需求说起来就这么多。接个光敏电阻到 ADC 口,读个电压值就完事了,半小时搞定的活。但 AI-Thinker ESP32-CAM 的 GPIO 本来就紧张,闪光灯占了 GPIO4,TF 卡占了 GPIO2/14/15,摄像头一堆引脚,剩下的没几个能用了。多接一个传感器还要面包板、杜邦线、分压电阻……光是想想就不想动。
那就用摄像头本身来感知亮度呗。摄像头不就是个感光元件吗,画面亮就是亮,暗就是暗。问题是固件运行时摄像头一直在以 JPEG 模式输出画面(MJPEG 流和动态检测都要用),得在不影响正常工作的前提下判断环境亮度。
读 OV2640 曝光寄存器——不行
第一反应是读 OV2640 的 AEC(Auto Exposure Control)寄存器。理论上曝光值能反映环境亮度:越暗曝光越高,直接读寄存器零延迟零开销,完美。
实测下来这个寄存器在连续 JPEG 输出模式下根本不靠谱——aec_value 一直稳定在最大值 671 不动弹,不管实际光线怎么变。查了 OV2640 的 datasheet,也没找到明确的说法。可能是连续输出模式下 AEC 反馈回路有 bug,也可能就是这颗 sensor 的"特色"了。总之这条路走不通。
灰度像素采样:把摄像头当光敏传感器
读寄存器不行,那就直接读像素。做法不复杂:
- 每 30 秒做一次亮度探测
- 把摄像头从 JPEG 模式临时切换到灰度模式(GRAYSCALE + QQVGA 160×120)
- 抓一帧,遍历所有像素算平均亮度
- 切回 JPEG 模式继续干活
| |
QQVGA 160×120 总共 19200 个像素,遍历一遍几百微秒,对性能没什么影响。灰度模式每个像素就是一个字节(0~255),直接求平均就行,简单粗暴。
不过有个前提条件:如果有人在浏览器看 MJPEG 画面,就不能做灰度探测——切模式会导致 MJPEG 流断掉。这种时候只能靠下面的备用方案。
JPEG 大小反推亮度:零开销的野路子
有人在线看画面的时候没法切模式,但还得判断亮度。怎么办?答案就藏在每一帧的 JPEG 数据里:JPEG 文件大小本身就是亮度指标。
原理不复杂。暗场景下画面大部分区域是黑的,大面积相同颜色 JPEG 压缩率非常高,文件就小。场景越亮细节越多,文件越大。实际拿 SVGA 分辨率、quality=10 测了一组数据:
- 暗光(关灯):12~14 KB
- 正常室内:14~17 KB
- 明亮(对着窗户):17~25 KB
根据这个做了个线性映射:
| |
精度不如灰度采样,但好处是零额外开销——动态检测本来就要抓帧做帧差,JPEG 大小顺手拿到的,不多干一件事。
两套方案的关系:
- 灰度探测(method=2):精确,但得切模式,有人看画面时不能用
- JPEG 大小推算(method=1):精度一般,但随时能用,没有任何额外消耗
flowchart TD
A[每帧动态检测] --> B{灰度探测可用?}
B -->|30秒内探测过 & 无 MJPEG 客户端| C[使用灰度探测结果]
B -->|MJPEG 客户端在线或超时| D[用 JPEG 大小估算]
C --> E{亮度 < 阈值?}
D --> E
E -->|是| F[标记为暗场景]
E -->|否| G[标记为亮场景]
F --> H[检测到动态时开闪光灯拍照]
G --> I[正常拍照]闪光灯控制:80% 占空比保命
AI-Thinker ESP32-CAM 的闪光灯接在 GPIO4 上。这里有个硬件坑:板子上闪光灯没串限流电阻。LED 直接挂 GPIO 上,全功率跑的话电流可能超标,时间长搞不好烧板子。
所以用 LEDC PWM 控制,占空比卡在 80%(205/255),给自己留条后路:
| |
注意 Timer 和 Channel 不能随便选。摄像头 XCLK 占了 Timer 0 / Channel 0,闪光灯必须用 Timer 1 / Channel 1,不然两个外设打架。这种坑 ESP-IDF 文档里可不会写,烧进去发现不对劲才知道。
闪光灯只在拍照时用,检测的时候绝不开
这个设计很关键。闪光灯只在做完帧差检测、确认有动态、需要保存照片的那一刻才开。做帧差比较的时候绝不开灯。
原因一想就明白:开灯关灯之间画面亮度剧烈变化,帧差算法一看——好家伙整个画面都在动,直接报个 80% 以上的差异,全成假报警了。
| |
开灯之后等了 200ms 才拍照。刚开灯那会儿白平衡还没反应过来,拍出来颜色是偏的。等一下让 OV2640 的自动白平衡追上来,照片才看得清。
暗场景自动降动态检测阈值
还有个细节:暗场景下 JPEG 帧差值天然偏低(画面暗细节少,压缩后帧间差异小),用正常阈值可能检测不到动态。所以暗场景自动把阈值降到原来的四分之一:
| |
不多解释,想一下就能明白为什么。
其他功能
闪光灯是花时间最多的部分,其他就没什么好展开的了,列一下:
- MJPEG 流:双缓冲 + PSRAM,浏览器打开就能看
- WiFi 管理:AP/STA 双模式,首次上电 AP 配网,之后自动 STA
- TF 卡:GPIO14 跟摄像头共用,必须等摄像头初始化完再挂载,顺序搞错直接炸
- NAS 上传:WebDAV/HTTP POST,拍完自动传
- Web 界面:配参数、看状态、下照片,手机操作
- Prometheus 指标:
/metrics端点,丢进 Grafana 画面板
写在后面
回头看,接个光敏电阻可能半小时就完事了。但用摄像头本身做亮度检测这个思路,倒逼着把 OV2640 的模式切换、JPEG 压缩特性、LEDC PWM 资源分配这些细节都摸了一遍。值不值不好说,反正折腾的过程挺有意思的。
代码全在 GitHub:Mi-Bee-Studio/ai-thinker-esp32-cam,README 里有构建和烧录步骤。