作者:Md. Khairul Alam
来自:亚太大学
该项目主要用于通过物体检测和文本到语音的转换,实现向视障人士解释环境的功能,让我们一起了解一下这个项目的开发过程吧。
材料清单
序号 | 名称 | 数量 |
---|---|---|
1 | Seeed Studio XIAO ESP32S3 Sense | 1 |
2 | Rasberry Pi 1 Model B+ | 1 |
3 | Rechargeable Battery,Lithium lon | 1 |
应用软件
序号 | 名称 |
---|---|
1 | Edge Impulse Studio |
2 | Arduino IDE |
3 | Raspberry Pi Raspbin |
故事背景
全球约有 22 亿人看不见东西,其中 90% 来自低收入国家。因此,对于这些低收入国家的视障人士来说,方便使用且成本低廉的解决方案非常重要。
视障人士无法像正常人一样感知环境和导航,这导致他们的行动能力下降。在这个项目中,我将展示如何利用人工智能和计算机视觉来解决这个问题。通过实施这个项目,盲人现在可以减少对当前环境和人的依赖。
在这个项目中,我加入了物体检测和文本到语音的转换,以便向视障人士解释环境。盲人可以通过耳机听到转换后的语音。
使用 Edge Impulse Studio 开发物体检测模型
我使用 Edge Impulse Studio 来训练物体检测模型。Edge Impulse 是边缘设备机器学习的领先开发平台。
要启动一个项目,您需要在 Edge Impulse 输入您的账户凭据(或创建一个免费账户)。然后就可以创建新项目了。数据是任何机器学习项目的主要动力。
在 Edge Impulse 中,您可以上传以前的数据,也可以记录新数据。在我的项目中,我准备了一个数据集,其中包括家里的一些常见物品,如椅子、桌子、床和脸盆。数据集中包含的物品越多,模型就越有效。数据集的大小也很重要。我们能拍摄到的特定物体的图像越多,准确度就越高。
我在最初的项目中上传了 6 个物体的 281 张图片。以后我会上传更多对象的更多图像。数据可以从 Edge Impulse Studio 的 “数据采集 ”选项卡中上传和标注。您可以让工作室在训练和测试之间自动分割数据,也可以手动分割。
Impulse设计
上传和标记数据后,下一步就是设计脉冲。脉冲接收原始数据(在本例中为图像),提取特征(调整图片大小),然后使用学习模块对新数据进行分类
在这一阶段,您应确定如何进行预处理:
- 预处理包括将单个图像的大小从 320 x 240 调整为 96 x 96,并将其压扁(正方形,不进行裁剪)。
- 设计模型,在这种情况下,您需要添加 “对象检测”。
完整的 “Impulse ”系统将如下所示。
保存 Impulse 后,Studio 会自动转到下一部分 “生成特征”,在这里将对所有样本进行预处理,生成一个包含 96x96x3 图像或 12,216 个特征的数据集。
现在,将训练我们的模型。需要在设置选项中设置神经网络参数,然后点击训练按钮。训练过程需要的时间取决于设置和数据集的大小。
增加数据集的规模需要更多的时间来训练,从而提高准确率。训练周期和学习率等神经网络参数也会影响准确率。经过多次试验后,我得到了以下结果。我的数据集生成以下结果花费了大约 10 分钟。虽然结果不是很令人满意,但对于测试项目来说还是可以的。当然,在实际应用中,需要添加更多的样本图像,以提高准确率。
部署模型(使用 Arduino IDE)
为了对物体进行实时检测(或推理),需要将模型上传到XIAO ESP32S3 Sense。幸运的是,可以从 Edge Impulse 下载 Arduino 库中的模型,该库可以轻松集成或定制,用于开发 Arduino IDE 支持的边缘设备固件。
因此,让我们为我们的电路板下载 Arduino 库。为此,请选择 Arduino 库和量化 (int8) 模型,在部署选项卡上启用 EON 编译器,然后点击 [Build(构建)]。
打开 Arduino IDE,在 Sketch 下进入 Include Library(包含库)并添加 ZIP Library。选择从 Edge Impulse Studio 下载的文件,就这样!
在 Arduino IDE 的 “示例 ”选项卡下,您应该能在项目名称下找到草图代码 (esp32 > esp32\\_camera)。
项目链接:
https://studio.edgeimpulse.com/studio/503872/impulse/1/deployment
为提供正确的摄像头连接,应将第 39 至 55 行(定义了 XIAO ESP32S3 Sense 的摄像头型号和引脚)改为与我们的型号相关的数据。复制并粘贴以下几行,替换第 39-55 行:
#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
更新摄像头配置后,尝试编译代码,但无法编译。得到了以下错误信息。
我试着用不同的方法来解决这个问题,最后,我把 esp32 板管理器的版本降到了 2.0.17,终于编译成功了。然后我把代码上传到了电路板上。
配置
树莓派
XIAO ESP32S3 Sense 可检测周围环境中的物体,并返回物体的名称和位置。Raspberry Pi 用于通过 UART 接收物体名称和位置,并将文本转换为语音。
例如:冰箱在左边,床在前面
在这里使用的是 Raspberry Pi 1 B,性能令人满意。在 Raspberry Pi 上安装操作系统后,配置音频控制系统,并将音量设置为 100%。
sudo raspi-config
然后,在 Pi 上安装了免费的软件包 Festival。Festival 由英国语音技术研究中心编写,为构建语音合成系统提供了一个框架。它通过多种应用程序接口提供完整的文本到语音功能:从 shell 层、通过
命令解释器
、作为 C++ 库、从 Java 和 Emacs 编辑器界面。
使用以下命令安装 festival:
sudo apt-get install -y libasound2-plugins festival
安装festival后,连接一个音频放大器,并使用以下音频进行了测试,结果令人惊叹
echo "Hello World!" | festival --tts
然后,在 Raspberry Pi 上安装了 python 串行模块。
通过一根 USB-C 电缆将 XIAO ESP32S3 Sense 与 Raspberry Pi 连接起来。
最后,通过 Raspberry Pi 的音频输出端口连接了一个耳机。
为树莓派编写代码
在编写代码之前,需要知道 XIAO Sense 板的串口号。
连接好 XIAO Sense 板并将其插入 Raspberry Pi 后,可以在终端运行以下命令。
dmesg | grep tty
结果是:
现在知道串行端口号了。是时候编写代码了。为 Raspberry Pi 编写了以下代码,将接收到的文本转换为语音
#!/usr/bin/env python
# 这行指定脚本使用的解释器(Python)
import time
import serial
import os
# 设置串口连接的参数,用于与设备进行通信
ser = serial.Serial(
port='/dev/ttyACM0', # 指定设备连接的端口。
baudrate = 115200, # 设置串口通信的波特率
parity=serial.PARITY_NONE, # 不使用奇偶校验位
stopbits=serial.STOPBITS_ONE, # 使用一个停止位
bytesize=serial.EIGHTBITS, # 每个字节有8位数据位
timeout=1 # 设置读取串口时的超时时间为1秒
)
# 进入一个无限循环,不断读取串口数据
while True:
receive_msg=ser.readline() # 从串口读取一行数据
print(receive_msg) # 打印接收到的数据
# 如果接收到的数据中包含“basin”的字样,则执行以下操作:
if b'basin' in receive_msg.lower():
os.system('echo "basin in front" | festival --tts')# 使用festival语音合成引擎朗读提示信息。
# 如果接收到的数据中包含“bed”的字样,则执行以下操作:
if b'bed' in receive_msg.lower():
os.system('echo "bed in front" | festival --tts')
# 如果接收到的数据中包含“chair”的字样,则执行以下操作:
if b'chair' in receive_msg.lower():
os.system('echo "chair in front" | festival --tts')
# 如果接收到的数据中包含“table”的字样,则执行以下操作:
if b'dining table' in receive_msg.lower():
os.system('echo "dining tabl in front" | festival --tts')
# 如果接收到的数据中包含“oven”的字样,则执行以下操作:
if b'oven' in receive_msg.lower():
os.system('echo "oven in front" | festival --tts')
# 如果接收到的数据中包含“refrigerator”的字样,则执行以下操作:
if b'refrigerator' in receive_msg.lower():
os.system('echo "refrigerator in front" | festival --tts')
最终设置
XIAO ESP32S3 Sense 板将从 Raspberry Pi 获取电源。可以使用移动电源为 Pi 供电。
Arduino Code
/* 边缘脉冲 Arduino 示例
* Copyright (c) 2022 EdgeImpulse Inc.
*
* 特此允许任何获得本软件副本及相关文档文件(以下简称 “软件”)的人免费处理本软件
* 本软件及相关文档文件(以下简称 “软件”)的副本的任何人,均可免费使用本软件
* 不受限制地处理本软件,包括但不限于以下权利
* 使用、复制、修改、合并、出版、分发、再许可和/或销售本软件的权利
* 使用、复制、修改、合并、出版、分发、再许可和/或出售本软件的副本,并允许获得本软件的人
* 允许获得本软件的人这样做,但须符合以下条件: *
上述版权声明和本许可声明应包含在
* 上述版权声明和本许可声明应包含在本软件的所有副本或实质部分中。
*
* 本软件按 “原样 ”提供,不作任何明示或暗示的保证。
* 软件按 “原样 ”提供,不附带任何明示或暗示的保证,包括但不限于适销性保证、
* 对特定用途的适用性和非侵权性的保证。在任何情况下
在任何情况下,作者或版权持有者均不对任何索赔、损害或其他*责任负责。
* 作者或版权持有者在任何情况下均不对因以下原因引起的任何索赔、损害或其他责任承担责任,无论是合同诉讼、侵权诉讼还是其他诉讼
* 作者或版权所有者对任何索赔、损害赔偿或其他责任概不负责,无论是合同诉讼、侵权诉讼还是其他诉讼。
* 软件。
*/
// 这些程序设计使用 2.0.4 ESP32 Arduino 内核进行测试
// https://github.com/espressif/arduino-esp32/releases/tag/2.0.4
/* Includes ---------------------------------------------------------------- */
#include <eye_for_blind_inferencing.h>
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "esp_camera.h"
// 选择摄像头模型 - 更多摄像头模型可以在 camera_pins.h 文件中找到//https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Camera/CameraWebServer/camera_pins.h
#define CAMERA_MODEL_ESP_EYE // 带有 PSRAM 的 ESP-EYE 模型
//#define CAMERA_MODEL_AI_THINKER // 带有 PSRAM 的 AI-THINKER 模型
//根据摄像头模型定义 GPIO 引脚
#if defined(CAMERA_MODEL_ESP_EYE)
#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_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
/* 常量定义 -------------------------------------------------------- */
#define EI_CAMERA_RAW_FRAME_BUFFER_COLS 320
#define EI_CAMERA_RAW_FRAME_BUFFER_ROWS 240
#define EI_CAMERA_FRAME_BYTE_SIZE 3
/* 私有变量 ------------------------------------------------------- */
static bool debug_nn = false; // 设置为 true 时,会输出原始信号的特征等调试信息
static bool is_initialised = false;
uint8_t *snapshot_buf; //指向捕获图像的缓冲区
//摄像头配置
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
//XCLK 20MHz 或 10MHz 对 OV2640 进行双 FPS 处理(实验性)
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG, //可选格式:YUV422,GRAYSCALE,RGB565,JPEG
.frame_size = FRAMESIZE_QVGA, //设置帧大小为 QVGA,建议在非 JPEG 模式下不使用超过 QVGA 的大小
.jpeg_quality = 12, //JPEG 质量,0-63,数值越低质量越高
.fb_count = 1, //帧缓冲区数目,如果大于 1,则 I2S 将以连续模式运行。只在 JPEG 模式下使用
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,};// 抓取模式:在缓冲区为空时抓取数据
/* 函数声明 ------------------------------------------------------- */
bool ei_camera_init(void);
void ei_camera_deinit(void);
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) ;
/**
* @brief Arduino setup function
*/
void setup()
{
Serial.begin(115200); // 初始化串口通信
while (!Serial); // 等待串口连接
Serial.println("Edge Impulse Inferencing Demo");
// 初始化摄像头
if (ei_camera_init() == false) {
ei_printf("Failed to initialize Camera!\r\n");
}
else {
ei_printf("Camera initialized\r\n");
}
ei_printf("\nStarting continious inference in 2 seconds...\n");
ei_sleep(2000);}
/**
* @brief 获取数据并运行推理
*
* @param[in] debug 如果为 true,则获取调试信息
*/
void loop()
{
// 使用 ei_sleep 等待信号...
if (ei_sleep(5) != EI_IMPULSE_OK) {
return;
}
// 分配图像捕获缓冲区
snapshot_buf=(uint8_t*)malloc(EI_CAMERA_RAW_FRAME_BUFFER_COLS*EI_CAMERA_RAW_FRAME_BUFFER_ROWS * EI_CAMERA_FRAME_BYTE_SIZE);
// 检查分配是否成功
if(snapshot_buf == nullptr) {
ei_printf("ERR: Failed to allocate snapshot buffer!\n");
return;
}
ei::signal_t signal;
signal.total_length = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT;
signal.get_data = &ei_camera_get_data;
// 捕获图像
if (ei_camera_capture((size_t)EI_CLASSIFIER_INPUT_WIDTH, (size_t)EI_CLASSIFIER_INPUT_HEIGHT, snapshot_buf) == false) {
ei_printf("Failed to capture image\r\n");
free(snapshot_buf);
return;
}
// 运行分类器
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, debug_nn);
if (err != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", err);
return;
}
// 打印推理结果
ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
#if EI_CLASSIFIER_OBJECT_DETECTION == 1
// 打印目标检测边界框
ei_printf("Object detection bounding boxes:\r\n");
for (uint32_t i = 0; i < result.bounding_boxes_count; i++) {
ei_impulse_result_bounding_box_t bb = result.bounding_boxes[i];
if (bb.value == 0) {
continue;
}
ei_printf(" %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
bb.label,
bb.value,
bb.x,
bb.y,
bb.width,
bb.height);
}
// 打印分类推理结果
#else
ei_printf("Predictions:\r\n");
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
ei_printf(" %s: ", ei_classifier_inferencing_categories[i]);
ei_printf("%.5f\r\n", result.classification[i].value);
}
#endif
// 打印异常分数(如有)
#if EI_CLASSIFIER_HAS_ANOMALY==1
ei_printf("Anomaly prediction: %.3f\r\n", result.anomaly);
#endif
#if EI_CLASSIFIER_HAS_VISUAL_ANOMALY
ei_printf("Visual anomalies:\r\n");
for (uint32_t i = 0; i < result.visual_ad_count; i++) {
ei_impulse_result_bounding_box_t bb = result.visual_ad_grid_cells[i];
if (bb.value == 0) {
continue;
}
ei_printf(" %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
bb.label,
bb.value,
bb.x,
bb.y,
bb.width,
bb.height);
}
#endif
free(snapshot_buf);
}
/**
* @brief 设置图像传感器并开始流式传输
*
* @retval 如果初始化失败则为 false
*/
bool ei_camera_init(void) {
if (is_initialised) return true;
#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif
//初始化摄像机
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
return false;
}
sensor_t * s = esp_camera_sensor_get();
// 初始传感器垂直翻转,颜色有点饱和
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, 0); // lower the saturation
}
#if defined(CAMERA_MODEL_M5STACK_WIDE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#elif defined(CAMERA_MODEL_ESP_EYE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
s->set_awb_gain(s, 1);
#endif
is_initialised = true;
return true;
}
/**
* @brief 停止传感器数据流
*/
void ei_camera_deinit(void) {
//停用摄像机
esp_err_t err = esp_camera_deinit();
if (err != ESP_OK)
{
ei_printf("Camera deinit failed\n");
return;
}
is_initialised = false;
return;
}
/**
* @brief 捕捉、调整和裁剪图像
*
* @param[in] img_width 输出图像的宽度
* @param[in] img_height 输出图像的高度
* @param[in] out_buf 存储输出图像的指针,可使用 NULL
* if ei_camera_frame_buffer is to be used for capture and resize/cropping.
*
* @retval false if not initialised, image captured, rescaled or cropped failed
*
*/
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) {
bool do_resize = false;
if (!is_initialised) {
ei_printf("ERR: Camera is not initialized\r\n");
return false;
}
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
ei_printf("Camera capture failed\n");
return false;
}
bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, snapshot_buf);
esp_camera_fb_return(fb);
if(!converted){
ei_printf("Conversion failed\n");
return false;
}
if ((img_width != EI_CAMERA_RAW_FRAME_BUFFER_COLS)
|| (img_height != EI_CAMERA_RAW_FRAME_BUFFER_ROWS)) {
do_resize = true;
}
if (do_resize) {
ei::image::processing::crop_and_interpolate_rgb888(
out_buf,
EI_CAMERA_RAW_FRAME_BUFFER_COLS,
EI_CAMERA_RAW_FRAME_BUFFER_ROWS,
out_buf,
img_width,
img_height);
}
return true;
}
static int ei_camera_get_data(size_t offset, size_t length, float *out_ptr)
{
// 我们已经有了一个 RGB888 缓冲区,所以要将偏移量重新计算为像素索引
size_t pixel_ix = offset * 3;
size_t pixels_left = length;
size_t out_ptr_ix = 0;
while (pixels_left != 0) {
// 在这里将 BGR 换成 RGB
// 基于 https://github.com/espressif/esp32-camera/issues/379
out_ptr[out_ptr_ix] = (snapshot_buf[pixel_ix + 2] << 16) + (snapshot_buf[pixel_ix + 1] << 8) + snapshot_buf[pixel_ix];
// 进入下一个像素
out_ptr_ix++;
pixel_ix+=3;
pixels_left--;
}
// 完成!
return 0;
}
#如果 !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_CAMERA
#错误 "当前传感器的模型无效”
#endif
Raspberry Pi代码
#!/usr/bin/env python
# 这行指定脚本使用的解释器(Python)
import time
import serial
import os
# 设置串口连接的参数,用于与设备进行通信
ser = serial.Serial(
port='/dev/ttyACM0', # 指定设备连接的端口。
baudrate = 115200, # 设置串口通信的波特率
parity=serial.PARITY_NONE, # 不使用奇偶校验位
stopbits=serial.STOPBITS_ONE, # 使用一个停止位
bytesize=serial.EIGHTBITS, # 每个字节有8位数据位
timeout=1 # 设置读取串口时的超时时间为1秒
)
# 进入一个无限循环,不断读取串口数据
while True:
receive_msg=ser.readline() # 从串口读取一行数据
print(receive_msg) # 打印接收到的数据
# 如果接收到的数据中包含“basin”的字样,则执行以下操作:
if b'basin' in receive_msg.lower():
os.system('echo "basin in front" | festival --tts')# 使用festival语音合成引擎朗读提示信息。
# 如果接收到的数据中包含“bed”的字样,则执行以下操作:
if b'bed' in receive_msg.lower():
os.system('echo "bed in front" | festival --tts')
# 如果接收到的数据中包含“chair”的字样,则执行以下操作:
if b'chair' in receive_msg.lower():
os.system('echo "chair in front" | festival --tts')
# 如果接收到的数据中包含“table”的字样,则执行以下操作:
if b'dining table' in receive_msg.lower():
os.system('echo "dining tabl in front" | festival --tts')
# 如果接收到的数据中包含“oven”的字样,则执行以下操作:
if b'oven' in receive_msg.lower():
os.system('echo "oven in front" | festival --tts')
# 如果接收到的数据中包含“refrigerator”的字样,则执行以下操作:
if b'refrigerator' in receive_msg.lower():
os.system('echo "refrigerator in front" | festival --tts')会上传更多对象的更多图像。数据可以从 Edge