FastBond2阶段2-用ESP32S3基于机器学习识别猫咪的喂食器
FastBond2 ESP32-S3 机器学习 OV2640 舵机 esp-idf vscode I2C
标签
嵌入式系统
测试
显示
FastBond第二季
aramy
更新2023-10-20
1037
接触了很多日常使用的小电器,手机、智能音箱、打卡机……。现在的机器学习能力越来越强大了,语音识别、人脸识别都不在话下。学习到esp32的AI能力,也是让我羡慕不已,总想自己动手做一个项目来体验一下机器学习的强大,借此Fastboand活动,尝试做一个猫咪猫咪喂食的项目,来玩一把机器学习!
一、项目思路
本项目设计为一个家庭小型的宠物智能投喂器。基本功能就是通过摄像头识别宠物是否为猫咪,如果是猫咪,就投喂食物,不是就不投喂食物。按活动要求,我选用乐鑫家的ESP系列产品。原计划使用的是ESP32-S2,但是看有新产品ESP32-S3出来了,就喜新厌旧一把,使用更新的产品ESP32-S3。
硬件准备:项目开始,有两个思路。
1、是选择ESP32-S3模块,自己制作PCB,完成整个项目,但是自己的焊接手艺实在上不了台面,又特喜欢小的封装,于是放弃这条思路。
2、是选择已有的开发板,用来拓展功能。在这个思路下去寻找ESP32-S3的开发板,最贴合自己需求的是ESP-EYE板子,但是价格太贵,放弃了。
最终选择了Firebeetle 2 ESP32-S3这个板子,价格不错,主控是ESP32-S3-WROOM-1-N16R8模组拥有16MB Flash和8MB PSRAM。主控是32 位 LX7 双核处理器,主频高达 240 MHz;内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions);45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、DAC、UART、SD/MMC 主机控制器和 TWAITM 控制器等。板子上带着OV2640摄像头。板子也很小巧。在这个板子的基础上,再做一个扩展PCB,加上TFT显示屏,就可以来跑自己自己的项目啦!既然打样了PCB就顺便加上了麦克风、SD卡,和一颗SHT30。
FpVAZHutRCfRegdzxFw4LPODeMwU
FhshkZRq9XfPJJ97Vb2lPifG8Y_C
FrLLP_3IGdB5W9NImTu6r9nyS603
 

二 设计框图及原理介绍
项目框图初步设计都是由Scheme-it网页绘制。这里是分享链接:

https://www.digikey.cn/schemeit/project/esp32-s3-catfeed-3cf574619fec4bfa85f548852a34f69c

FhiOJoOtUKlVWokgwRVk_7MEWBQh

如图所示,整个项目并不复杂。OV2640摄像头作为信号输入,摄像头将图像信息传送给主控。主控进行分析,是否是猫咪出现。这里判断猫咪出现使用了乐鑫提供的AI算法,使用了ESP-WHO这个项目中的cat_face_detection例程。主控将摄像头获得的图像在TFT屏幕上显示出来,当判断出当前图像中有猫咪存在,就驱动舵机去添加猫粮。这里舵机我使用了优必选的串口舵机,使用3个串口舵机做成一个机械臂,用来实现需要的动作。这个舵机是串口舵机,通过串口命令字来进行控制。
FouCNEf_h7KnXL0m6C5DYRQAiUZG

FhS-K1489KPwt3VBnn9ZlZuwWDPQFvTKRrkNrMmKXPnoC6INWHUqpRpe

三 项目具体实现

整改项目是依赖cat_face_detection例程来实现的,首先从github上将esp-who项目项目clone回来。首先驱动摄像头,在配置文件中我选择了“ESP-S3-EYE DevKit”,然后修改components/modules/camera/who_camera.h文件,这个文件中指明了OV2640与ESP32S3连接对应的管脚。

#elif CONFIG_CAMERA_MODULE_ESP_S3_EYE
#define CAMERA_MODULE_NAME "ESP-S3-EYE"
#define CAMERA_PIN_PWDN -1
#define CAMERA_PIN_RESET -1

#define CAMERA_PIN_VSYNC 6
#define CAMERA_PIN_HREF 42
#define CAMERA_PIN_PCLK 5
#define CAMERA_PIN_XCLK 45

#define CAMERA_PIN_SIOD 1
#define CAMERA_PIN_SIOC 2

#define CAMERA_PIN_D0 39
#define CAMERA_PIN_D1 40
#define CAMERA_PIN_D2 41
#define CAMERA_PIN_D3 4
#define CAMERA_PIN_D4 7
#define CAMERA_PIN_D5 8
#define CAMERA_PIN_D6 46
#define CAMERA_PIN_D7 48

