摄像头OV7670#

ESP-WHO#

ESP-WHO 是乐鑫专为 AIoT 领域推出的软件开发框架。

由于单独驱动摄像头需要繁杂的图像处理以及驱动编写,故本实验使用乐鑫的ESP-WHO开发框架进行学习。

ESP-WHO 提供了例如人脸检测、人脸识别、猫脸检测和手势识别等示例。您可以基于这些示例,衍生出丰富的实际应用。ESP-WHO 的运行基于 ESP-IDF。ESP-DL 为 ESP-WHO 提供了丰富的深度学习相关接口,配合各种外设可以实现许多有趣的应用。

image-20231126174243207

由于ESP-WHO处于不断的开发与更新当中,对相关内容感兴趣或遇到问题的可以前往ESP-WHO的官方论坛进行浏览和发帖:https://www.esp32.com/viewforum.php?f=31

准备工作#

目前我们手头的ESP开发板中,除了实验箱上的,还有另外两款可以进行实验,并且能实现更多功能且无需配置,想深入学习的同学可以向老师提出实验室设备借用申请。

芯片

ESP32

ESP32-S3

开发板

ESP-EYE

ESP-S3-EYE

软件准备#

默认的实验室电脑上都已经安装好ESP-WHO,需要自行安装的同学可以参考下列教程。

注意!由于在编写教程时使用的idf是4.4版本,who也需要拉取对应版本分支,自行安装的同学需要注意自己安装的ESP-IDF和WHO的版本对应!!!

git clone --recursive https://github.com/espressif/esp-who.git

并且可以使用 git submodule update –recursive –init 拉取和更新 ESP-WHO 的所有子模块。

运行示例#

ESP-WHO 的所有示例都存放在 examples 中。该文件夹架构如下所示:

├── examples
│   ├── cat_face_detection          // 猫脸检测示例
│   │   ├── lcd                     // 结果显示方式为 LCD 屏
│   │   └── terminal                // 结果显示方式为终端
│   ├── code_recognition            // 一维码/二维码识别示例
│   ├── human_face_detection        // 人脸检测示例
│   │   ├── lcd
│   │   └── terminal
│   ├── human_face_recognition      // 人脸识别示例
│   │   ├── lcd
│   │   ├── terminal
│   │   └── README.md               // 示例的具体说明
│   └── motion_detection            // 移动侦测示例
│       ├── lcd 
│       ├── terminal
│       ├── web
│       └── README.rst         

对于硬件准备中所提到的开发板,所有示例都是开箱即用的,要运行示例仅需执行步骤 1:设定目标芯片和步骤 4:运行和监视。

步骤 1:设定目标芯片#

打开终端,进入一个示例(例如:examples/human_face_detection/lcd),运行以下命令设定目标芯片:

idf.py set-target [SoC]

将 [SoC] 替换成您的目标芯片,例如 esp32(实验箱上的也是这个)、esp32s3。

注意: 所有关于 esp32s3 的示例都是基于 ESP32-S3-EYE 开发的。所以默认的烧写和监听都设定为通过 USB。如果你正在使用 esp32s3 的其他开发板,请先确认是通过 USB 还是 UART 实现烧写和监听,

如果通过 USB,保持默认配置即可, 如果通过 UART,需要在 menuconfig 中修改烧写监听方法。

(可选)步骤 2:摄像头配置#

若您使用的不是硬件准备中提到的乐鑫开发板,则需自行配置摄像头管脚。在终端输入 idf.py menuconfig,依次点击 (Top) -> Component config -> ESP-WHO Configuration 可进入 ESP-WHO 的配置界面,如下图所示:

image-20231126175112069

选择 Camera Configuration 进入摄像头配置,根据您使用的开发板选择摄像头的管脚配置,如下图所示:

image-20231126175312170

如上图中没有您使用的开发板,请选择 Custom Camera Pinout,并正确配置对应管脚,如下图所示:

image-20231126175326345

(可选)步骤 3:Wi-Fi 配置#

若您选择的示例输出显示方式为网页,可选择 Wi-Fi Configuration 进入 Wi-Fi 配置,配置 Wi-Fi 密码等参数,如下图所示:

image-20231126175337850

步骤 4:运行和监视#

烧录程序,运行 IDF 监视器:

idf.py flash monitor

开发板的默认二进制文件 各开发板的默认二进制文件存放在文件夹 default_bin 中。您可使用烧写工具烧录二进制文件。

示例讲解#

取其中一个猫脸识别的实例,主要代码在app_main.cpp。

