【问题标题】:esp32_cam read and process imageesp32_cam 读取和处理图像
【发布时间】:2020-10-08 14:18:38
【问题描述】:

我正在尝试在 esp32_cam 上使用 tensorflow-lite 对图像进行分类。 我定义了以下我需要解决的子任务:

  1. 拍照
  2. 将照片尺寸减小到(例如)28x28 像素灰度
  3. 使用经过训练的模型运行推理

现在我被困在第 1 点和第 2 点之间,无法解决。 到目前为止我做了什么: 我使用esp_camera_fb_get() 将图像保存到缓冲区中。之后,我将缓冲区中的值放入 2D 数组中。 但是,当我打印出其中一些值时,它们从未变为 0 或 255,即使我覆盖了整个镜头或将明亮的光源放在镜头附近。

我有四个问题:

  1. 如何正确记录图像?
  2. 如何将其转换为二维数组?
  3. 如何将尺寸从(例如)160x120 缩小到 28x28?
  4. 我怎样才能正确地Serial.print() 每个像素值复制这些值并将它们绘制在我的计算机上(例如使用 python matplotlib)

#define CAMERA_MODEL_AI_THINKER

 #include <esp_camera.h>
 #include "camera_pins.h"

 #define FRAME_SIZE FRAMESIZE_QQVGA
 #define WIDTH 160
 #define HEIGHT 120    

 uint16_t img_array [HEIGHT][WIDTH] = { 0 };


 bool setup_camera(framesize_t);
 void frame_to_array(camera_fb_t * frame);
 void print_image_shape(camera_fb_t * frame);
 bool capture_image();

 void setup() {
     Serial.begin(115200);
     Serial.println(setup_camera(FRAME_SIZE) ? "OK" : "ERR INIT");
 }

 void loop() {
     if (!capture_image()) {
         Serial.println("Failed capture");
         delay(2000);

         return;
     }

     //print_features();
     delay(3000);
 }



 bool setup_camera(framesize_t frameSize) {
     camera_config_t config;

     config.ledc_channel = LEDC_CHANNEL_0;
     config.ledc_timer = LEDC_TIMER_0;
     config.pin_d0 = Y2_GPIO_NUM;
     config.pin_d1 = Y3_GPIO_NUM;
     config.pin_d2 = Y4_GPIO_NUM;
     config.pin_d3 = Y5_GPIO_NUM;
     config.pin_d4 = Y6_GPIO_NUM;
     config.pin_d5 = Y7_GPIO_NUM;
     config.pin_d6 = Y8_GPIO_NUM;
     config.pin_d7 = Y9_GPIO_NUM;
     config.pin_xclk = XCLK_GPIO_NUM;
     config.pin_pclk = PCLK_GPIO_NUM;
     config.pin_vsync = VSYNC_GPIO_NUM;
     config.pin_href = HREF_GPIO_NUM;
     config.pin_sscb_sda = SIOD_GPIO_NUM;
     config.pin_sscb_scl = SIOC_GPIO_NUM;
     config.pin_pwdn = PWDN_GPIO_NUM;
     config.pin_reset = RESET_GPIO_NUM;
     config.xclk_freq_hz = 20000000;
     config.pixel_format = PIXFORMAT_GRAYSCALE;
     config.frame_size = frameSize;
     config.jpeg_quality = 12;
     config.fb_count = 1;

     bool ok = esp_camera_init(&config) == ESP_OK;

     sensor_t *sensor = esp_camera_sensor_get();
     sensor->set_framesize(sensor, frameSize);

     return ok;
 }



 bool capture_image() {

     camera_fb_t * frame = NULL;
     frame = esp_camera_fb_get();

     print_image_shape(frame);

     frame_to_array(frame);

     esp_camera_fb_return(frame);

     if (!frame)
         return false;

     return true;
 }


 void print_image_shape(camera_fb_t * frame){

     // print shape of image and total length (=heigth*width)
     Serial.print("Width: ");
     Serial.print(frame->width);
     Serial.print("\tHeigth: ");
     Serial.print(frame->height);
     Serial.print("\tLength: ");
     Serial.println(frame->len);
 }

 void frame_to_array(camera_fb_t * frame){

     int len = frame->len;
     char imgBuffer[frame->len];
     int counter = 0;

     uint16_t img_array [HEIGHT][WIDTH] = { 0 };

     int h_counter = 0;
     int w_counter = 0;

     // write values from buffer into 2D Array
     for (int h=0; h < HEIGHT; h++){
         //Serial.println(h);
         for (int w=0; w < WIDTH; w++){
             //Serial.println(w);
             int position = h*(len/HEIGHT)+w;

             //Serial.println(position);
             img_array[h][w] = {frame->buf[position]};

             //Serial.print(img_array[h][w]);
             //Serial.print(",");
             //delay(2);
         }
     }


     //Serial.println("Current frame:");

     Serial.println("=====================");

 }

