本帖最后由 小虾米小 于 2025-11-19 10:37 编辑
先看效果:
在你的桌面上,有一双眼睛一直灵活的在跟着你动,是不是很有感觉?如果你说话的时候,他也在看着你,并且还时不时的眨眨眼,就更加的活灵活现了。
我用矩阵激光传感器捣腾了这样一个应用。
开干!
硬件准备
Firebeetle ESP32-C5,货号:DFR1236,产品链接:https://www.dfrobot.com.cn/goods-4196.html

3.5英寸IPS触摸显示屏,货号:DFR1092,产品链接:https://www.dfrobot.com.cn/goods-4264.html

矩阵激光测距传感器,货号:SEN0628,产品链接:https://www.dfrobot.com.cn/goods-4243.html

硬件连接
ESP32-C5与3.5屏幕之间使用显示屏配送的FPC线进行连接。
矩阵激光传感器使用PH2.0-4P线连接到ESP32-C5扩展板的I2C接口。
如下图所示:

将矩阵激光传感器的拨码开关拨到如图所示的位置,设置模式为I2C模式,地址为0x33。

将矩阵激光传感器用双面胶粘贴在显示屏上方,注意,由于显示屏上方有排针孔,不要短路了。也可以粘贴到其它合适的地方。

下载及安装库
使用Arduino进行开发,相关配置如下:
打开Arduino平台,首先安装库矩阵激光Arduino库:DFRobot_matrixLidarDistanceSensor
链接:https://github.com/DFRobot/DFRobot_matrixLidarDistanceSensor.git

在Arduino平台上打开工具-导入库-添加.zip库,选择刚刚重命名的zip库,确定进行添加。
在Arduino平台上打开文件-示例-DFRobot_matrixLidarDistanceSensor,你能看到它就代表你已成功导入。

接下来,下载并安装DFRobot_GDL库,方法同矩阵激光传感器库相似。库链接:https://gitee.com/dfrobot/DFRobot_GDL
安装ESP32 SDK
安装教程请见链接:https://wiki.dfrobot.com.cn/Add_ESP32_board_to_Arduino_IDE
选择板卡
SDK安装完成后,按如图所示选择板卡为“ESP32C5 Dev Module” 。

修改显示屏SPI引脚映射
在ESP32-C5上使用FPC线连接显示屏时,需修改显示屏的引脚映射,修改方式如下:
在电脑的如下目录,打开“pins_arduino.h”文件:C:\Users\Administrator\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.3.4-cn\variants\esp32c5

修改引脚映射表:
引脚名称
| 对应引脚
| 备注
| SCLK
| 23
|
| MOSI
| 24
|
| MISO
| 25
|
| SDA
| 9
|
| SCL
| 10
|
|
具体修改位置如图:

下载代码及素材文件
下载地址: 眼睛跟随人动源码及素材.rar
程序下载
程序功能
- 当检测距离小于30 mm时,人物处于闭眼状态。
- 当检测距离在30-1500 mm时,人物眼睛处于跟踪状态。
- 当检测距离大于1500mm时,人物处于眨眼状态。
程序编译及下载
打开附件中的ino代码,并编译下载。

最终完成效果见帖子最上方。
实现原理
矩阵激光传感器WIKI链接:https://wiki.dfrobot.com.cn/SKU_SEN0628_%E7%9F%A9%E9%98%B5%E6%BF%80%E5%85%89%E6%B5%8B%E8%B7%9D%E4%BC%A0%E6%84%9F%E5%99%A8
通过WIKI我们发现,矩阵激光会打出8*8共64个测距点,这就意味着不仅可以检测障碍物的距离,还能检测障碍物的位置。这是实现移动检测的主要原理。如图:

在显示部分,原理如下:
先用AI生成一张人脸图。
正脸就像是一张最底部照片的画布,当程序处于眨眼状态时,眉毛和眼睛都会持续的跟新局部像素进行刷新,持续在最底部的画布上进行绘制眉毛和眼睛的绘制,以实现眨眼动画的刷新
眼睛的绘制主要我是通过在眼白画布上持续的刷和写,这样其他地方的图片像素不需要改变,只需要让眼睛眼白中进行刷写就能够实现眼睛的眼睛眼白中运动,在眼白中建立坐标系,这样我们就只需要知道每一帧眼睛的坐标即可。

眼睛在眼白中圆滑运动实现原理,当坐标出现变化时,当前眼睛坐标(x1,y1),离目标眼睛(x2,y2)的差值|(x2-x1,y2-y1)|越大时,眼珠变化的速度就越快,反之越小,这样实现了平滑运动,如下图所示:

实现步骤
图片帧素材准备
在这个项目时,首先要准备除了需要用到的试验耗材外,还需准备图片素材。这个图片素材需要使用视频素材转化而来。
如果你没有直接使用适合的素材,可使用AI生成一张合适的人物正脸图片,有了这张图片过后,使用这个链接AI生****物闭上眼睛的动态视频: https://app.klingai.com/global/video-extend/new.
拥有视频过后,你就可以通过(剪映、pr工具)提取出该人物的动态连续帧,如下:

图片处理
选出几个图片关键帧,并对关键部位(如眼球、眉毛、眼框)进行裁剪和提取,我这里选了四张关键帧进行处理,如下:
  
注意:1.在进行图片裁剪时,我们需要注意记录(眼球、眼眶、眉毛)的照片的像素大小要一致。因为这些地方在程序进行动态改变时,需要保证像素不变,否则会造成像素撕裂。
在进行裁剪时,需要记住每张裁剪的坐标,这个坐标都是裁剪坐标左上角,可使用光标进行标注进行X-Y轴坐标的查看,这些坐标需要保留到程序中使用。

需要保留大小和坐标清单如下(除眼球中点坐标除外):

图片转化
接下来就是将png图片转化为RGB565,这一步需要使用到转换工具。
可以使用bilbil 博主(爆辣小电匞)的工具教程,链接如下:https://www.bilibili.com/video/BV1oe411176s/?vd_source=e791586b6b2a1e5dc8602bdf57db8b18
将上面处理的这些图片一并进行转换,结果在素材的像素文件里,如下:

在代码中我合并成了一个figure.h文件,这里的figure.h文件是figure.png转化而来
注意:1.在前面的实现原理中,我们提到了眼白画布,既然它是画布,它其中的数据就是可变的,在进figure.h进行合并时,需要将左右眼白的画布定义为二维数组,并且不能使用PROGMEM和const进行声明。其它的都可以使用const和PROGMEM进行声明,这样可以将大量的数据存放在flash中,减少RAM的负担,我使用的ESP32 C5,如果全部放在RAM中,编译时会报错(内存不足)。

程序说明
这一部分是眼睛裁剪大小和坐标的关键参数(如果你要改动素材,就需要改动这里)