Firebeetle 2 ESP32-S3这个板子摄像头兼容OV2640和0V7725,板子上使用了一颗AXP313A的电源管理芯片,来控制摄像头的电源。所以在使用摄像头之前还需要使用IIC协议,控制电源芯片,先给摄像头供电。修改components/modules/camera/who_camera.c文件,这里我使用的是esp-idf4.4的写法。

#include "who_camera.h"

#include "esp_log.h"
#include "esp_system.h"
static const char *TAG = "who_camera";

// ---------------------------------------------
#include "driver/i2c.h"
#define AXP313A_ADDR                 0x36  
#define I2C_MASTER_SCL_IO           2
#define I2C_MASTER_SDA_IO           1     
#define I2C_MASTER_NUM              0 
#define I2C_MASTER_TX_BUF_DISABLE   0                          
#define I2C_MASTER_RX_BUF_DISABLE   0
#define I2C_MASTER_FREQ_HZ          400000 
#define I2C_MASTER_TIMEOUT_MS       1000
static esp_err_t AXP313A_register_write_byte(uint8_t reg_addr, uint8_t data)
{
    int ret;
    uint8_t write_buf[2] = {reg_addr, data};

    ret = i2c_master_write_to_device(I2C_MASTER_NUM, AXP313A_ADDR, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_RATE_MS);

    return ret;
}

static esp_err_t i2c_master_init(void)
{
    int i2c_master_port = I2C_MASTER_NUM;

    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };

    i2c_param_config(i2c_master_port, &conf);

    return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
// ---------------------------------------------

static QueueHandle_t xQueueFrameO = NULL;

static void task_process_handler(void *arg)
{
    while (true)
    {
        camera_fb_t *frame = esp_camera_fb_get();
        if (frame)
            xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
    }
}

void register_camera(const pixformat_t pixel_fromat,
                     const framesize_t frame_size,
                     const uint8_t fb_count,
                     const QueueHandle_t frame_o)
{
    ESP_LOGI(TAG, "Camera module is %s", CAMERA_MODULE_NAME);

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    //开启摄像头前,先打开电源
    ESP_ERROR_CHECK(i2c_master_init());
    ESP_LOGI(TAG, "I2C initialized successfully");
    ESP_ERROR_CHECK(AXP313A_register_write_byte(0x00,0x04));
    vTaskDelay(100);
    ESP_ERROR_CHECK(AXP313A_register_write_byte(0x10,0x19));
    ESP_ERROR_CHECK(AXP313A_register_write_byte(0x16,0x07));        //1.2v
    ESP_ERROR_CHECK(AXP313A_register_write_byte(0x17,23));        //2.8v
    ESP_LOGI(TAG, "I2C unitialized successfully");
    vTaskDelay(1000);
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#if CONFIG_CAMERA_MODULE_ESP_EYE || CONFIG_CAMERA_MODULE_ESP32_CAM_BOARD
    /* IO13, IO14 is designed for JTAG by default,
     * to use it as generalized input,
     * firstly declair it as pullup input */
    gpio_config_t conf;
    conf.mode = GPIO_MODE_INPUT;
    conf.pull_up_en = GPIO_PULLUP_ENABLE;
    conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    conf.intr_type = GPIO_INTR_DISABLE;
    conf.pin_bit_mask = 1LL << 13;
    gpio_config(&conf);
    conf.pin_bit_mask = 1LL << 14;
    gpio_config(&conf);
#endif

    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = CAMERA_PIN_D0;
    config.pin_d1 = CAMERA_PIN_D1;
    config.pin_d2 = CAMERA_PIN_D2;
    config.pin_d3 = CAMERA_PIN_D3;
    config.pin_d4 = CAMERA_PIN_D4;
    config.pin_d5 = CAMERA_PIN_D5;
    config.pin_d6 = CAMERA_PIN_D6;
    config.pin_d7 = CAMERA_PIN_D7;
    config.pin_xclk = CAMERA_PIN_XCLK;
    config.pin_pclk = CAMERA_PIN_PCLK;
    config.pin_vsync = CAMERA_PIN_VSYNC;
    config.pin_href = CAMERA_PIN_HREF;
    config.pin_sscb_sda = CAMERA_PIN_SIOD;
    config.pin_sscb_scl = CAMERA_PIN_SIOC;
    config.pin_pwdn = CAMERA_PIN_PWDN;
    config.pin_reset = CAMERA_PIN_RESET;
    config.xclk_freq_hz = XCLK_FREQ_HZ;
    config.pixel_format = pixel_fromat;
    config.frame_size = frame_size;
    config.jpeg_quality = 10;
    config.fb_count = fb_count;
    config.fb_location = CAMERA_FB_IN_PSRAM;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;

    // camera init
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
        return;
    }

    sensor_t *s = esp_camera_sensor_get();
    s->set_vflip(s, 0); // flip it back
    // initial sensors are flipped vertically and colors are a bit saturated
    if (s->id.PID == OV3660_PID)
    {
        s->set_brightness(s, 1);  // up the blightness just a bit
        s->set_saturation(s, -2); // lower the saturation
    }

    xQueueFrameO = frame_o;
    xTaskCreatePinnedToCore(task_process_handler, TAG, 2 * 1024, NULL, 5, NULL, 1);
}