camera_pin.h:

#if defined(CAMERA_MODEL_WROVER_KIT)
#define PWDN_GPIO_NUM    -1
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    21
#define SIOD_GPIO_NUM    26
#define SIOC_GPIO_NUM    27

#define Y9_GPIO_NUM      35
#define Y8_GPIO_NUM      34
#define Y7_GPIO_NUM      39
#define Y6_GPIO_NUM      36
#define Y5_GPIO_NUM      19
#define Y4_GPIO_NUM      18
#define Y3_GPIO_NUM       5
#define Y2_GPIO_NUM       4
#define VSYNC_GPIO_NUM   25
#define HREF_GPIO_NUM    23
#define PCLK_GPIO_NUM    22

#elif defined(CAMERA_MODEL_ESP_EYE)
#define PWDN_GPIO_NUM    -1
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    4
#define SIOD_GPIO_NUM    18
#define SIOC_GPIO_NUM    23

#define Y9_GPIO_NUM      36
#define Y8_GPIO_NUM      37
#define Y7_GPIO_NUM      38
#define Y6_GPIO_NUM      39
#define Y5_GPIO_NUM      35
#define Y4_GPIO_NUM      14
#define Y3_GPIO_NUM      13
#define Y2_GPIO_NUM      34
#define VSYNC_GPIO_NUM   5
#define HREF_GPIO_NUM    27
#define PCLK_GPIO_NUM    25

#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     25
#define SIOC_GPIO_NUM     23

#define Y9_GPIO_NUM       19
#define Y8_GPIO_NUM       36
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       39
#define Y5_GPIO_NUM        5
#define Y4_GPIO_NUM       34
#define Y3_GPIO_NUM       35
#define Y2_GPIO_NUM       32
#define VSYNC_GPIO_NUM    22
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_M5STACK_WIDE)
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     22
#define SIOC_GPIO_NUM     23

#define Y9_GPIO_NUM       19
#define Y8_GPIO_NUM       36
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       39
#define Y5_GPIO_NUM        5
#define Y4_GPIO_NUM       34
#define Y3_GPIO_NUM       35
#define Y2_GPIO_NUM       32
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

#else
#error "Camera model not selected"
#endif