函数说明
这个程序实现了一个基于3.5英寸屏幕和激光矩阵距离传感器的交互式眼睛显示系统。主要功能是通过激光传感器检测用户位置,控制屏幕上的一对虚拟眼睛进行实时跟踪和表情变化:当检测到用户靠近时,眼睛会注视用户;用户远离时眨眼;用户离的太近时闭眼;还能实现平滑的眼球移动和自然的眨眼动画效果;创造出生动的人机交互体验。
- /*!
- * @file screenInteractiveEye.ino
- * @brief This document mainly realizes the interactive display function of a 3.5-inch screen combined with a laser matrix distance sensor
- * @copyright Copyright (c) 2025 DFRobot Co.Ltd (http://www.dfrobot.com)
- * @license The MIT License (MIT)
- * @author [jiali](zhixinliu@dfrobot.com)
- * @version V1.0
- * @date 2025-07-15
- * @url https://www.dfrobot.com.cn/goods-4167.html
- */
- #include "Arduino.h"
- #include "DFRobot_GDL.h"
- #include "DFRobot_MatrixLidar.h"
- #include "figure.h"
-
- #define TFT_DC 8
- #define TFT_CS 27
- #define TFT_RST 26
- #define TFT_BL 15
-
- #define MIN(a, b) ((a) < (b) ? (a) : (b))
- #define MAX(a, b) ((a) > (b) ? (a) : (b))
-
- DFRobot_ST7365P_320x480_HW_SPI display(/*dc=*/TFT_DC,/*cs=*/TFT_CS,/*rst=*/TFT_RST,/*bl=*/TFT_BL);
- DFRobot_MatrixLidar_I2C tof(0x33);
-
- #define EYE_SOCKET_WIDE 49
- #define EYE_SOCKET_LONG 43
- #define EYE_MID_POS_X 24 //眼睛中点在画布上的坐标
- #define EYE_MID_POS_Y 21
- #define LEFT_EYE_COORDINATE_X 81
- #define LEFT_EYE_COORDINATE_Y 188
- #define RIGHT_EYE_COORDINATE_X 194
- #define RIGHT_EYE_COORDINATE_Y 188
- const uint8_t eyebrowWide = 185,eyebrowLong = 21;
- const uint8_t eyebrowX = 72,eyebrowY = 152;
- const uint8_t eyeWide1 = 54,eyeLong1 = 54;
- const uint8_t eyeLeftX = 77,eyeLeftY = 180;
- const uint8_t eyeRight_x = 192,eyeRight_y = 180;
-
- //眼睛状态
- uint8_t eyeStates = 4;//0---睁眼状态 1---闭眼状态 2---眨眼过程中
-
- int eyeWide = 27;//眼睛照片的大小
- int eyeLong = 35;
- int baseEye_x = EYE_MID_POS_X;
- int baseEye_y = EYE_MID_POS_Y;
- uint8_t oldEye_l[35][27] = {0};
- uint8_t oldEye_r[35][27] = {0};
- uint8_t first_erasure = 0;
-
- int baseMoveEyeLeft_x = 28;//左眼睛中点在画布上的坐标
- int baseMoveEyeLeft_y = 22;
- int baseMoveEyeRight_x = 19;//右眼睛中点在画布上的坐标
- int baseMoveEyeRight_y = 22;
-
- uint64_t newTime;
- uint64_t oldTime;
-
- // 坐标范围
- const uint8_t X_MIN = 5;
- const uint8_t X_MAX = 43;
- const uint8_t Y_MIN = 10;
- const uint8_t Y_MAX = 37;
-
- uint8_t targetX = 0,targetY = 0;
-
- int32_t focusX = -255;
- int32_t focusY = -255;
- int32_t randomFocusX = 255,randomFocusY = 255;
- int32_t minThld = 1500;
-
- uint8_t interactFlag = 0;
- uint8_t oldInteractFlag = 0;
- uint8_t blinkFlag = 0;
-
- uint64_t end_time;
- uint64_t start_time;
-
- void setup()
- {
- Serial.begin(115200);
- display.begin();
- display.setRotation(0);
-
- display.drawPIC(0,0,320 ,480,(uint8_t *)figure);
-
- sensorModuleInit();
-
- oldTime = millis();
- }
-
- void loop()
- {
- interactFlag = coordinatop(0);
-
- if(interactFlag == 0 && oldInteractFlag != 0){
- eyeStates = 1;
- }else if(interactFlag == 1){
- eyeStates = 4;
- }else if(interactFlag == 2 && oldInteractFlag != 2){
- eyeStates = 2;
- blinkFlag = 1;
- }
-
- oldInteractFlag = interactFlag;
-
- newTime = millis();
- if(newTime -oldTime >= 3000){
-
- if(blinkFlag == 0 && eyeStates == 2){
- blinkFlag = 1;
- }
- oldTime = newTime;
- }
- eyeInteraction();
- }
-
- void sensorModuleInit(){
- while(tof.begin() != 0){
- Serial.println("begin error !!!!!");
- }
- Serial.println("begin success");
-
- while(tof.setRangingMode(eMatrix_8X8) != 0){
- Serial.println("init error !!!!!");
- delay(1000);
- }
- Serial.println("init success");
-
- // for(int i = 0;i<2;i++){
- // minThld = 5000;
- // interactFlag = coordinatop(1);
- // delay(10);
- // }
- onBaseDraweye(1);
- eyeWhiteRefresh();
- }
-
- uint8_t coordinatop(uint8_t initMode)
- {
- uint16_t buf[64];
- int32_t minDate = minThld;
- int32_t temp;
- focusX = 255;
- focusY = 255;
-
- uint8_t flag[4] = {0};
-
- tof.getAllData(buf);
- for(int i = 0;i < 8;i++){
- for(int j = 0;j<8;j++){
- if(initMode){
-
- if(minThld > buf[(i*8)+j]){
- minThld = buf[(i*8)+j];
- }
- }else{
- // Serial.print(buf[(i*8)+j]);
- // Serial.print("\t");
- if(buf[(i*8)+j]<30){
- flag[0] = 1;
- }
- if(buf[(i*8)+j] > minThld){
- flag[2] = 1;
- }
- if(minDate >= buf[(i*8)+j]){
- minDate = buf[(i*8)+j];
- focusX = j + 1;
- focusY = i + 1;
- flag[1] = 1;
- }
-
- }
- }
- // Serial.println();
- }
- // Serial.println("______________________________");
- for(int i = 2;i >= 0;i--){
- if(flag[i] == 1){
- flag[3] = i;
- }
- }
-
- if(initMode){
- minThld = minThld - 30;
- if(minThld > 1330){
- minThld = 1300;
- }
- }
-
- return flag[3];
- }
-
- //限幅
- void positionLimitation(uint8_t *x,uint8_t *y)
- {
- MIN(*x,X_MIN);
- MAX(*x,X_MAX);
- MIN(*y,Y_MIN);
- MAX(*y,Y_MAX);
- }
-
- uint8_t V[5] = {7, 5, 3, 2, 1}; // 速度数组(步长值)
-
- // 眼睛平滑移动函数
- void eyeSmoothMotion(uint8_t targetX, uint8_t targetY) {
- static uint8_t oldTargetX = 255, oldTargetY = 255; // 存储上一次的目标位置
- static bool isMovingX = false, isMovingY = false;
- if (oldTargetX != targetX) {
- oldTargetX = targetX;
- isMovingX = true; // 标记X轴开始移动
- }
- if (oldTargetY != targetY) {
- oldTargetY = targetY;
- isMovingY = true; // 标记Y轴开始移动
- }
- int diffX = targetX - baseEye_x;
- int diffY = targetY - baseEye_y;
-
- uint8_t absDiffX = abs(diffX);
- uint8_t absDiffY = abs(diffY);
- if (isMovingX) {
- uint8_t vIndexX;
- if (absDiffX > 20) {
- vIndexX = 0;
- } else if (absDiffX > 15) {
- vIndexX = 1;
- } else if (absDiffX > 5) {
- vIndexX = 2;
- } else if (absDiffX > 2) {
- vIndexX = 3;
- } else {
- vIndexX = 4;
- }
- uint8_t speedStepX = V[vIndexX];
- if (absDiffX <= speedStepX) {
- baseEye_x = targetX;
- isMovingX = false;
- } else {
- if (diffX > 0) {
- baseEye_x += speedStepX;
- }else {
- baseEye_x -= speedStepX;
- }
- }
- }
- if (isMovingY) {
- uint8_t vIndexY;
- if (absDiffY > 20) {
- vIndexY = 0;
- } else if (absDiffY > 15) {
- vIndexY = 1;
- } else if (absDiffY > 5) {
- vIndexY = 2;
- } else if (absDiffY > 2) {
- vIndexY = 3;
- } else {
- vIndexY = 4;
- }
-
- uint8_t speedStepY = V[vIndexY];
- if (absDiffY <= speedStepY) {
- baseEye_y = targetY;
- isMovingY = false;
- } else {
- if (diffY > 0) {
- baseEye_y += speedStepY;
- } else {
- baseEye_y -= speedStepY;
- }
- }
- }
- }
-
- void eyeWhiteRefresh(void)
- {
- display.drawPIC(LEFT_EYE_COORDINATE_X,LEFT_EYE_COORDINATE_Y,EYE_SOCKET_WIDE ,EYE_SOCKET_LONG,(uint8_t*)base_l);
- display.drawPIC(RIGHT_EYE_COORDINATE_X,RIGHT_EYE_COORDINATE_Y,EYE_SOCKET_WIDE ,EYE_SOCKET_LONG,(uint8_t*)base_r);
- }
-
- void onBaseDraweye(uint8_t initPlace)
- {
- int drawEyeLeft_x,drawEyeRight_x;
- int drawEyeLeft_y,drawEyeRight_y;
-
- int drawEye_x,drawEye_y;
-
- if(initPlace == 0){
- drawEye_x = baseEye_x - eyeWide / 2;//眼睛照片在画布左上角的坐标
- drawEye_y = baseEye_y - eyeLong / 2;
-
- drawEyeLeft_x = drawEye_x;
- drawEyeLeft_y = drawEye_y;
- drawEyeRight_x = drawEye_x;
- drawEyeRight_y = drawEye_y;
-
- }else{
- drawEye_x = baseMoveEyeLeft_x - eyeWide / 2;//眼睛照片在画布左上角的坐标
- drawEye_y = baseMoveEyeLeft_y - eyeLong / 2;
-
- drawEyeLeft_x = drawEye_x;
- drawEyeLeft_y = drawEye_y;
-
- drawEye_x = baseMoveEyeRight_x - eyeWide / 2;//眼睛照片在画布左上角的坐标
- drawEye_y = baseMoveEyeRight_y - eyeLong / 2;
-
- drawEyeRight_x = drawEye_x;
- drawEyeRight_y = drawEye_y;
-
- }
-
- static int oldDrawEye_L_x = drawEye_x;
- static int oldDrawEye_L_y = drawEye_y;//保存老的坐标点
-
- static int oldDrawEye_R_x;
- static int oldDrawEye_R_y;
-
- if(first_erasure == 0){
- first_erasure = 1;
- }else{
- erasePaint(oldDrawEye_L_x,oldDrawEye_L_y,0,base_l,oldEye_l);
- erasePaint(oldDrawEye_R_x,oldDrawEye_R_y,0,base_r,oldEye_r);
- }
- erasePaint(drawEyeLeft_x,drawEyeLeft_y,1,base_l,oldEye_l);
- erasePaint(drawEyeRight_x,drawEyeRight_y,1,base_r,oldEye_r);
- oldDrawEye_L_x = drawEyeLeft_x;
- oldDrawEye_L_y = drawEyeLeft_y;
- oldDrawEye_R_x = drawEyeRight_x;
- oldDrawEye_R_y = drawEyeRight_y;
-
- }
-
- void erasePaint(int draweye_x,int draweye_y,uint8_t erasureFlag,uint16_t base[43][49],uint8_t oldEye[35][27])
- {
- int itemp,jtemp;
-
- for(int i = 0;i < eyeLong;i++){
- itemp = draweye_y + i;
- if(itemp < 0 || itemp >= EYE_SOCKET_LONG){
- continue;
- }
- for(int j = 0;j < eyeWide;j++){
- jtemp = draweye_x + j;
- if(jtemp < 0 || jtemp >= EYE_SOCKET_WIDE)
- continue;
- if(erasureFlag){
- if(base[itemp][jtemp] == 0xEF1B){
- oldEye[i][j] = 1;
- base[itemp][jtemp] = eye[i][j];
- }
- }else{
- if(oldEye[i][j] == 1){
- base[itemp][jtemp] = 0xEF1B;
- oldEye[i][j] = 0;
- }
- }
- }
- }
- }
-
- void eyeInteraction()
- {
- static uint8_t blinkingCount = 0;
- static uint8_t eyeTrackFlag = 0;
- static uint8_t eyeStates_ = eyeStates;
-
- switch(eyeStates_)//0---睁眼状态 1---闭眼状态 2---闭眼过程中 3.睁眼过程中
- {
- case 0:
- blinkingCount = 0;
- if(eyeStates_ != eyeStates){
- eyeStates_ = 5;
- }
- break;
- case 1://闭上眼睛
- if(blinkingCount < 2){
- blinkingCount+=1;
- }
- if(eyeStates_ != eyeStates){
- eyeStates_ = 3;
- }
-
- break;
- case 2://闭眼过程中,衔接睁眼
- if(blinkFlag){
- if(++blinkingCount >= 2){
- eyeStates_ = 3;
- }
- }else{
- blinkingCount = 0;
- }
- if(eyeStates_ != eyeStates && blinkFlag == 0){
- eyeStates_ = 5;
- }
- break;
- case 3://睁眼过程中,衔接正脸
- if(--blinkingCount <= 0){
- eyeStates_ = 0;
- blinkFlag = 0;
- }
- break;
- case 4://眼睛跟踪扫描
- if(eyeTrackFlag){//启动之前确保眼睛处于睁开状态
- eyeballRefresh();
- }else{
- eyeRefresh(0);
- eyeTrackFlag = 1;
- }
- if(eyeStates_ != eyeStates){
- eyeStates_ = 5;
- eyeTrackFlag = 0;
-
- }
- break;
- case 5://转换器
-
- if(eyeStates_ != eyeStates){
-
- eyeStates_ = eyeStates;
- blinkingCount = 0;
- }
-
- break;
- }
-
- if(eyeStates_ <= 3 && eyeStates_ >= 0 || eyeStates_ == 5){
- eyeRefresh(blinkingCount);
- }
- }
-
- void eyeballRefresh(void)
- {
- if(interactFlag == 4){
- targetX = EYE_MID_POS_X;
- targetY = EYE_MID_POS_Y;
- }else{
- targetX = map(focusX,0,7,X_MAX, X_MIN);
- targetY = map(focusY,0,7,Y_MIN, Y_MAX);
- positionLimitation(&targetX,&targetY);
- }
- eyeSmoothMotion(targetX,targetY);
- onBaseDraweye(0);
- eyeWhiteRefresh();
- }
-
- void eyeRefresh(uint8_t blink)
- {
- switch(blink)
- {
- case 0:
- display.drawPIC(eyeLeftX,eyeLeftY,eyeWide1 ,eyeLong1,(uint8_t*)eye_l_0);
- display.drawPIC(eyeRight_x,eyeRight_y,eyeWide1 ,eyeLong1,(uint8_t*)eye_r_0);
- display.drawPIC(eyebrowX,eyebrowY,eyebrowWide ,eyebrowLong,(uint8_t*)eyebrow_0);
- break;
- case 1:
- display.drawPIC(eyeRight_x,eyeRight_y,eyeWide1 ,eyeLong1,(uint8_t*)eye_r_2);
- display.drawPIC(eyeLeftX,eyeLeftY,eyeWide1 ,eyeLong1,(uint8_t*)eye_l_2);
- display.drawPIC(eyebrowX,eyebrowY,eyebrowWide ,eyebrowLong,(uint8_t*)eyebrow_3);
- break;
- case 2:
- //display.drawPIC(eyebrowX,eyebrowY,eyebrowWide ,eyebrowLong,(uint8_t*)eyebrow_3);
- display.drawPIC(eyeLeftX,eyeLeftY,eyeWide1 ,eyeLong1,(uint8_t*)eye_l_3);
- display.drawPIC(eyeRight_x,eyeRight_y,eyeWide1 ,eyeLong1,(uint8_t*)eye_r_3);
- break;
- }
- }
复制代码
|