1. 项目概述:一个能“看见”你的桌面伙伴
几年前,我在一个创客社区看到了一个叫“Twitch”的电子宠物项目,它通过触摸和眨眼与人互动,非常有趣。但作为一个喜欢折腾嵌入式视觉的人,我总觉得少了点什么——它不能真正地“看”着你。于是,一个想法冒了出来:能不能做一个能主动追踪人脸、与你进行眼神交流的小玩意儿?这就是“WatchEye”的起点。
WatchEye本质上是一个基于ESP32-CAM的人脸追踪桌面机器人。它的核心目标很简单:当你出现在它面前时,它能从“睡眠”中醒来,转动它的“头”(云台),让那只由LED矩阵构成的“眼睛”始终注视着你,并模拟自然的眨眼动作。这不仅仅是一个玩具,更是一个融合了计算机视觉、嵌入式控制和物联网概念的综合性实践项目。它适合对Arduino平台有一定了解,并希望深入探索图像识别与硬件交互的创客、电子爱好者或相关专业的学生。通过这个项目,你将亲手搭建一个从“感知”(摄像头+PIR)到“决策”(人脸识别算法)再到“执行”(伺服电机)的完整智能体,理解如何让冷冰冰的硬件具备基础的“视觉注意力”。
2. 核心硬件选型与设计思路拆解
一个项目能否成功,硬件选型是地基。我的设计原则是在满足功能的前提下,尽可能选择通用、低成本且易于获得的模块,降低大家的复现门槛。
2.1 视觉核心:为什么是ESP32-CAM?
在众多微控制器中选择ESP32-CAM,是基于其独特的“All-in-One”特性。对于人脸追踪应用,我们需要一个能同时处理图像采集、算法运行并控制外设的核心。
- 图像处理能力:ESP32内置的Xtensa双核处理器,其中一个核心可以专门用于运行摄像头驱动和图像处理任务。虽然其算力无法与高端AI芯片相比,但运行经过优化的、基于Haar级联分类器的人脸检测算法(如OpenCV的
lbpcascade_frontalface移植版)是绰绰有余的。这种算法通过在图像上滑动不同大小的检测窗口,并计算其Haar特征(一种基于像素块亮度差的特征),再通过一个预先训练好的级联分类器进行快速判断,效率很高,非常适合嵌入式场景。 - 集成摄像头与Wi-Fi:这是最关键的一点。ESP32-CAM模组直接集成了OV2640摄像头传感器和ESP32芯片,省去了复杂的摄像头接口电路。Wi-Fi功能虽然在本项目中未用于联网,但它为未来扩展(如视频流远程查看、OTA升级)预留了可能,也简化了开发时的串口通信配置(可以通过Wi-Fi进行日志输出和调试)。
- 充足的GPIO与PWM:我们需要控制两个伺服电机(需要PWM信号)和一个I2C接口的LED矩阵,ESP32-CAM引出了足够的IO口,尽管部分引脚在启动时有特殊电平要求,需要仔细分配。
注意:ESP32-CAM没有内置USB转串口芯片,这是新手最容易“踩坑”的地方。直接焊接排针后,它无法像普通Arduino开发板一样通过USB线编程,必须借助外部的USB转TTL串口模块(如FTDI、CH340等)进行烧录和供电。
2.2 执行机构:云台与伺服电机的考量
让人偶“转头”需要两个自由度的运动:水平旋转(Pan)和垂直俯仰(Tilt)。我最初尝试用亚克力板自制云台结构,但对大多数朋友来说,直接使用现成的二自由度云台套件是更稳妥高效的选择。这种套件通常包含两个塑料或金属舵盘和一套支架,结构紧凑,安装方便。
驱动云台的核心是SG90微型伺服电机。选择它主要基于以下几点:
- 扭矩与速度:SG90的扭矩约为1.8kg·cm,对于负载不重的摄像头和LED矩阵足够用。其运动速度也适合实现平滑而非突兀的追踪动作。
- 控制接口:标准的PWM控制,与Arduino生态完全兼容。ESP32可以轻松生成精确的PWM信号来控制其角度。
- 尺寸与功耗:体积小,便于集成到紧凑的外壳中;在非运动状态下的静态电流也较低,有利于电池供电。
2.3 感知与节能:PIR传感器的妙用
让设备24小时全功率运行人脸检测既耗电也无必要。因此,我引入了HC-SR501 PIR(被动红外)传感器作为系统的“唤醒开关”。它的原理是检测人体发出的特定波长红外线变化。
- 工作逻辑:当无人时,ESP32-CAM进入深度睡眠模式,功耗可降至微安级别。PIR传感器持续监测,一旦检测到移动,其输出引脚会从低电平跳变为高电平。我将这个引脚连接到ESP32的一个具有中断唤醒功能的引脚上。引脚电平变化触发中断,将ESP32从深度睡眠中唤醒,随后系统初始化摄像头并开始人脸追踪流程。
- 参数调节:HC-SR501上有两个电位器,可调节延迟时间(触发后输出高电平的持续时间)和灵敏度。在这个项目中,我将延迟时间调得较短(如2-3秒),这样在人员短暂离开后能快速休眠;灵敏度调到适中,避免因远处微小热源误触发。
2.4 “灵魂之窗”:LED矩阵的选择
“眼睛”是赋予项目生命力的关键。我选用了一块8x8的绿色LED点阵屏,并通过I2C接口的驱动板(如HT16K33)进行控制。选择I2C接口是因为它仅需两根信号线(SDA, SCL)即可控制64个LED,极大节省了宝贵的GPIO资源。在驱动板上预存多帧“眼睛”图案(正视、左看、右看、眨眼动画),主控通过I2C发送指令即可快速切换显示,实现生动表情。
3. 电路连接与系统集成详解
正确的电路连接是项目稳定的物理基础。下面我将提供详细的接线表和注意事项。
3.1 核心接线图与供电方案
首先,必须明确供电是重中之重。伺服电机在启动和堵转时会产生很大的瞬时电流,如果与ESP32-CAM共用一套孱弱的电源(比如电脑USB口或小容量电池),极易导致电压骤降,引起ESP32重启或摄像头工作异常。
我的供电方案是:双电源或大容量单电源。
- 推荐方案(分立供电):使用一块5V/2A以上的移动电源或稳压模块单独为两个SG90伺服电机供电。ESP32-CAM和LED矩阵则由另一块电池或USB口供电。两地电源的GND(地线)必须连接在一起,形成共同的参考地。
- 简化方案(统一供电):如果使用一块容量足够大、输出稳定的5V/3A电池,可以为整个系统供电。务必在电源输出端并联一个470μF或以上的电解电容,以缓冲电机动作时的电流冲击。
以下是基于ESP32-CAM常见引脚定义的接线表:
| 元件 | 引脚/接口 | 连接到 ESP32-CAM | 备注 |
|---|---|---|---|
| SG90 (Pan) | 信号线 (黄/橙) | GPIO 12 | 水平旋转舵机 |
| SG90 (Tilt) | 信号线 (黄/橙) | GPIO 13 | 垂直俯仰舵机 |
| 两个SG90 | VCC (红) | 外部5V电源正极 | 切勿接ESP32的5V或3.3V! |
| 两个SG90 | GND (棕/黑) | 外部电源负极 & ESP32 GND | 地线必须共接 |
| LED矩阵 (I2C) | SDA | GPIO 14 | I2C数据线 |
| LED矩阵 (I2C) | SCL | GPIO 15 | I2C时钟线 |
| LED矩阵 (I2C) | VCC | ESP32 3.3V 或 5V | 查看模块支持电压 |
| LED矩阵 (I2C) | GND | ESP32 GND | |
| HC-SR501 PIR | OUT | GPIO 4 | 用于中断唤醒 |
| HC-SR501 PIR | VCC | ESP32 5V | |
| HC-SR501 PIR | GND | ESP32 GND | |
| FTDI编程器 | TX | ESP32 U0R (GPIO3) | |
| FTDI编程器 | RX | ESP32 U0T (GPIO1) | |
| FTDI编程器 | 5V | ESP32 5V | 烧录时供电 |
| FTDI编程器 | GND | ESP32 GND |
实操心得:焊接ESP32-CAM的排针时,建议使用母对母排针,或者将排针焊在背面(元件面),这样可以将板子插在面包板或直接杜邦线连接,避免因焊接不当损坏脆弱的焊盘。给伺服电机接线时,务必先确认电源极性,接反会瞬间损坏舵机。
3.2 PCB设计与集成优化
为了提升项目的可靠性和美观度,我设计并焊接了一块简单的PCB。它将ESP32-CAM、LED矩阵驱动板、伺服电机接口和电源接口集成在一起。这样做的好处是:
- 稳定性:避免了面包板上连接线松动导致的问题。
- 空间优化:所有元件可以紧凑排列,方便安装在云台上。
- 可维护性:电源、电机、信号线路清晰分明。
对于不想自己画PCB的朋友,可以使用万用板(洞洞板)进行焊接,效果一样可靠。核心是将ESP32-CAM的IO口通过排针引出,并与LED矩阵驱动板、伺服电机接口焊在一起。
4. 软件框架与核心代码解析
软件是项目的灵魂。整个程序运行在Arduino IDE环境下,核心逻辑围绕人脸检测、云台PID控制和状态机管理展开。
4.1 开发环境搭建与库文件准备
- 安装ESP32开发板支持:在Arduino IDE的“文件->首选项->附加开发板管理器网址”中,添加
https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后在“工具->开发板->开发板管理器”中搜索并安装“esp32”。 - 选择正确的开发板:安装后,在“工具->开发板”中选择“AI Thinker ESP32-CAM”。
- 安装必要的库:
Adafruit LED Backpack Library和Adafruit GFX Library:用于控制I2C LED矩阵。ESP32Servo库:ESP32专用的伺服电机控制库,能利用其硬件PWM资源,产生更稳定的信号。
4.2 主程序逻辑与状态机
程序的核心是一个简单的状态机,主要包含两个状态:SLEEP_MODE(休眠)和TRACKING_MODE(追踪)。
// 状态定义 enum SystemState { SLEEP_MODE, TRACKING_MODE }; SystemState currentState = SLEEP_MODE; // PIR唤醒中断函数 void IRAM_ATTR wakeUpFromPIR() { // 此函数被PIR中断触发,仅用于唤醒芯片 } void setup() { Serial.begin(115200); // 初始化LED矩阵、伺服电机、摄像头 initEye(); initServos(); initCamera(); // 配置PIR引脚为输入,并绑定中断 pinMode(PIR_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(PIR_PIN), wakeUpFromPIR, RISING); // 初始进入休眠 enterSleepMode(); } void loop() { switch(currentState) { case SLEEP_MODE: // 此处不会执行,因为芯片已休眠 break; case TRACKING_MODE: performFaceTracking(); // 检查是否一段时间未检测到人脸,若是,则返回休眠 if (noFaceTimeout()) { enterSleepMode(); } break; } } void enterSleepMode() { // 关闭摄像头、伺服电机电源(可通过MOSFET控制) // 设置LED矩阵显示“睡眠”图案(如闭合的眼睛) drawSleepyEye(); // 配置ESP32进入深度睡眠,并由PIR引脚中断唤醒 esp_sleep_enable_ext0_wakeup(GPIO_NUM_4, HIGH); // GPIO4 高电平唤醒 esp_deep_sleep_start(); }4.3 人脸检测与云台追踪算法
这是技术核心。我们使用ESP32-CAM库中内置的基于Haar特征的人脸检测功能。
#include “esp_camera.h” #include “fd_forward.h” // 人脸检测前向声明 camera_fb_t *fb = NULL; // 帧缓冲区 dl_matrix3du_t *image_matrix = NULL; // 图像矩阵 box_array_t *net_boxes = NULL; // 检测到的人脸框数组 void performFaceTracking() { fb = esp_camera_fb_get(); // 从摄像头获取一帧图像 if (!fb) return; // 将图像数据转换为算法可处理的格式 fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item); // 进行人脸检测 net_boxes = face_detect(image_matrix, &mtmn_config); if (net_boxes != NULL && net_boxes->len > 0) { // 至少检测到一张人脸 box_t face_box = net_boxes->box[0]; // 取第一个(最大)人脸框 int face_center_x = face_box.box_p[0] + face_box.box_p[2] / 2; // 人脸框中心X坐标 int face_center_y = face_box.box_p[1] + face_box.box_p[3] / 2; // 人脸框中心Y坐标 // 计算人脸中心与图像中心的偏差 int img_center_x = fb->width / 2; int img_center_y = fb->height / 2; int error_x = face_center_x - img_center_x; int error_y = face_center_y - img_center_y; // 根据偏差控制云台转动 controlPanTilt(error_x, error_y); // 更新LED眼睛看向人脸方向 updateEyeDirection(error_x); // 重置无人脸超时计数器 resetNoFaceTimer(); } else { // 未检测到人脸,可能执行缓慢扫视或等待 slowlyScan(); } esp_camera_fb_return(fb); // 释放帧缓冲区 fb = NULL; if(net_boxes != NULL){ free(net_boxes); net_boxes = NULL; } }关键参数解析:
mtmn_config:这是一个结构体,包含了人脸检测算法的阈值参数(如评分阈值、非极大值抑制阈值等)。适当调低阈值可以提高检测灵敏度,但也可能增加误检(如将某些物体识别人脸)。通常使用默认值即可。- 图像分辨率:在
camera_config_t中设置frame_size = FRAMESIZE_VGA (640x480)。这是性能与精度的平衡点。QVGA (320x240) 更快但检测距离近;SVGA (800x600) 更清晰但处理速度慢,可能导致追踪不流畅。
4.4 云台伺服电机控制:从偏差到动作
计算出人脸中心与画面中心的偏差(error_x,error_y)后,需要将其转化为伺服电机的目标角度。这里采用最简单的比例(P)控制,就已经能获得不错的效果。
#include <ESP32Servo.h> Servo panServo; // 水平舵机 Servo tiltServo; // 垂直舵机 int panCenterAngle = 90; // 舵机中位角度 int tiltCenterAngle = 90; float Kp = 0.1; // 比例系数,需要根据实际测试调整 void controlPanTilt(int errorX, int errorY) { // 计算角度调整量,误差越大,调整角度越大 int panAdjust = (int)(-errorX * Kp); // 注意符号:人脸偏右(errorX>0),云台应向左转(角度减小) int tiltAdjust = (int)(errorY * Kp); // 人脸偏下(errorY>0),云台应向上转(角度增大) int targetPanAngle = constrain(panCenterAngle + panAdjust, 0, 180); int targetTiltAngle = constrain(tiltCenterAngle + tiltAdjust, 0, 180); panServo.write(targetPanAngle); tiltServo.write(targetTiltAngle); // 可选:加入死区(Dead Zone),当误差很小时不动作,避免电机持续微颤 // if(abs(errorX) < 10) { panServo.write(panCenterAngle); } // if(abs(errorY) < 10) { tiltServo.write(tiltCenterAngle); } }实操心得:比例系数
Kp的调整至关重要。Kp值太大,云台会剧烈振荡,在目标周围来回摆动;Kp值太小,追踪则缓慢迟钝,跟不上人的移动。最佳方法是在固定位置测试:让人脸在画面中缓慢移动,观察云台跟随是否平滑且无超调。通常从0.05开始尝试。
5. 机械组装与外壳制作
硬件和软件调试完毕后,将它们可靠地组装起来是最后一步。
5.1 云台与主控板的安装
- 将两个SG90伺服电机安装到云台套件中,通常水平舵机作为底座,垂直舵机安装在水平舵机的舵盘上。
- 将焊接好的主控PCB(集成了ESP32-CAM和LED矩阵)牢固地固定在垂直舵机的舵盘上。确保摄像头镜头前方无遮挡,LED矩阵朝前。
- 仔细整理伺服电机、PIR传感器的连接线,可以用扎带或热熔胶固定,避免运动时拉扯脱落。
5.2 外壳设计与制作
外壳不仅是为了美观,也起到保护内部电路和塑造“宠物”形象的作用。我使用5mm厚的椴木板通过激光切割制作了一个立方体盒子。
- 设计要点:
- 前脸开孔:为摄像头镜头、LED矩阵(“眼睛”)和PIR传感器预留精确的开孔。PIR传感器的菲涅尔透镜需要暴露在外。
- 侧边开孔:预留电源开关孔、充电接口孔(如果使用锂电池)以及散热孔。
- 内部空间:确保有足够空间容纳电池(我使用了一块18650锂电池盒)和所有线路,并留有余量便于散热。
- 可拆卸面板:至少设计一个可拆卸的面板(如用螺丝固定),方便后期调试和维护。
- 装饰:我用黑色卡纸为LED矩阵做了一个“眼罩”装饰框,让圆形的点阵屏看起来更像一只眼睛,增强了拟人化效果。
6. 调试、优化与问题排查实录
项目搭建过程中,一定会遇到各种问题。下面是我总结的常见问题及其解决方案。
6.1 摄像头初始化失败或图像异常
- 现象:串口监视器提示 “Camera probe failed” 或 “Camera init failed”。
- 排查:
- 电源问题(最常见):ESP32-CAM对电源非常敏感。确保使用5V/2A以上的电源单独为其供电,或者在其3.3V稳压芯片输入端并联一个100μF以上的电容。
- 引脚冲突:检查代码中摄像头引脚定义是否与你的硬件版本匹配。不同厂商的ESP32-CAM模组引脚定义可能有细微差别。
- 硬件损坏:摄像头排线是否插紧?可以尝试重新拔插。在排除软件和供电问题后,有可能是摄像头模组本身损坏。
6.2 伺服电机不转动或抖动
- 现象:电机发出“滋滋”声但不转动,或轻微抖动。
- 排查:
- 供电不足:这是绝对的首因。用万用表测量电机VCC和GND之间的电压,在电机转动时是否跌落到4.5V以下?如果是,请立即升级你的电源。
- 信号线连接错误:确认信号线连接到了ESP32正确的PWM引脚(如GPIO12, 13)。
- 代码问题:确保使用了正确的伺服电机库(ESP32Servo),并且
Servo.attach(pin)函数执行成功。检查write()函数的值是否在0-180之间。 - 机械卡死:手动转动舵盘,检查云台结构是否有物理干涉,导致电机扭矩不足。
6.3 人脸检测不稳定或无法检测
- 现象:时而有反应,时而没有,或者距离稍远就检测不到。
- 优化:
- 光照条件:Haar分类器对光照敏感。确保环境光线充足、均匀,避免强烈的逆光或侧光在人脸上造成深阴影。
- 调整检测阈值:尝试修改
mtmn_config中的阈值参数,例如降低score_threshold以提高灵敏度(但会增加误检)。 - 降低图像分辨率:如果追踪延迟大,可以尝试将
frame_size改为FRAMESIZE_QVGA,牺牲一些检测距离换取更高的帧率。 - 优化检测区域:如果场景背景复杂,可以尝试只对图像中心区域进行人脸检测,减少计算量。
6.4 PIR传感器误触发或不触发
- 现象:没人的时候自己醒了,或者人走过没反应。
- 调节:
- 调节灵敏度电位器:顺时针调高灵敏度,逆时针调低。将其调整到人正常行走经过时能稳定触发,但远处轻微热源变化(如空调出风口)不会触发的程度。
- 调节延时电位器:这个决定了触发后输出高电平的持续时间。对于本项目,可以调短一些(如2-5秒),因为一旦唤醒,就由人脸检测逻辑来决定何时休眠。
- 注意安装位置:避免将PIR传感器正对着窗户、暖气片或风扇,这些地方的温度变化会引起误报。
6.5 系统整体功耗与续航
为了延长电池续航,除了使用PIR传感器控制深度睡眠外,还可以:
- 在软件休眠前,显式地关闭摄像头电源(如果硬件上支持通过MOSFET控制),并将伺服电机切换到省力模式(部分舵机支持)。
- 选择高效的电源模块,如使用带真关断功能的低压差稳压器(LDO)。
- 实测:在我的配置中(使用一节3400mAh的18650电池),在无人状态下(深度睡眠),系统待机电流约0.8mA,理论上可待机数月。在活跃追踪状态下,整体电流约300-400mA,可持续工作8-10小时。
完成所有调试后,你的WatchEye就应该能安静地待在角落,一旦你进入它的视野,便会转动那双灵动的“眼睛”,静静地跟随你的身影。这个项目从视觉感知到物理运动的完整链条,为我们打开了嵌入式智能交互设备的一扇窗,其中的每个模块——从传感器的选型、控制算法的调试到机械结构的实现——都是未来更复杂项目不可或缺的基石。