【问题讨论】:

    标签: c++ arduino tensorflow-lite esp32


    【解决方案1】:

    我没有使用过 ESP32 相机,所以我不能谈论这个,但我在 STM32 上做过一个类似的项目,所以我只能回答:

    1。如何正确记录图像?

    我在微控制器上设置摄像头时也遇到了麻烦,所以我的想法和你一样,通过串行将图像返回到 PC。请参考第4点。

    2。如何将其转换为二维数组?

    我怀疑您想这样做是为了将其复制到 tflite 微型模型输入缓冲区。如果是这样的话,你就不需要了!您可以将展平的一维图像数组写入模型输入缓冲区,因为这是 tflite micro 实际期望的:

    uint8_t img_array[HEIGHT * WIDTH] = { 0 }; // grayscale goes from 0 to 255. fits in 8bits
    TfLiteTensor* model_input = nullptr;
    ...
    void setup(){
        ... // Create your tflite interpreter and rest of your code
        model_input = interpreter->input(0); // get model input pointer
    }
    void loop() {
        ...
        // tflite model has input shape [batch_size, height, width, channels]
        // which in turn is [1, HEIGHT, WIDTH, 1] one channel because I think you are
        // using grayscale images, otherwise 3(RGB)
        // but tflite micro expects flattened 1D array so you can just do this
        for (uint32_t i = 0; i < HEIGHT*WIDTH; i++){
            // Assuming your model input expects signed 8bit integers
            model_input->data.int8[i] = (int8_t) (img_array[i] - 128);
        }
    }
    

    编辑:最后一行将model_input 指针指向模型输入结构并访问其data 成员(如果您不熟悉C 中的结构指针,请参阅this)。然后,由于我假设您的模型输入数据类型是 8 位有符号整数,它使用 int8 访问数据 union。例如,如果您的模型输入数据类型是 32 位浮点数,您可以使用 model_input-&gt;data.f[i]Here 是所有可用访问类型的源代码。正确寻址模型输入缓冲区后,我们分配相应的img_array 像素数据。由于像素数据的范围是 [0, 255],我们需要将其转换为有效的有符号 8 位整数类型和范围,所以必须减去 128,得到 [-128, 127] 范围。

    希望你能明白。如果您使用其他格式,如 RGB565,请告诉我,我会给您一个不同的 sn-p。

    编辑:如果您正在捕获 RGB 图像,最常用的格式是 RGB565,这意味着每 16 位有一个像素数据(红色 5 个,绿色 6 个,蓝色 5 个)。这是一个 sn-p,它将以该格式捕获的图像转换为 RGB888(这是您的模型可能期望的)并将其复制到模型输入缓冲区:

    // NOTICE FRAME BUFFER IS NOW uint16_t to store each pixel
    uint16_t img_array[HEIGHT * WIDTH] = { 0 }; 
    TfLiteTensor* model_input = nullptr;
    ...
    void setup(){
        ... // Create your tflite interpreter and rest of your code
        model_input = interpreter->input(0); // get model input pointer
    }
    void loop() {
        ...
        // Fill input buffer
        uint32_t input_ix = 0; // index for the model input
    
        // tflite model has input shape [batch_size, height, width, channels]
        // which in turn is [1, HEIGHT, WIDTH, 3] three channels because RGB
        // but tflite micro expects flattened 1D array so you can just do this
        for (uint32_t pix = 0; i < HEIGHT*WIDTH; pix++){
           // Convert from RGB55 to RGB888 and int8 range
           uint16_t color = img_array[pix];
           int16_t r = ((color & 0xF800) >> 11)*255/0x1F - 128;
           int16_t g = ((color & 0x07E0) >> 5)*255/0x3F - 128;
           int16_t b = ((color & 0x001F) >> 0)*255/0x1F - 128;
    
           model_input->data.int8[input_ix] =   (int8_t) r;
           model_input->data.int8[input_ix+1] = (int8_t) g;
           model_input->data.int8[input_ix+2] = (int8_t) b;
    
           input_ix += 3;
        }
    }
    

    Here 是 C 语言中从 RGB888 到 RGB565 的分步指南,我只是反其道而行之。您可能已经注意到屏蔽颜色通道位后的乘法。以红色为例:一旦你屏蔽了(color &amp; 0xF800) &gt;&gt; 11) 位,红色值将从 [0, (2^5)-1] 开始,但我们想要一个 [0, 255] 范围,所以我们除以那个数字( ( 2^5)-1 = 31 = 0x1F ) 并乘以 255,得到我们想要的范围。然后我们可以减去 128 得到一个 [-128, 127] 有符号的 8 位范围。之前完成乘法的事实是为了保持精度。蓝色通道相同,绿色通道除以 (2^6)-1=63=0x3F,因为它有 6 位。

    3。如何将尺寸从(例如)160x120 缩小到 28x28?

    您可以在 C 中实现一个算法,但我采用了简单的方法:我在我已经训练好的模型中添加了一个预处理 lambda 层,它就是这样做的:

    IMG_SIZE = (28, 28)
    
    def lm_uc_preprocess(inputs):
        # 'nearest' is the ONLY method supported by tflite micro as of October 2020 as you can see in
        # https://github.com/tensorflow/tensorflow/blob/a1e5d73663152b0d7f0d9661e5d602b442acddba/tensorflow/lite/micro/all_ops_resolver.cc#L70
        res_imgs = tf.image.resize(inputs, IMG_SIZE, method='nearest') 
        # Normalize to the range [-1,1] # (OPTIONAL)
        norm_imgs = res_imgs*(1/127.5) -1 # multiply by reciprocal of 127.5 as DIV is not supported by tflite micro
        return norm_imgs
    

    编辑:大多数计算机视觉模型期望图像输入值范围为 [0, 1] 或 [-1, 1] 但像素值通常为 8 位,因此它们的范围为 [ 0, 255]。要将它们的值标准化到所需范围 [a, b],我们可以应用以下公式:

    在我们的例子中,min(x)=0,max(x)=255,a=-1,b=1。因此,每个归一化值是 x_normalized = x_value/127.5 -1。
    直观地你可以看到 255/127.5 -1 = 1,以及 0/255 -1 = -1。这就是 127.5 和 -1 值的来源。

    现在您可以定义完整的模型了:

    capture_height, capture_width, channels = (160, 120, 1)
    
    uc_final_model = keras.models.Sequential([
        keras.layers.InputLayer((capture_height, capture_width, channels), dtype=tf.float32),
        keras.layers.Lambda(lm_uc_preprocess), # (160, 120) to (28, 28)
        my_trained_model
    ])
    
    # You should quantize your model parameters and inputs to int8 when compressing to tflite after this
    

    这样,最终模型的输入形状就等于相机捕捉分辨率。这允许我复制图像数组,如第 2 点所示。

    4。如何正确 Serial.print() 每个像素值来复制值并将它们绘制在我的计算机上(例如使用 python matplotlib)

    我尝试了几件事,这对我有用:您可以尝试打印像 123, 32, 1, 78, 90,(即用逗号分隔)这样的值,这应该很容易做到。然后,如果您使用的是 Arduino,您可以使用this cool program 来记录串行数据。如果您不使用 arduino,Putty 具有日志记录功能。然后你可以这样做:

    with open("img_test.txt") as f:
        str_img_test = f.read()
    
    img_test = np.array(str_img_test.split(",")[:-1], dtype=np.uint8)
    img_test = img_test.reshape(160, 120)
    
    plt.figure()
    plt.imshow(img_test)
    plt.axis('off')
    plt.show()
    

    捕获图像并保存日志的过程有点麻烦,但它不应该太令人沮丧,因为这只是在您的图像被正确捕获的情况下进行调试。

    这是一个非常笼统的问题,所以如果我遗漏了什么或者您想在某些方面更深入,请告诉我。

    编辑

    我已公开(并开源)我的完整代码和文档on this repository,其中包含与您正在构建的应用非常相似的应用。此外,我还计划将计算机视觉示例移植到 ESP32。请注意存储库正在开发中,将持续一段时间,尽管此示例已经完成(待修订)。

    我认为很多对微控制器深度学习感兴趣的人会发现该存储库有趣且有用。

    【讨论】:

    • 太棒了!非常感谢你!这澄清了我的许多问题。我现在似乎或多或少地工作,但我会在接下来的日子里投入更多。第 3 节 lm_uc_preprocess(inputs) 中的 127.5 来自哪里?你能解释一下第 2 节中循环函数的最后一行吗?为 RGB 图像截取的代码也很棒!
    • 当然,您正在以 RGB565 模式拍摄图像,对吗?这就是为什么你有一个 uint16_t img_array 是吗?
    • 现在我使用灰度,但是一旦我运行了基本模型,我想扩展到 rgb。灰度应该有 uint8_t,而 rgb 提供 uint16_t 如果我做对了
    • 你是对的。我已经添加了您要求的所有内容。让我知道是否还有任何不清楚的地方。您可能想知道为什么不在预处理模型中添加 RGB565->RGB888。我试过了,结果发现不支持 dtype uint16 ,也不支持 tflite micro 中的按位运算。我计划尽快发布我的类似应用程序的完整代码和文档,以便您获得更多灵感。我将在本周答案的末尾添加一个链接。
    • 非常感谢您的出色回答!我很乐意看看您的代码,这肯定会帮助我开发和改进我的代码!
    猜你喜欢
    • 2015-07-26
    • 2012-09-10
    • 1970-01-01
    • 2016-03-22
    • 2019-08-28
    • 2014-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多