ESP32-CAM接入百度智能云车辆识别API完整开发笔记(附避坑指南)

摘要:本文详细记录ESP32-CAM通过Arduino框架调用百度AI车辆识别API的完整流程,包含硬件选型、内存优化、HTTPS请求、Token管理等核心问题,并提供可直接部署的量产级代码。


目录

  1. 项目概述
  2. 硬件准备与开发板配置
  3. 百度智能云配置
  4. 核心代码实现
  5. 关键问题深度解析
  6. 性能优化与调优
  7. 常见问题排查
  8. 总结与扩展

项目概述

功能目标

  • ESP32-CAM实时捕获图像
  • 调用百度智能云vehicle_detect API检测车辆位置/类型
  • 或调用car API识别具体车型(品牌+型号)
  • 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. 创建应用

  1. 登录百度智能云控制台
  2. 进入 产品服务 → 图像识别
  3. 点击 “创建应用”
  4. 勾选 “车辆识别”“车辆检测”
  5. 记录生成的 API KeySecret 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服务器极易耗尽

优化策略:

  1. 启用PSRAM:必须配置PSRAM: Enabled
  2. 单缓冲模式config.fb_count = 1
  3. 合理分辨率:QVGA(320x240)是最佳平衡点
  4. 减小JSON缓冲区StaticJsonDocument<2048>
  5. 移除可选功能:如不需要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)

排查步骤:

  1. 检查PSRAM是否启用
  2. 确认引脚定义与模块匹配
  3. 降低分辨率到FRAMESIZE_QQVGA测试
  4. 测量5V电源是否稳定(不得低于4.8V)

Q2: WiFi连接后无法获取Token

排查步骤:

  1. 确认API Key/Secret Key正确
  2. 检查URL末尾是否有空格
  3. 打印完整URL到串口验证
  4. 确认已开通对应服务(车辆识别/检测)

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)
  • 确保车牌/车标清晰可见

总结与扩展

项目价值

  1. 低成本方案: ESP32-CAM(约30元)+百度AI免费额度
  2. 快速落地: Arduino开发门槛低,代码可直接商用
  3. 灵活扩展: 可轻松切换为车牌识别、人流统计等其他AI能力

扩展方向

  1. 接入MQTT: 将识别结果推送到云平台
  2. 本地缓存: SD卡存储识别记录
  3. 多摄像头: 通过RS485扩展多个摄像头节点
  4. 离线识别: 部署轻量化模型到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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