然后驱动TFT屏幕,例程中屏幕使用的是240x240的屏幕,我使用的是172x32的屏幕,做好屏幕适配即可。阅读例程的主程序,主程序看上去特别简单,使用了两个消息队列:摄像头不停地获取数据,丢到消息队列里。然后有个猫脸识别的线程,不停地从消息队列中读取图片,然后进行识别。识别出猫脸后,在图片中框出猫脸的位置,然后放到另外一个队列中。最后有个屏幕显示的线程,从队列中获取图片展示出来。
我这边就直接修改猫脸识别的线程,整个流程如图(使用Scheme-it网页绘制)

FoJgLSFx8KXL8i5pdKaTq65zVVAw

代码如下,修改了components/modules/ai/who_cat_face_detection.cpp文件:

// #include "freertos/semphr.h"
// #include "freertos/task.h"
#include "driver/uart.h"
#include "string.h"
#include "driver/gpio.h"

#include "who_cat_face_detection.hpp"

#include "esp_log.h"
#include "esp_camera.h"

#include "dl_image.hpp"
#include "cat_face_detect_mn03.hpp"

#include "who_ai_utils.hpp"

static const char *TAG = "cat_face_detection";

static QueueHandle_t xQueueFrameI = NULL;
static QueueHandle_t xQueueEvent = NULL;
static QueueHandle_t xQueueFrameO = NULL;
static QueueHandle_t xQueueResult = NULL;

static bool gEvent = true;
static bool gReturnFB = true;

static QueueHandle_t xQueueAIFrame = NULL;
static QueueHandle_t xQueueLCDFrame = NULL;
// ---------------------------------------------------------------------
SemaphoreHandle_t semphrHandle; // 定义一个信号量
bool catfeed = false;
#define TXD_PIN (GPIO_NUM_43)
#define RXD_PIN (GPIO_NUM_44)
#define UART_NUM UART_NUM_0
static const int CMD_BUF_SIZE = 10; // 舵机每命令字10字节,多个舵机 就用10的倍数
#define SPIFFS_PATH "/spiffs"       // 机械臂命令文件

// 串口初始化
void uart_init(void)
{
    const uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_APB,
    };
    // We won't use a buffer for sending data.
    uart_driver_install(UART_NUM, 256, 0, 0, NULL, 0);
    uart_param_config(UART_NUM, &uart_config);
    uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

