一、硬件介绍:
Seeed Studio XIAO ESP32S3 Sense 集成了摄像头传感器、数字麦克风和支持 SD 卡的功能。处理器 ESP32-S3R8 SoC,支持 2.4GHz WiFi 和低功耗蓝牙® BLE 5.0 双模,适用于多种无线应用。它具有锂电池充电管理功能。该电路板配备了一个插入式 OV2640 摄像头传感器,可显示 1600*1200 的全分辨率。它的底座甚至兼容 OV5640,支持高达 2592*1944 的分辨率。电路板还配有数字麦克风,用于语音感应和音频识别。SenseCraft AI 可为XIAO ESP32S3 Sense 提供各种预训练的人工智能(AI)模型和无代码部署。
为了方便使用,给Seeed Studio XIAO ESP32S3 Sense 配了个扩展板。该扩展板专为 Seeed Studio XIAO 设计,仅有半个 Raspberry Pi 4 的大小。它使得原型开发和项目构建变得简单快捷。通过其丰富的外设,包括 OLED、RTC、可扩展内存、无源蜂鸣器、RESET/用户按钮、5V伺服连接器、多种数据接口……您可以探索 Seeed Studio XIAO 的无限可能性。
二、任务选择:
这次活动任务只有一个:互动标牌。这里我把Seeed XIAO ESP32S3 Sense做为一个眼睛,用它来看世界,用AI来理解看到的东西,通过文字的方式,用OLED展示出来。
三、任务实现:
实现本项目,我使用Vscode+platformIO,使用Arduino编程。系统框图如下,摄像头用来采集看到的数据,按键和OLED屏幕负责和用户交互。
1、基本思路:Seeed XIAO ESP32S3 Sense通过摄像头收集图像数据,当用户按下按键,就把当前摄像头获得的图像打包为json,通过http方式上送到阿里云的AI接口。AI负责解析图片内容,然后返回解析的内容,ESP32S3收到返回的内容,就用OLED现实出来,展示给用户解读。因为OLED比较小,采用多页展示的方式显示给用户。以此逻辑绘制流程图:
2、首先在阿里云大模型服务平台 申请API Key,后边图像的解析都需要用到阿里云的大模型服务平台。
3、然后系统先初始化硬件,给摄像头单独启用一个进程,用来不停地读取摄像头。并将每次读取到的图片转换为base64编码格式。
uint8_t CAMER_FLAG = 0;
String imageStr;
uint8_t camera_init()
{
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_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_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_JPEG;
// init with high specs to pre-allocate larger buffers
if (psramFound())
{
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
}
else
{
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
Serial.printf("Camera init failed with error 0x%x", err);
return err;
}
// drop down frame size for higher initial frame rate
sensor_t *s = esp_camera_sensor_get();
s->set_framesize(s, FRAMESIZE_QVGA);
return 0;
}
// 从摄像头不停读取图片,当有信号发生时,拍照
void TaskCamImage(void *pvParameters)
{
camera_fb_t *pic = NULL;
camera_init();
for (;;)
{
pic = esp_camera_fb_get();
if (!pic)
{
log_e("Camera capture failed");
}
log_d("img_buf_len=%d , width=%d , height=%d ", pic->len, pic->height, pic->width);
log_w("Total PSRAM: %d, %d", ESP.getPsramSize(), ESP.getFreePsram());
if (CAMER_FLAG==1)
{
// fb->buf转为base64字符串
imageStr = base64::encode(pic->buf, pic->len);
// Serial.print("image_base64 success!");
Serial.print(" len=");
Serial.print(pic->len);
Serial.println();
// Serial.print(" base64_str=");
// Serial.println(imageStr);
CAMER_FLAG = 9; //形成了完整的图片base64字符串,可以发送给AI了
}
esp_camera_fb_return(pic);
}
}
4、按键部分使用了“OneButton”库,监控着按键按下的动作,一旦有按键按下的动作,就构建AI能够设别的json语句,通过http接口上送当前采集到的图片内容。
// 构建待上传的json 数据
String buildPalyLoad()
{
String payload;
data_json.clear();
data_json["model"] = "qwen-vl-max-latest";
JsonArray msg = data_json.createNestedArray("messages"); // 添加数组.
StaticJsonDocument<120> subnode;
subnode["role"] = "user";
JsonArray content = subnode.createNestedArray("content");
StaticJsonDocument<120> connode;
connode["type"] = "image_url";
connode["image_url"] = "CAME_IMAGE_BASE64";
// connode["image_url"] = image_base64();
content.add(connode);
connode.clear();
connode["type"] = "text";
connode["text"] = "简单描述图片";
content.add(connode);
msg.add(subnode);
serializeJson(data_json, payload);
return payload;
}
void loop(void)
{
// String imageStr;
uint8_t butflag;
btn.tick();
if (WiFi.status() == WL_CONNECTED)
{
if (CAMER_FLAG == 9)
{
CAMER_FLAG = 10;
aiEchoStr = "网络请求中";
String payload = buildPalyLoad();
payload.replace("\"CAME_IMAGE_BASE64\"", "{\"url\":\"data:image/jeg;base64," + imageStr + "\"}");
// Serial.println(payload);
// Serial.println("请稍后...");
HTTPClient http_image_request;
http_image_request.setTimeout(60000);
http_image_request.begin(apiUrl);
http_image_request.addHeader("Content-Type", "application/json");
http_image_request.addHeader("Authorization", String("Bearer ") + String(apiKey));
int httpCode = http_image_request.POST(payload);
if (httpCode == 200)
{
String response = http_image_request.getString();
http_image_request.end();
DynamicJsonDocument jsonDoc(1024);
deserializeJson(jsonDoc, response);
const char* world = jsonDoc["choices"][0]["message"]["content"];
aiEchoStr = world; //获得到AI返回的文字
CAMER_FLAG = 11;
// Serial.println(response);
}
else
{
CAMER_FLAG = 0;
Serial.println("error request: " + String(httpCode));
http_image_request.end();
}
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
else
{
log_e("[WIFI] is not Connecting!");
vTaskDelay(10000 / portTICK_PERIOD_MS);
}
}
5、通过http将摄像头当前看见的图片内容上送后,阿里云那边会返回对图片的解析内容。解析的内容也是一串字符串,这里使用了中文,所以返回的字符串也是一串中文字符串。这里使用了U8g2来在OLED上现实中文。但是板子上OLED位128X64像素的屏幕,显示不了那么多的汉字,只好采用分屏方式逐次现实内容。每次现实4行内容,每行内容为8个汉字。每页停留2秒钟,然后自动翻页。
void oledDispRow(String dispstr, uint8_t rownum)
{
Serial.println(rownum);
u8g2.setFont(u8g2_font_unifont_t_chinese3); // use chinese2
u8g2.clear();
u8g2.firstPage();
do
{
for (uint16_t i = 0; i <= rownum; i++)
{
String subStr = dispstr.substring(i * OLED_ROW, i * OLED_ROW + OLED_ROW);
Serial.printf("i=%d ,rows= %d ", i, rownum);
Serial.println(subStr);
u8g2.setCursor(0, (i + 1) * 16);
u8g2.print(subStr); // Chinese "Hello World"
}
} while (u8g2.nextPage());
}
// 将AI获得的字符串解析出来,并显示
void TaskOledDisp(void *pvParameters)
{
uint16_t strlen = 0;
u8g2.begin();
u8g2.enableUTF8Print();
for (;;)
{
if (CAMER_FLAG == 10)
{
strlen = aiEchoStr.length();
Serial.print("10 strlen:");
Serial.println(strlen);
for (uint16_t i = 0; i < strlen; i += OLED_ROW * 4)
{
String subStr = aiEchoStr.substring(i, i + OLED_ROW * 4);
Serial.println(subStr);
oledDispRow(subStr, (subStr.length() / (OLED_ROW)));
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
if (CAMER_FLAG == 11)
{
strlen = aiEchoStr.length();
Serial.print("11 strlen:");
Serial.println(strlen);
for (uint16_t i = 0; i < strlen; i += OLED_ROW * 4)
{
String subStr = aiEchoStr.substring(i, i + OLED_ROW * 4);
Serial.println(subStr);
oledDispRow(subStr, (subStr.length() / (OLED_ROW)));
vTaskDelay(pdMS_TO_TICKS(5000));
}
CAMER_FLAG = 0;
}
if (CAMER_FLAG == 0) u8g2.clear();
vTaskDelay(pdMS_TO_TICKS(50));
}
}
四、效果展示:
系统上电后,需要保障WIFI的畅通。
当遇到感兴趣的画面后,将摄像头对准感兴趣的场景,然后按下蓝色按键(红色按键未接入系统)。oled显示网络请求中,需要等待一会,等待AI的处理。
稍后阿里平台传回AI模型分析的结果,在OLED上展示。
如果开发板有连着电脑,可以通过电脑串口看见AI平台返回的结果内容。
五、心得体会:
这次项目没有使用ESP32S3自身的机器学习能力,改为调用阿里云大模型服务平台的AI服务,能够不再受限于单片机的算力,可以实现更为复杂的AI分析。但是在使用OLED屏幕做展示时,遇到了问题。问题1,屏幕太小,显示内容有限,不太适合这种场合的内容展示。问题2,使用U8g2显示中文时,很多中文都无法显示,U8g2自带的中文字库局限性太大了。最后感谢电子森林举办的这次活动,通过这次活动实现了将AI工具随身携带的功能,收获满满!