#include "who_camera.h"
#include "who_cat_face_detection.hpp"

static QueueHandle_t xQueueAIFrame = NULL;

extern "C" void app_main()
{
    xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));

    register_camera(PIXFORMAT_RGB565, FRAMESIZE_240X240, 2, xQueueAIFrame);
    register_cat_face_detection(xQueueAIFrame, NULL, NULL, NULL, true);
}

下面就针对上面的这段代码进行讲解:

第一步:#

示例烧录到开发板后,摄像头开始工作,调用register_camera()的函数,将摄像头的获取的图片 一帧一帧的传输到作为帧的输出流的xQueueAIFrame队列缓冲区。 这个register_camera()函数具体是怎么用的呢?

    /**
     * @brief Initialize camera
     * 
     * @param pixformat    One of
     *                     - PIXFORMAT_RGB565
     *                     - PIXFORMAT_YUV422
     *                     - PIXFORMAT_GRAYSC
     *                     - PIXFORMAT_JPEG
     *                     - PIXFORMAT_RGB888
     *                     - PIXFORMAT_RAW
     *                     - PIXFORMAT_RGB444
     *                     - PIXFORMAT_RGB555
     * @param frame_size   One of
     *                     - FRAMESIZE_96X96,    // 96x96
     *                     - FRAMESIZE_QQVGA,    // 160x120
     *                     - FRAMESIZE_QCIF,     // 176x144
     *                     - FRAMESIZE_HQVGA,    // 240x176
     *                     - FRAMESIZE_240X240,  // 240x240
     *                     - FRAMESIZE_QVGA,     // 320x240
     *                     - FRAMESIZE_CIF,      // 400x296
     *                     - FRAMESIZE_HVGA,     // 480x320
     *                     - FRAMESIZE_VGA,      // 640x480
     *                     - FRAMESIZE_SVGA,     // 800x600
     *                     - FRAMESIZE_XGA,      // 1024x768
     *                     - FRAMESIZE_HD,       // 1280x720
     *                     - FRAMESIZE_SXGA,     // 1280x1024
     *                     - FRAMESIZE_UXGA,     // 1600x1200
     *                     - FRAMESIZE_FHD,      // 1920x1080
     *                     - FRAMESIZE_P_HD,     //  720x1280
     *                     - FRAMESIZE_P_3MP,    //  864x1536
     *                     - FRAMESIZE_QXGA,     // 2048x1536
     *                     - FRAMESIZE_QHD,      // 2560x1440
     *                     - FRAMESIZE_WQXGA,    // 2560x1600
     *                     - FRAMESIZE_P_FHD,    // 1080x1920
     *                     - FRAMESIZE_QSXGA,    // 2560x1920
     * @param fb_count     Number of frame buffers to be allocated. If more than one, then each frame will be acquired (double speed)
     */
    void register_camera(const pixformat_t pixel_fromat,
                         const framesize_t frame_size,
                         const uint8_t fb_count,
                         const QueueHandle_t frame_o);

pixel_fromat是像素的设置,frame_size是每一帧的大小,fb_count是帧的缓冲区的数量 frame_o是帧的输出流。 xQueueAIFrame队列是调用了xQueueCreate()的函数,创建了一个队列的深度(大小)为2,队列的每一项的大小是指向camera_fb_t类型的指针类型的大小,创建完后会返回QueueHandle_t类型的队列的句柄。

xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));
1

第二步:#

此时xQueueAIFrame队列缓冲区里面放置着摄像头获取到的图片,调用register_cat_face_detection()的函数,将xQueueAIFrame队列缓冲区作为帧的输入流传入,register_cat_face_detection()函数中会通过AI模型计算,识别到猫脸,然后帧的输出流设置为NULL,至此猫脸检测的结果就显示到终端上了。

整个流程如下图所示: image-20231126180325405

接下来我们看register_cat_face_detection()函数里面具体是怎么实现操作的:

image-20231126180427800

在猫脸检测的函数中,调用xTaskCreatePinnedToCore将task_process_handler函数创建到freeRTOS上 在task_process_handler函数中做了一件什么事情呢?

