c++ - esp32_cam 读取和处理图像
问题描述
我正在尝试在 esp32_cam 上使用 tensorflow-lite 对图像进行分类。我定义了以下我需要解决的子任务:
- 拍照
- 将照片尺寸减小到(例如)28x28 像素灰度
- 使用经过训练的模型进行推理
现在我被困在第 1 点和第 2 点之间,无法解决。到目前为止我所做的:我使用esp_camera_fb_get()
. 之后,我将缓冲区中的值放入 2D 数组中。然而,当我打印出其中一些值时,它们从未变为 0 或 255,即使我覆盖了整个镜头或将明亮的光源放在它附近。
我有四个问题:
- 如何正确记录图像?
- 如何将其转换为二维数组?
- 如何将尺寸从(例如)160x120 缩小到 28x28?
- 如何正确
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
解决方案
我没有使用过 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 中的结构指针,请参阅此内容)。然后,由于我假设您的模型输入数据类型是 8 位有符号整数,因此它使用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;
}
}
这是 C 语言中 RGB888 到 RGB565 的分步指南,我只是反其道而行之。您可能已经注意到屏蔽颜色通道位后的乘法。以红色为例:一旦你屏蔽掉这些位(color & 0xF800) >> 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,您可以使用这个很酷的程序来记录串行数据。如果您不使用 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。请注意存储库正在开发中,并且会持续一段时间,尽管这个示例已经完成(待修订)。
我认为很多对微控制器深度学习感兴趣的人会发现存储库有趣且有用。
推荐阅读
- java - 访问 (JUnit) Localstack 日志
- kotlin - 将 println 用于类实例时,Kotlin 奇怪的输出
- c# - 如何在泛型类型之间进行转换
- c# - 为什么 nameof 不能与 CreationAtAction return 语句一起使用
- c# - 如何计算列表中的每个整数单独的整数
- git - Git pull - 设备上没有剩余空间错误,剩余可用空间
- python - Python 2.7:根据 2 个字典列表中的一个键值查找常用元素
- pyspark - 与 show 方法或计数一起使用时,jupyter 单元执行挂起并引发异常
- javascript - Vuex中数组对象的反应式设置器的最佳实践是什么?
- deep-learning - 可以在两个权重矩阵之间添加非线性函数吗?