esp32_cam читать и обрабатывать изображение

Я пытаюсь использовать tenorflow-lite на esp32_cam для классификации изображений. Я определил следующие подзадачи, которые мне нужно решить:

  1. Сделать фотографию
  2. Уменьшить размер фотографии до (например) 28x28 пикселей в оттенках серого
  3. выполнить логический вывод с помощью обученной модели

На данный момент я застрял между пунктами 1 и 2 и не могу решить эту проблему. Что я сделал до сих пор: я сохраняю изображение в буфер, используя esp_camera_fb_get(). После этого я помещаю значения из буфера в 2D-массив. Однако, когда я распечатываю некоторые из этих значений, они никогда не становятся ни 0, ни 255, даже если я закрывал всю линзу или помещал рядом с ней источник яркого света.

У меня четыре вопроса:

  1. Как правильно записать изображение?
  2. Как я могу преобразовать его в 2D-массив?
  3. Как я могу уменьшить размер (например) с 160 x 120 до 28 x 28?
  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

person nopact    schedule 08.10.2020    source источник


Ответы (1)


Я не работал с камерой ESP32, поэтому я не могу об этом говорить, но я сделал аналогичный проект на STM32, поэтому вот все, что я могу ответить:

1. Как правильно записать изображение?

У меня также были проблемы с настройкой камеры на моем микроконтроллере, поэтому я думал так же, как и вы, возвращая изображение на ПК через последовательный порт. См. Пункт 4.

2. Как преобразовать его в 2D-массив?

Я подозреваю, что вы хотите сделать это для копирования во входной буфер микромодели 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);
    }
}

EDIT: последняя строка принимает model_input указатель на входную структуру модели и обращается к ее члену data (см. this, если вы не знакомы с указателями на структуры в C). Затем, поскольку я предполагал, что тип входных данных вашей модели - это 8-битные целые числа со знаком, он обращается к данным union с int8. Если бы тип входных данных вашей модели был 32-битным с плавающей запятой, вы могли бы, например, использовать model_input->data.f[i]. Здесь исходный код с все доступные типы доступа. После правильной адресации входного буфера модели мы назначаем соответствующие img_array пиксельные данные. Поскольку данные пикселей находятся в диапазоне от [0, 255], нам необходимо преобразовать их в допустимый 8-битный целочисленный тип со знаком и диапазон, поэтому вы должны вычесть 128, что приведет к диапазону [-128, 127].

Надеюсь, вы поняли идею. Сообщите мне, если вы используете другие форматы, такие как RGB565, и я дам вам другой фрагмент.

РЕДАКТИРОВАТЬ: при захвате изображений RGB наиболее часто используется формат RGB565, что означает, что пиксельные данные передаются каждые 16 бит (5 для красного, 6 для зеленого, 5 для синего). Вот фрагмент, который преобразует изображение, захваченное в этом формате, в 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;
    }
}

Здесь приведено пошаговое руководство по переходу от RGB888 к RGB565 на языке C , Я сделал наоборот. Вы могли заметить умножение после маскирования битов цветового канала. Возьмем, к примеру, красный цвет: как только вы замаскируете биты (color & 0xF800) >> 11), значение красного будет от [0, (2 ^ 5) -1], но нам нужен диапазон [0, 255], поэтому мы делим на это число ((2 ^ 5) -1 = 31 = 0x1F) и умножаем на 255, получая желаемый диапазон. Затем мы можем вычесть 128, чтобы получить 8-битный диапазон со знаком [-128, 127]. Тот факт, что умножение выполняется раньше, должен сохранять точность. Синий канал такой же, а в зеленом мы делим на (2 ^ 6) -1 = 63 = 0x3F, потому что он имеет 6 бит.

3. Как уменьшить размер (например) с 160x120 до 28x28?

Вы могли бы реализовать алгоритм на C, но я пошел по простому пути: я добавил лямбда-слой предварительной обработки в свою уже обученную модель, которая сделала именно это:

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], мы можем применить следующую формулу:
 normalization_formula

В нашем случае 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, вы можете использовать эту классную программу, чтобы зарегистрируйте последовательные данные. Если вы не используете 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()

Процесс захвата изображения и сохранения журнала немного обременительный, но он не должен сильно расстраивать, поскольку это только для отладки, если ваше изображение было захвачено правильно.

Это очень общий вопрос, поэтому дайте мне знать, если я что-то пропустил или вам нужно больше подробностей по некоторым аспектам.

Редактировать

Я опубликовал (с открытым исходным кодом) свой полный код и документацию в этом репозитории который содержит приложение, очень похожее на то, что вы создаете. Кроме того, я также планирую перенести пример компьютерного зрения на ESP32. Обратите внимание, что репозиторий находится в стадии разработки и будет работать некоторое время, хотя этот пример уже завершен (ожидает пересмотра).

Я думаю, что многие люди, заинтересованные в глубоком обучении на микроконтроллерах, найдут репозиторий интересным и полезным.

person PHAN    schedule 11.10.2020
comment
Потрясающие! Большое спасибо! Это прояснило многие из моих вопросов. Кажется, сейчас я работаю более или менее, но в следующие дни я займусь этим больше. Откуда 127.5 в разделе 3. lm_uc_preprocess (входы)? Не могли бы вы объяснить последнюю строку функции цикла в разделе 2? Также было бы замечательно вырезать код для изображений RGB! - person nopact; 12.10.2020
comment
Конечно, вы снимаете изображения в режиме RGB565, верно? Вот почему у вас uint16_t img_array да? - person PHAN; 12.10.2020
comment
На данный момент я использую оттенки серого, но я хотел бы расширить его до RGB, как только у меня будет запущена базовая модель. Оттенки серого должны иметь uint8_t, тогда как rgb предоставляет uint16_t, если я правильно понял - person nopact; 12.10.2020
comment
Ты прав. Я добавил все, о чем вы просили. Сообщите мне, если по-прежнему что-то неясно. Вы можете спросить, почему бы не добавить RGB565- ›RGB888 в модель предварительной обработки. Я пробовал, и оказалось, что dtype uint16 не поддерживается, как и побитовые операции в tflite micro. Я планирую в ближайшее время выпустить свой полный код и документацию по своему аналогичному приложению, чтобы вы могли получить больше вдохновения. Я добавлю ссылку в конце ответа на этой неделе. - person PHAN; 12.10.2020
comment
Большое спасибо за отличный ответ !! Я был бы признателен, если бы взглянул на ваш код, который, несомненно, поможет мне развить и улучшить мой! - person nopact; 12.10.2020
comment
Без проблем! Я только что опубликовал код, проверьте правку в конце ответа. Я очень надеюсь, что вы найдете это полезным и вдохновляющим. Приложение, которое вас интересует, - это обнаружение людей и автомобилей в репозитории. - person PHAN; 14.10.2020
comment
Спасибо, что поделились кодом! Я попытался изменить размер изображения с помощью предоставленного вами кода. При запуске я получаю: «Register_RESIZE_NEAREST_NEIGHBOR» не является членом «tflite :: ops :: micro». Я использую библиотеку platformIO TensorFlowLite_ESP32@^0.9.0. Источник тензорного потока на github включает Register_RESIZE_NEAREST_NEIGHBOR, но я не могу заставить его работать. Есть идеи, как решить эту проблему или обходной путь, например, изменение размера в С ++? Я попытался найти для него код, но мои познания в C ++ весьма ограничены. - person nopact; 20.10.2020