static void task_process_handler(void *arg)
{
    camera_fb_t *frame = NULL;
    CatFaceDetectMN03 detector(0.4F, 0.3F, 10, 0.3F);
    while (true)
    {
        if (gEvent)
        {
            bool is_detected = false;
            //从xQueueFrameI中接收一项数据
            if (xQueueReceive(xQueueFrameI, &frame, portMAX_DELAY))
            {
                //AI模型计算
                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;
                }
            }
            if (xQueueFrameO)
            {
                //向xQueueFrameO发送一项数据
                xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
            }
            else if (gReturnFB)
            {
                //                 /**
                //  * @brief Return the frame buffer to be reused again.返回要再次重用的帧缓冲区。
                //  *
                //  * @param fb    Pointer to the frame buffer
                //  */
                // void esp_camera_fb_return(camera_fb_t * fb);
                esp_camera_fb_return(frame);
            }
            else
            {
                free(frame);
            }
            if (xQueueResult)
            {
                //向xQueueResult发送一项数据
                xQueueSend(xQueueResult, &is_detected, portMAX_DELAY);
            }
        }
    }
}

首先如果xQueueFrameI队列缓冲区中有数据,可以调用xQueueReceive()的函数去接收一项;如果xQueueFrameI队列缓冲区中没有数据,则不去接收。当拿到了这项数据后,经过AI模型的计算,得出每一帧的结果。这时,再去调用xQueueSend()函数向xQueueFrameO队列缓冲区中发送一项数据。 如果事件队列不为空,则调用xTaskCreatePinnedToCore()函数将task_event_handler创建到freeRTOS上运行。 image-20231126180522476

task_event_handler()中不断的从xQueueEvent中接收一项。

显示到LCD#

如果你已经理解了猫脸检测输出到终端的示例代码,那么LCD的也就变得简单了。

#include "who_camera.h"
#include "who_cat_face_detection.hpp"
#include "who_lcd.h"
#include "who_trace.h"

static QueueHandle_t xQueueAIFrame = NULL;
static QueueHandle_t xQueueLCDFrame = NULL;

extern "C" void app_main()
{
    xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));
    xQueueLCDFrame = xQueueCreate(2, sizeof(camera_fb_t *));

    register_camera(PIXFORMAT_RGB565, FRAMESIZE_240X240, 2, xQueueAIFrame);
    register_cat_face_detection(xQueueAIFrame, NULL, NULL, xQueueLCDFrame, false);
    register_lcd(xQueueLCDFrame, NULL, true);

}

运行示例代码,将摄像头对准猫脸,监视器输出如下:

image-20231126180641641

示例烧录到开发板后,摄像头开始工作,调用register_camera()的函数,将摄像头的获取的图片 一帧一帧的传输到作为帧的输出流的xQueueAIFrame队列缓冲区。xQueueAIFrame队列缓冲区里面放置着摄像头获取到的图片,调用register_cat_face_detection()的函数,将xQueueAIFrame队列缓冲区作为帧的输入流传入,register_cat_face_detection()函数中会通过AI模型计算,识别到猫脸。如何将经过模型计算的每一帧的数据输出到LCD屏幕上呢? 我们需要一个作为帧输出流的队列缓冲区去接收经过模型计算的每一帧的数据,在本示例中就是xQueueLCDFrame,再调用register_lcd()函数将xQueueLCDFrame()队列作为帧的输入流传入,到此就显示到LCD屏幕上了。

image-20231126180710539

register_lcd()中也是先去初始化LCD的驱动,进行一些LCD的配置,紧接着调用xTaskCreatePinnedToCore()函数去调用一个任务函数。

显示到web#

理解了LCD的部分,那么web的代码就是将输出到LCD的队列xQueueLCDFrame改成了xQueueHttpFrame队列,最后调用register_httpd()方法将xQueueHttpFrame队列做为帧的输入流传入即可。

#include "who_camera.h"
#include "who_cat_face_detection.hpp"
#include "app_wifi.h"
#include "app_httpd.hpp"
#include "app_mdns.h"

static QueueHandle_t xQueueAIFrame = NULL;
static QueueHandle_t xQueueHttpFrame = NULL;

extern "C" void app_main()
{
    app_wifi_main();
    app_mdns_main();

    xQueueAIFrame = xQueueCreate(2, sizeof(camera_fb_t *));
    xQueueHttpFrame = xQueueCreate(2, sizeof(camera_fb_t *));

    register_camera(PIXFORMAT_RGB565, FRAMESIZE_QVGA, 2, xQueueAIFrame);
    register_cat_face_detection(xQueueAIFrame, NULL, NULL, xQueueHttpFrame);
    register_httpd(xQueueHttpFrame, NULL, true);
}

register_httpd()当你在打开网页后,触碰网页上的一些按钮,就会自动的触发一些方法

image-20231126180827164

image-20231126180843951

image-20231126180858475

image-20231126181059535