ESP32-CAM接入百度智能云车辆识别API完整开发笔记(附避坑指南)
·
ESP32-CAM接入百度智能云车辆识别API完整开发笔记(附避坑指南)
摘要:本文详细记录ESP32-CAM通过Arduino框架调用百度AI车辆识别API的完整流程,包含硬件选型、内存优化、HTTPS请求、Token管理等核心问题,并提供可直接部署的量产级代码。
目录
项目概述
功能目标
- ESP32-CAM实时捕获图像
- 调用百度智能云
vehicle_detectAPI检测车辆位置/类型 - 或调用
carAPI识别具体车型(品牌+型号) - Web界面实时预览
- Token自动刷新(30天有效期)
技术栈
- 硬件: ESP32-CAM(AI-Thinker/Greekcell)
- 开发环境: Arduino IDE 2.0+
- 关键库:
esp_camera.h(摄像头驱动)WiFiClientSecure.h(HTTPS)ArduinoJson.h(JSON解析)WebServer.h(Web服务)
硬件准备与开发板配置
1. 开发板选型建议
推荐型号: AI-Thinker ESP32-CAM(性价比最高,社区支持完善)
2. Arduino IDE配置
步骤1: 安装ESP32支持包
文件 → 首选项 → 附加开发板管理器网址:
https://dl.espressif.com/dl/package_esp32_index.json
步骤2: 在开发板管理器中搜索并安装 esp32 by Espressif Systems(建议v2.0.5+)
步骤3: 配置开发板参数(关键)
工具 → 开发板 → AI-Thinker ESP32-CAM
关键参数设置:
✓ Upload Speed: 921600
✓ CPU Frequency: 240MHz (WiFi/BT)
✓ Flash Mode: QIO
✓ Flash Size: 4MB (32Mb)
✓ PSRAM: Enabled ← 必须启用,否则内存不足
✓ Partition Scheme: Huge APP (3MB No OTA/1MB SPIFFS)
3. 硬件接线(上传程序时)
| ESP32-CAM引脚 | USB-TTL模块 |
|---|---|
| 5V | VCC (5V) |
| GND | GND |
| U0R | TXD |
| U0T | RXD |
| IO0 | GND(上传时短接) |
| EN | 悬空,上传时按复位键 |
上传步骤: IO0接地 → 点击上传 → 显示"Connecting…"时按EN键复位 → 等待完成 → 断开IO0 → 再次按EN键运行
百度智能云配置
1. 创建应用
- 登录百度智能云控制台
- 进入 产品服务 → 图像识别
- 点击 “创建应用”
- 勾选 “车辆识别” 和 “车辆检测”
- 记录生成的 API Key 和 Secret Key
2. 理解Access Token机制
Token本质: 临时访问凭证,有效期30天,需缓存避免重复获取
Token结构示例:
24.f9ba9c5241b67688bb4adbed8bc91dec.2592000.1485570332.282335-8574074
获取Token的HTTP请求:
GET https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=【AK】&client_secret=【SK】
核心代码实现
完整工程代码(修复版)
/**
* @file ESP32-CAM百度车辆识别
* @brief 支持Web预览+自动Token管理+内存优化
* @version 1.1 (修复URL空格、内存泄漏问题)
*/
#include <esp_camera.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include <base64.h>
// ==================== 配置区域(必须修改) ====================
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASSWORD "你的WiFi密码"
#define BAIDU_API_KEY "你的百度API Key"
#define BAIDU_SECRET_KEY "你的百度Secret Key"
// API选择(二选一):
#define BAIDU_VEHICLE_API "https://aip.baidubce.com/rest/2.0/image-classify/v1/car" // 识别车型
// #define BAIDU_VEHICLE_API "https://aip.baidubce.com/rest/2.0/image-classify/v1/vehicle_detect" // 仅检测位置
#define BAIDU_ACCESS_TOKEN_URL "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
// ==================== 摄像头引脚配置(AI-Thinker) ====================
#define CAMERA_MODEL_AI_THINKER
#if 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 "未定义的摄像头型号"
#endif
// ==================== 全局变量 ====================
String accessToken = "";
unsigned long tokenExpiresTime = 0;
const unsigned long TOKEN_VALID_TIME = 29 * 24 * 60 * 60 * 1000; // 29天
WebServer server(80);
// ==================== URL编码函数 ====================
String urlEncode(String str) {
String encoded = "";
char c;
char code0;
char code1;
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (c == ' ') {
encoded += '+';
} else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else {
code1 = (c & 0xf) + '0';
if ((c & 0xf) > 9) {
code1 = (c & 0xf) - 10 + 'A';
}
c = (c >> 4) & 0xf;
code0 = c + '0';
if (c > 9) {
code0 = c - 10 + 'A';
}
encoded += '%';
encoded += code0;
encoded += code1;
}
}
return encoded;
}
// ==================== 初始化 ====================
void setup() {
Serial.begin(115200);
Serial.println("\nESP32-CAM 车辆识别系统启动...");
// 打印内存信息
Serial.printf("可用堆内存: %d 字节\n", ESP.getFreeHeap());
#ifdef BOARD_HAS_PSRAM
Serial.printf("PSRAM: %d 字节\n", ESP.getPsramSize());
#endif
// 初始化摄像头
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_JPEG;
// 内存优化配置(关键)
config.frame_size = FRAMESIZE_QVGA; // 320x240,平衡效果与内存
config.jpeg_quality = 20; // 0-63,质量与大小平衡
config.fb_count = 1; // 单缓冲,节省内存
config.fb_location = CAMERA_FB_IN_PSRAM; // 使用PSRAM存储图像
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("摄像头初始化失败: 0x%x\n", err);
return;
}
Serial.println("摄像头初始化成功");
// 连接WiFi
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi连接成功, IP: %s\n", WiFi.localIP().toString().c_str());
// 获取Access Token
if (getAccessToken()) {
Serial.println("Token获取成功");
} else {
Serial.println("Token获取失败");
}
// 启动Web服务器
setupWebServer();
Serial.println("Web服务器已启动");
}
// ==================== 主循环 ====================
void loop() {
server.handleClient(); // 处理Web请求
if (accessToken == "") {
Serial.println("Token无效,重试中...");
getAccessToken();
delay(5000);
return;
}
// Token刷新检查
if (millis() > tokenExpiresTime) {
Serial.println("Token过期,刷新中...");
getAccessToken();
}
// 执行车辆识别
Serial.println("\n开始捕获图像...");
String imageBase64 = captureAndEncode();
if (imageBase64.length() > 0) {
Serial.printf("图像编码完成: %d 字节\n", imageBase64.length());
detectVehicle(imageBase64);
} else {
Serial.println("图像捕获失败");
}
delay(8000); // 8秒间隔,避免频繁调用
}
// ==================== 获取Access Token ====================
bool getAccessToken() {
HTTPClient http;
WiFiClientSecure client;
client.setInsecure(); // 跳过证书验证
String url = String(BAIDU_ACCESS_TOKEN_URL) +
"&client_id=" + BAIDU_API_KEY +
"&client_secret=" + BAIDU_SECRET_KEY;
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
http.end();
StaticJsonDocument<512> doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error && doc.containsKey("access_token")) {
accessToken = doc["access_token"].as<String>();
tokenExpiresTime = millis() + TOKEN_VALID_TIME;
return true;
}
}
http.end();
return false;
}
// ==================== 捕获并编码图像 ====================
String captureAndEncode() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("摄像头捕获失败");
return "";
}
// Base64编码
String encoded = base64::encode(fb->buf, fb->len);
encoded.replace("\n", ""); // 移除换行符
esp_camera_fb_return(fb); // 释放内存
return encoded;
}
// ==================== 车辆识别API调用 ====================
void detectVehicle(String imageBase64) {
HTTPClient http;
WiFiClientSecure client;
client.setInsecure();
String url = String(BAIDU_VEHICLE_API) + "?access_token=" + accessToken;
http.begin(client, url);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
// URL编码后发送
String requestBody = "image=" + urlEncode(imageBase64);
int httpCode = http.POST(requestBody);
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println("\n========== 识别结果 ==========");
parseVehicleResult(payload);
} else {
Serial.printf("API调用失败: %d\n", httpCode);
if (httpCode == 403) {
accessToken = ""; // 清空token强制刷新
}
}
http.end();
}
// ==================== 解析识别结果 ====================
void parseVehicleResult(String jsonStr) {
StaticJsonDocument<2048> doc; // 根据API调整大小
DeserializationError error = deserializeJson(doc, jsonStr);
if (error) {
Serial.printf("JSON解析错误: %s\n", error.c_str());
return;
}
// 检查错误码
if (doc.containsKey("error_code")) {
int errorCode = doc["error_code"];
String errorMsg = doc["error_msg"];
Serial.printf("API错误 [%d]: %s\n", errorCode, errorMsg.c_str());
return;
}
// 使用car API时的解析逻辑
if (doc.containsKey("result")) {
JsonArray results = doc["result"];
Serial.printf("检测到 %d 辆车\n", results.size());
for (JsonObject vehicle : results) {
String name = vehicle["name"].as<String>();
double score = vehicle["score"];
String year = vehicle["year"].as<String>();
Serial.printf("车型: %s %s (置信度: %.2f%%)\n",
name.c_str(), year.c_str(), score * 100);
}
}
// 使用vehicle_detect API时的解析逻辑
if (doc.containsKey("vehicle_info")) {
JsonArray vehicleInfo = doc["vehicle_info"];
Serial.printf("检测到 %d 个车辆目标\n", vehicleInfo.size());
for (JsonObject vehicle : vehicleInfo) {
String type = vehicle["type"].as<String>();
double probability = vehicle["probability"].as<double>();
Serial.printf("类型: %s (置信度: %.2f%%)\n",
type.c_str(), probability * 100);
}
}
}
// ==================== Web服务器 ====================
void setupWebServer() {
server.on("/", handleRoot);
server.on("/stream", handleStream);
server.begin();
}
void handleRoot() {
String html = "<!DOCTYPE html><html><head>";
html += "<meta charset='UTF-8'><meta name='viewport' content='width=device-width'>";
html += "<title>ESP32-CAM监控</title>";
html += "<style>body{font-family:Arial;text-align:center;background:#f0f0f0;}";
html += "img{max-width:100%;border:2px solid #ddd;}</style></head><body>";
html += "<h1>ESP32-CAM实时画面</h1>";
html += "<p>IP: " + WiFi.localIP().toString() + "</p>";
html += "<img src='/stream' alt='摄像头'>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleStream() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Camera error");
return;
}
server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
关键问题深度解析
🔴 问题1:Token URL末尾空格导致404
现象: HTTP响应码: 404
根源:
#define BAIDU_ACCESS_TOKEN_URL ".../token?grant_type=client_credentials " // 错误!
修复: 删除末尾空格
🔴 问题2:API选择混淆
| API接口 | 功能 | 返回数据 | 适用场景 |
|---|---|---|---|
/v1/car |
车型识别 | 品牌、型号、年份、颜色 | 需要知道具体车型 |
/v1/vehicle_detect |
车辆检测 | 位置、类型(car/truck/bus) | 仅需计数和定位 |
🔴 问题3:内存溢出崩溃
现象: 随机重启或卡死
分析: ESP32-CAM内存约300KB,同时运行摄像头+WiFi+HTTPS+Web服务器极易耗尽
优化策略:
- 启用PSRAM:必须配置
PSRAM: Enabled - 单缓冲模式:
config.fb_count = 1 - 合理分辨率:QVGA(320x240)是最佳平衡点
- 减小JSON缓冲区:
StaticJsonDocument<2048> - 移除可选功能:如不需要Web预览,可禁用Web服务器
🔴 问题4:电源不稳定
现象: 上传失败、图像有条纹、随机重启
解决方案:
- 独立5V 1A+电源(电脑USB不足)
- 并联470μF电容在5V和GND之间
- 缩短电源线长度
性能优化与调优
1. Token缓存持久化(避免重启重复获取)
#include <Preferences.h>
Preferences prefs;
void saveToken(String token) {
prefs.begin("baidu", false);
prefs.putString("token", token);
prefs.putULong("expire", millis() + TOKEN_VALID_TIME);
prefs.end();
}
String loadToken() {
prefs.begin("baidu", true);
String token = prefs.getString("token", "");
prefs.end();
return token;
}
2. 图像质量动态调整
// 根据光线自动调整
config.jpeg_quality = (WiFi.RSSI() > -50) ? 15 : 25; // 信号好时高质量
3. 异步处理(FreeRTOS)
// 将车辆识别放在独立任务
void detectTask(void *pvParameters) {
while(1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等待信号
String img = captureAndEncode();
detectVehicle(img);
vTaskDelay(pdMS_TO_TICKS(8000));
}
}
常见问题排查
Q1: 摄像头初始化失败(0x20001或0x20002)
排查步骤:
- 检查PSRAM是否启用
- 确认引脚定义与模块匹配
- 降低分辨率到
FRAMESIZE_QQVGA测试 - 测量5V电源是否稳定(不得低于4.8V)
Q2: WiFi连接后无法获取Token
排查步骤:
- 确认API Key/Secret Key正确
- 检查URL末尾是否有空格
- 打印完整URL到串口验证
- 确认已开通对应服务(车辆识别/检测)
Q3: API返回"image too large"
原因: Base64编码后超过2MB限制
解决:
config.frame_size = FRAMESIZE_QVGA; // 降低分辨率
config.jpeg_quality = 30; // 提高压缩率
Q4: 识别效果差
优化方法:
- 增加补光(ESP32-CAM自带LED,GPIO4控制)
- 调整摄像头角度(与车辆保持水平)
- 降低jpeg_quality(15-20)
- 确保车牌/车标清晰可见
总结与扩展
项目价值
- 低成本方案: ESP32-CAM(约30元)+百度AI免费额度
- 快速落地: Arduino开发门槛低,代码可直接商用
- 灵活扩展: 可轻松切换为车牌识别、人流统计等其他AI能力
扩展方向
- 接入MQTT: 将识别结果推送到云平台
- 本地缓存: SD卡存储识别记录
- 多摄像头: 通过RS485扩展多个摄像头节点
- 离线识别: 部署轻量化模型到ESP32-S3(支持AI加速)
生产环境建议
- 使用Authorization Header方式(更安全)
- 增加看门狗防止死机
- 实现OTA远程升级
- 添加异常告警机制
4. 串口调试日志示例
ESP32-CAM 车辆识别系统启动...
可用堆内存: 298456 字节
PSRAM: 4194304 字节
摄像头初始化成功
WiFi连接成功, IP: 192.168.1.100
Token获取成功
Web服务器已启动
开始捕获图像...
图像编码完成: 24567 字节
========== 识别结果 ==========
车型: 宝马X5 2020款 (置信度: 98.76%)
==============================
版权声明: 本文为原创内容,转载请注明出处。
联系方式: 如有问题欢迎留言讨论。
更新时间: 2024-01-17
测试环境:
- ESP32-CAM AI-Thinker
- Arduino IDE 2.2.1
- esp32 package v2.0.11
更多推荐



所有评论(0)