// 向串口发送机械臂移动命令,命令字由 spiffs文件获取
void sendCmdData(void)
{
    char buf[CMD_BUF_SIZE];
    uint8_t sec = 1;
    FILE *fp = fopen(SPIFFS_PATH "/action.hts", "rb");
    if (fp == NULL)
    {
        ESP_LOGE(TAG, "Fail to open file: %s", SPIFFS_PATH "/action.txt");
        return;
    }
    // 读取文件
    while (!feof(fp))
    {
        memset(buf, 0, sizeof(buf));
        if (fread(buf, sizeof(char), sizeof(buf), fp) >= CMD_BUF_SIZE)
        {
            // for (int i = 0; i < CMD_BUF_SIZE; i++)
            // {
            //     printf("%02x , ", buf[i]);
            // }
            // printf("\r\n");
            sec++;
            uart_write_bytes(UART_NUM, buf, CMD_BUF_SIZE); // 写入串口命令字
            vTaskDelay(pdMS_TO_TICKS(50));
            if (sec % 3 == 0)
                vTaskDelay(pdMS_TO_TICKS(3000));
        }
    }
    fclose(fp);
}
// 喂猫的任务
void catFeedTask(void *pvParam)
{
    
    uart_init();
    while (1)
    {
        xSemaphoreTake(semphrHandle, portMAX_DELAY);
        if (catfeed )
        {
            printf("Feed cat process is running! \n");
            catfeed = false;
            sendCmdData(); // 操纵舵机工作
            vTaskDelay(pdMS_TO_TICKS(1000));
        }
        xSemaphoreGive(semphrHandle);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

static void task_process_handler(void *arg)
{
    time_t curltime,lasttime= time(NULL);
    camera_fb_t *frame = NULL;
    CatFaceDetectMN03 detector(0.4F, 0.3F, 10, 0.3F);
    while (true)
    {
        if (gEvent)
        {
            bool is_detected = false;
            if (xQueueReceive(xQueueFrameI, &frame, portMAX_DELAY))
            {
                std::list<dl::detect::result_t> &detect_results = detector.infer((uint16_t *)frame->buf, {(int)frame->height, (int)frame->width, 3});
                if (detect_results.size() > 0)
                {
                    draw_detection_result((uint16_t *)frame->buf, frame->height, frame->width, detect_results);
                    print_detection_result(detect_results);
                    is_detected = true;

                    curltime = time(NULL);
                    xSemaphoreTake(semphrHandle, portMAX_DELAY); // 探测到猫咪,允许喂猫
                    if(curltime-lasttime>30){
                        lasttime= time(NULL);
                        catfeed = true;
                    }
                    xSemaphoreGive(semphrHandle);
                }
            }

            if (xQueueFrameO)
            {
                xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
            }
            else if (gReturnFB)
            {
                esp_camera_fb_return(frame);
            }
            else
            {
                free(frame);
            }

            if (xQueueResult)
            {
                xQueueSend(xQueueResult, &is_detected, portMAX_DELAY);
            }
        }
    }
}

static void task_event_handler(void *arg)
{
    while (true)
    {
        xQueueReceive(xQueueEvent, &(gEvent), portMAX_DELAY);
    }
}

void register_cat_face_detection(const QueueHandle_t frame_i,
                                 const QueueHandle_t event,
                                 const QueueHandle_t result,
                                 const QueueHandle_t frame_o,
                                 const bool camera_fb_return)
{
    xQueueFrameI = frame_i;
    xQueueFrameO = frame_o;
    xQueueEvent = event;
    xQueueResult = result;
    gReturnFB = camera_fb_return;

    semphrHandle = xSemaphoreCreateBinary();
    xSemaphoreGive(semphrHandle); // 释放信号量

    xTaskCreate(catFeedTask, "catFeedTask", 1024 * 2, NULL, 1, NULL);
    xTaskCreatePinnedToCore(task_process_handler, "cat_face_process", 3 * 1024, NULL, 5, NULL, 1);
    if (xQueueEvent)
        xTaskCreatePinnedToCore(task_event_handler, "cat_face_event", 1 * 1024, NULL, 5, NULL, 1);
}

这个文件做了这几个地方的修改:
1:添加了串口功能。我使用的舵机是串口舵机,即需要使用串口来控制的舵机,所以这里启动了串口0,波特率115200,使用GPIO44作为串口输出。在系统启动后就初始化好串口。
2:添加了一个布尔值“catfeed”,用来在猫脸识别线程和舵机驱动线程两个线程间通讯,决定是否驱动舵机动作。使用二进制互斥量保障两个线程修改该变量的安全。
3:使用spiffs文件系统,用来保存舵机的命令字。舵机不同的动作有对应的命令字,使用文件系统就可以解耦舵机动作驱动。只需要预先制作好舵机的动作,保存成文件,需要是读取并发送到串口即可。
4:设置了个时钟限制,如果上次识别和这次识别间隔没有超过30秒,就不动作;只有超过了30秒钟,才会驱动舵机动作。
Fp08a8EiWmhebrkkO13S55vgAsqN
Fsas5mUDfUr1nHXgoUx8QXvFga0V

五、总结感悟

经历了重重波折,总算是把项目完成了。ESP32S3功能是真的强大,esp-idf也是真的复杂,整个项目基于已有的例程,却也遇到了重重困难,总算是逐一解决了。AI做猫脸识别速度挺快,识别率也挺不错的,不过当放入狗脸时,还是有蛮大几率误识别的,不得不说是个蛮大的缺憾。感谢电子森林举办的这次活动,让我完完整整地体验了一把在ESP32S3上AI开发之路。

代码以及舵机说明文件:
链接:https://pan.baidu.com/s/1xgYQeKIWc2gN14NMt6mlPQ 
提取码:8888

附件下载
PCB.zip
扩展板
团队介绍
单片机业余爱好者,瞎捣鼓小能手。
团队成员
aramy
单片机业余爱好者,瞎捣鼓小能手。
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号