AI 摄像头:ESP32-CAM 抓图 + 端侧识别 + 推送手机
- 做出一台能抓图、在板上做简单识别(运动/颜色/人脸检测)、有情况就把画面推到手机或服务器的 AI 摄像头
- 用可跑的 ESP-IDF esp_camera 代码走通「初始化摄像头 → 抓一帧 → 处理这帧」的完整链路,而不是只刷官方网页示例
- 认清 ESP32-CAM 的三个成品级命门:PSRAM 必需、IO0 下载模式、摄像头排线,把它们在接线阶段一次躲干净
- 想清端侧能做什么(运动/颜色/人脸检测)、做不到什么(认出是谁、是什么),复杂识别怎么往边缘 AI 或云端接
| 器材 | 数量 | 参考 |
|---|---|---|
| ESP32-CAM 开发板(AI-Thinker,板载 OV2640 摄像头 + PSRAM) | 1 | —约 20-40 元(以商城实际为准) |
| USB 转 TTL 下载器(FTDI / CP2102,3.3V/5V 可切) | 1 | —约 5-12 元(以商城实际为准) |
| 杜邦线(母对母若干) | 1 套 | —约 3-8 元(以商城实际为准) |
价格随渠道波动,以购买页实时为准。
桌上放一块比火柴盒还小的板子,一头一颗黄豆大的摄像头对着门口。你人在公司,没人动它时它悄悄省电;一旦画面里有东西动了,它抓下那一帧,"叮"地推到你手机上——门口来人了。整套物料,不到五十块钱。
这就是本项目要带你做出来的:一台会"看"、还会"判断该不该报"的 AI 摄像头。 它不是让你再刷一遍官方网页直播示例——那个 ESP32-CAM 入门已带你跑通过,你知道它能出画面了。project 这一层要往前推一大步:自己写代码抓帧、在板上做点简单识别、有情况才推出去,串成一件能挂门口天天用的成品。
这一篇不重复讲 ESP32-CAM 是什么、三个坑为什么坑——那些 l4-esp32cam 讲透了,这里默认你跑通过官方画面。我们只干一件事:把摄像头接进你自己的 ESP-IDF 代码里,让它从"能被看"变成"会自己看、自己判断"。
第一步:想清成品长什么样,再定选型
动手前把"成品行为"钉死,选型才有依据。我们的 AI 摄像头就三个行为:
- 抓帧:设备能主动拍下当前画面,拿到一块可处理的图像内存,而不是被动等浏览器来看。
- 识别:板上对这帧做轻量判断——最典型的是运动检测(前后两帧差异大就是"有动静"),也可以是找某种颜色、或简单人脸检测。
- 告警/推送:判断出"有情况",就把这一帧或一条消息推到手机 / 服务器;没情况就安静省电。
行为定了,选型每一步就都有理由。
ESP32-CAM 还是 S3 带摄像头板?
这是本项目唯一要做的选择题,答案取决于你的识别野心:
| ESP32-CAM(AI-Thinker) | ESP32-S3 摄像头板(如 S3-EYE 类) | |
|---|---|---|
| 芯片算力 | 原始 ESP32,双核 240MHz | S3,带向量指令,跑轻量神经网络快得多 |
| PSRAM | 板载约 4MB,够抓图 | 通常 8MB,跑模型更宽裕 |
| 端侧 AI | 只适合运动/颜色/简单人脸检测 | 能跑量化后的小模型(人脸识别、简单分类) |
| 价格 | 更便宜,二十几块 | 贵一截 |
| 供电脾气 | 娇气,推流易 Brownout | 相对稳,多带正规 USB 口 |
一句话决策:先做运动/颜色告警这类"察觉变化"的项目,ESP32-CAM 足够、也最省钱,本文主线就用它;想在板上真跑识别模型(认人脸、分类物体),直接上 S3 摄像头板。 两者的 esp_camera 代码几乎一模一样,只差引脚定义那张表,换板迁移几乎零成本。
顺带说清一件常被搞混的事:AI-Thinker ESP32-CAM 用的是最原始的 ESP32 芯片(不是 ESP32-S3),没有 S3 那套加速轻量神经网络的向量指令。所以本文主线代码写成跨芯片通用——ESP32、ESP32-S3 都能跑,只在换板时改那张引脚表;真要跑端侧模型时 S3 的算力优势才显出来。
为什么必须是带 PSRAM 的板子
这是 ESP32-CAM 成品级的第一条命门,比选脚还重要:一帧图像太大,芯片内置那点 RAM 装不下。 一张 QVGA(320×240)的 JPEG 就要几十 KB,稍高分辨率上百 KB,而 ESP32 内置 SRAM 拢共几百 KB 还要留给系统和 Wi-Fi。所以帧缓冲必须放进 PSRAM(板载的那颗外部大内存)。
好在 AI-Thinker 这类标准 ESP32-CAM 都自带 PSRAM,你只要在代码里把帧缓冲位置指定成 CAMERA_FB_IN_PSRAM(下面代码里有)。买板时务必确认"带 PSRAM"——极少数阉割版没有,那种只能出最低分辨率,稍微一调高就初始化失败,是新手最冤的翻车点。
下载器:为什么要单独买
l4-esp32cam 讲过,最常见的 ESP32-CAM 没有 USB 口,一排光秃秃的引脚。要刷代码,得靠一个 USB 转 TTL 下载器(FTDI 或 CP2102 都行)在电脑和板子之间转一道。买的时候认准一点:电压能切到能提供 5V——ESP32-CAM 推流瞬间电流不小,下载器供电偏弱是它反复重启的头号原因。
第二步:接线——排线、下载模式、供电三关
ESP32-CAM 的接线和你之前做台灯、环境站不一样:它没面包板那套自由发挥,就固定这几根线,但一根都不能错。
摄像头排线:多半已焊好,别去乱抠
OV2640 摄像头通过一条软排线(FPC)连到板上的座子。绝大多数 ESP32-CAM 出厂就插好了,你不需要动它。真要注意的只有:如果你收到货是分开的、或者调试中出现初始化失败,翻开排线座的黑色卡扣,把排线蓝色面朝向卡扣、金属触点朝下平推到底,再压回卡扣。排线插反、没插到底、金属面朝错,都会让摄像头初始化失败——这是花屏和 Camera init failed 的一大来源。
下载器接线:TX/RX 交叉,别接反
下载器和 ESP32-CAM 就这几根线:
USB 转 TTL 下载器 ESP32-CAM
5V ───────────────────── 5V
GND ───────────────────── GND
TX ───────────────────── U0R (RX) ← 交叉
RX ───────────────────── U0T (TX) ← 交叉
下载模式(烧录时才接):
IO0 ───────────────────── GND
- 供电走 5V:ESP32-CAM 的
5V脚接下载器 5V。它板载有稳压,别去接 3.3V 脚硬灌,容易供不上。 - TX/RX 必须交叉:下载器的
TX接板子的U0R(接收),下载器的RX接板子的U0T(发送)。一方的"发"接另一方的"收",接反了刷不进也没日志——这是仅次于供电的高频翻车点。
烧录前把 IO0 接到 GND,进下载模式;上电(或按 RST)后板子才等着你烧。烧完,把 IO0 和 GND 断开,再按一下 RST,它才会去跑你刚烧的程序。很多人"刷不进"或"刷进去却不动",就是这根线没处理对——烧时接上、跑时拔掉。这跟 l4-esp32cam 里说的完全一致,只是这次你会反复烧好几版代码,每次都别忘了这个动作。
供电这一关,决定稳不稳
摄像头一开工、Wi-Fi 一推数据,电流会冲高。供电稍弱,板子就在启动或推流瞬间突然重启,串口刷 Brownout detector was triggered——看着像程序崩了,其实是电不够。对策:用能给 5V 的下载器、换一根短而粗的好 USB 线;实在压不住就单独给板子一路稳的 5V。供电这关过不去,下面代码写得再对也跑不稳,先解决它。
第三步:分步把 ESP-IDF 代码写出来
我们不一次甩一大段,而是分三步长出来,每步都能单独烧进去看效果——出问题你才知道是哪步坏的。
前置:把官方 esp32-camera 组件加进工程。在工程目录下用组件管理器拉一次即可:
idf.py add-dependency "espressif/esp32-camera"
它会把 esp_camera.h 那套接口拉进来,下面代码就是围着它写的。
步 1:初始化摄像头,抓下第一帧
第一步只验证一件事:摄像头配对了、PSRAM 用上了、能拿到一帧数据。 这段是整个项目的地基,把 AI-Thinker 的引脚表、时钟、帧缓冲位置一次配好:
#include "esp_camera.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "ai-cam";
// AI-Thinker ESP32-CAM 的引脚定义(换 S3 板就换这一整张表,代码逻辑不动)
#define PWDN_GPIO 32
#define RESET_GPIO -1
#define XCLK_GPIO 0
#define SIOD_GPIO 26 // SCCB 数据
#define SIOC_GPIO 27 // SCCB 时钟
#define Y9_GPIO 35
#define Y8_GPIO 34
#define Y7_GPIO 39
#define Y6_GPIO 36
#define Y5_GPIO 21
#define Y4_GPIO 19
#define Y3_GPIO 18
#define Y2_GPIO 5
#define VSYNC_GPIO 25
#define HREF_GPIO 23
#define PCLK_GPIO 22
static esp_err_t camera_init(void)
{
camera_config_t config = {
.pin_pwdn = PWDN_GPIO,
.pin_reset = RESET_GPIO,
.pin_xclk = XCLK_GPIO,
.pin_sccb_sda = SIOD_GPIO,
.pin_sccb_scl = SIOC_GPIO,
.pin_d7 = Y9_GPIO, .pin_d6 = Y8_GPIO, .pin_d5 = Y7_GPIO, .pin_d4 = Y6_GPIO,
.pin_d3 = Y5_GPIO, .pin_d2 = Y4_GPIO, .pin_d1 = Y3_GPIO, .pin_d0 = Y2_GPIO,
.pin_vsync = VSYNC_GPIO, .pin_href = HREF_GPIO, .pin_pclk = PCLK_GPIO,
.xclk_freq_hz = 20000000, // 20MHz 时钟,标准值
.ledc_timer = LEDC_TIMER_0, // 摄像头时钟借用 LEDC 生成
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG, // 抓 JPEG,省内存、好传
.frame_size = FRAMESIZE_QVGA, // 320x240,入门先用小的稳
.jpeg_quality = 12, // 数字越小越清晰、越占内存,10~15 之间挑
.fb_count = 2, // 双缓冲,抓帧更顺
.fb_location = CAMERA_FB_IN_PSRAM, // 帧缓冲放 PSRAM——命门,没它必崩
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "摄像头初始化失败 0x%x:查排线/PSRAM/供电", err);
}
return err;
}
void app_main(void)
{
if (camera_init() != ESP_OK) return;
// 抓一帧看看
camera_fb_t *fb = esp_camera_fb_get(); // 拿到一帧(frame buffer)
if (!fb) {
ESP_LOGE(TAG, "抓帧失败:多半是内存不足或摄像头没就绪");
return;
}
ESP_LOGI(TAG, "抓到一帧!格式=%d 宽=%d 高=%d 大小=%u 字节",
fb->format, fb->width, fb->height, (unsigned)fb->len);
esp_camera_fb_return(fb); // 用完必须归还,否则下一帧没缓冲
}
烧进去 idf.py build flash monitor(别忘了烧前 IO0 接 GND、烧后拔掉再复位)。你应该看到串口打出一行 抓到一帧!... 大小=xxxxx 字节,大小通常几 KB 到几十 KB。看到这个,说明摄像头引脚、PSRAM、供电三件事全对——这一步的意义就是把最容易崩的地基先隔离验证掉。
esp_camera_fb_get()拿帧、esp_camera_fb_return(fb)还帧,必须成对。忘了 return,缓冲池很快被占满,下一次 get 直接返回 NULL、程序就"卡"在抓不到帧——这是自己写摄像头代码最常见的一个坑,比台灯那个"忘了 update_duty"还隐蔽。从项目一开始就把 get/return 当一对来写。
步 2:做最实用的端侧识别——运动检测
有了稳定抓帧,加"识别"。端侧算力有限,l4-esp32cam 讲过它的边界:能做运动/颜色/简单人脸检测,认不出"是谁、是什么"。 这里选最实用、最省算力的运动检测——不需要"认识"任何东西,纯比前后两帧的差异。
思路:JPEG 不好直接逐像素比,所以这一步把像素格式换成灰度、分辨率压到很小(比如 96×96),拿两帧灰度图逐点相减、差异超阈值的点够多,就判定"动了"。注意:做识别时把 pixel_format 改成 PIXFORMAT_GRAYSCALE、frame_size 设小(识别不需要高清,小图省内存又快):
// 把步 1 的 config 改两处:
// .pixel_format = PIXFORMAT_GRAYSCALE, // 灰度,一个像素一字节,好比对
// .frame_size = FRAMESIZE_96X96, // 96x96,运动检测够用
#define MOTION_PIX_THRESH 40 // 单个像素亮度差超过它,算"这个点变了"
#define MOTION_CNT_THRESH 200 // 变了的点超过这么多,算"画面动了"
static uint8_t *g_prev = NULL; // 上一帧灰度图,放堆上
static size_t g_prev_len = 0;
// 返回 true 表示检测到运动
static bool detect_motion(camera_fb_t *fb)
{
if (g_prev == NULL || g_prev_len != fb->len) { // 第一帧或尺寸变了,先存下当基准
free(g_prev);
g_prev = malloc(fb->len);
if (!g_prev) return false;
memcpy(g_prev, fb->buf, fb->len);
g_prev_len = fb->len;
return false; // 第一帧没得比,不算动
}
int changed = 0;
for (size_t i = 0; i < fb->len; i++) {
int diff = (int)fb->buf[i] - (int)g_prev[i];
if (diff < 0) diff = -diff;
if (diff > MOTION_PIX_THRESH) changed++;
}
memcpy(g_prev, fb->buf, fb->len); // 更新基准帧
return changed > MOTION_CNT_THRESH;
}
void app_main(void)
{
if (camera_init() != ESP_OK) return;
while (1) {
camera_fb_t *fb = esp_camera_fb_get();
if (fb) {
if (detect_motion(fb)) {
ESP_LOGW(TAG, ">>> 检测到运动!<<<");
}
esp_camera_fb_return(fb); // 无论动没动,帧都要还
}
vTaskDelay(pdMS_TO_TICKS(200)); // 每 200ms 看一眼,5 帧/秒够用还省电
}
}
你应该看到:画面静止时串口安静;你手在镜头前一晃,立刻刷出 >>> 检测到运动!<<<。两个阈值调灵敏度——MOTION_PIX_THRESH 调低更敏感(连光线微变都报),MOTION_CNT_THRESH 调低更容易触发。屋里光照抖动老误报,就把这两个都往上抬一点。
想做颜色检测(比如"画面里出现红色物体就触发"),把格式换成
PIXFORMAT_RGB565,逐像素判断 R 分量是否明显大于 G/B,思路和这里一模一样,只是比的从"前后帧差异"变成"当前帧里某颜色的像素数"。简单人脸检测要接官方的人脸检测组件,那是把小模型跑在板上,S3 板更从容——想走这条深路,看把 AI 跑在板子上。
步 3:有情况才推——把画面/告警送到手机
最后一步,把"识别到了"变成"你手机能收到":检测到运动,就把这一帧 JPEG POST 到你的服务器 / 消息推送接口(自建告警服务,或任意能收图的 HTTP 端点)。
先把一件容易埋雷的事讲清:运动检测和推图,用的是两种不同的像素格式,不能图省事拿一路数据既检测又推送。 步 2 的 detect_motion 是逐字节比灰度像素——它只对 PIXFORMAT_GRAYSCALE 的原始像素有意义;而推给手机存证要的是清晰 JPEG。JPEG 是压缩后的字节流,逐字节相减不对应任何像素,拿它跑 detect_motion 等于白检测。
所以正确做法是检测归检测、推送归推送,两种格式各抓各的:
- 运动检测:仍沿用步 2 的配置——
PIXFORMAT_GRAYSCALE+ 低分辨率(96×96),跑detect_motion。这一路省算力、判断快。 - 推图存证:检测到运动后,要一张清晰 JPEG,就得单独抓一帧 JPEG——不能拿检测用的那帧灰度小图直接推。
板上摄像头同一时刻只有一个像素格式,做不到"一帧同时是灰度又是 JPEG"。想在检测到运动后推清晰 JPEG,实际有两条路:(A)切换像素格式重抓一帧——esp_camera_deinit() 后用 JPEG 配置 esp_camera_init() 重新初始化再抓,代价是切换有开销、这一瞬间检测会短暂中断;(B)用两台/两路缓冲——高阶玩法,硬件和内存都更吃紧。入门先走 A:平时用灰度小图连续检测,一旦判定"动了",切到 JPEG 抓一帧推出去,推完再切回灰度继续盯。 下面代码演示这条 A 路。
先看 POST 那段(它只管把一帧 JPEG 发出去,跟检测无关):
#include "esp_http_client.h"
#define ALERT_URL "http://你的服务器IP:端口/alert" // 收图的端点,换成你自己的
// 把一帧 JPEG 通过 HTTP POST 发出去
static void push_frame(camera_fb_t *fb)
{
esp_http_client_config_t cfg = {
.url = ALERT_URL,
.method = HTTP_METHOD_POST,
.timeout_ms = 8000,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
esp_http_client_set_header(client, "Content-Type", "image/jpeg");
esp_http_client_set_post_field(client, (const char *)fb->buf, fb->len);
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
ESP_LOGI(TAG, "已推送告警图,服务器返回 %d",
esp_http_client_get_status_code(client));
} else {
ESP_LOGE(TAG, "推送失败:%s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
}
主循环把两路串起来:平时用灰度小图连续检测;一旦判定"动了"、且过了冷却期,就切到 JPEG 抓一帧推出去,推完切回灰度继续盯。 冷却是必须的——不然人一直在镜头前,它一秒推十张,把你手机和服务器都刷爆。切格式用两个小助手包住 deinit + init,读起来清楚:
#include "esp_timer.h"
#define ALERT_COOLDOWN_MS 10000 // 两次告警至少隔 10 秒
// 用灰度 96x96 初始化——检测模式(复用步 1 的引脚,只改这两项)
static esp_err_t camera_init_gray(void)
{
camera_config_t config = { /* …引脚同步 1… */
.pixel_format = PIXFORMAT_GRAYSCALE,
.frame_size = FRAMESIZE_96X96,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
/* xclk / ledc 等同步 1 */
};
return esp_camera_init(&config);
}
// 用 JPEG QVGA 初始化——推图模式
static esp_err_t camera_init_jpeg(void)
{
camera_config_t config = { /* …引脚同步 1… */
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QVGA,
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
/* xclk / ledc 等同步 1 */
};
return esp_camera_init(&config);
}
// 检测到运动时:切 JPEG → 抓一帧清晰图推出去 → 切回灰度继续检测
static void snap_jpeg_and_push(void)
{
esp_camera_deinit(); // 先卸掉灰度那套
if (camera_init_jpeg() != ESP_OK) { // 换成 JPEG 配置
ESP_LOGE(TAG, "切 JPEG 失败,跳过这次推送");
camera_init_gray(); // 尽量切回去别卡死
return;
}
// 切格式后头一两帧常是脏的,丢掉再取一张干净的
camera_fb_t *warm = esp_camera_fb_get();
if (warm) esp_camera_fb_return(warm);
camera_fb_t *jpg = esp_camera_fb_get();
if (jpg) {
push_frame(jpg); // 推的是真 JPEG,不是灰度差分那路
esp_camera_fb_return(jpg);
}
esp_camera_deinit(); // 推完卸掉 JPEG
camera_init_gray(); // 切回灰度,继续盯
}
void app_main(void)
{
// 联网部分照搬 l3-wifi:wifi_init_sta() 连上路由器后再进循环
if (camera_init_gray() != ESP_OK) return; // 开机先进检测模式(灰度)
int64_t last_alert = -(int64_t)ALERT_COOLDOWN_MS * 1000; // 微秒
while (1) {
camera_fb_t *fb = esp_camera_fb_get(); // 这是灰度小图
if (fb) {
bool moved = detect_motion(fb); // 只对灰度像素做差分,成立
esp_camera_fb_return(fb); // 灰度帧用完先还,别占着去切格式
int64_t now = esp_timer_get_time();
if (moved && (now - last_alert) > (int64_t)ALERT_COOLDOWN_MS * 1000) {
ESP_LOGW(TAG, ">>> 有情况,切 JPEG 抓一帧推送 <<<");
snap_jpeg_and_push(); // 切 JPEG → 推 → 切回灰度
last_alert = now;
}
}
vTaskDelay(pdMS_TO_TICKS(200)); // 每 200ms 看一眼,5 帧/秒够用还省电
}
}
你应该看到:镜头前有动静 → 串口打 有情况,切 JPEG 抓一帧推送 并显示服务器返回码 → 你服务器 / 手机端收到那张清晰 JPEG(不是灰度差分用的那张小图)。冷却生效时,连续晃动只会隔 10 秒推一次,不再刷屏。
切格式那一下有开销(
deinit + init大概几十到上百毫秒),这期间检测会短暂停一下——对"门口有人动一下"这类告警完全够用,别为了追帧率把它复杂化。要真做到检测不中断,得上双路缓冲那套高阶玩法,等手感稳了再啃。关键是记住:检测那路永远是灰度、推送那路永远是 JPEG,别让一路数据串岗。
联网这块(
wifi_init_sta、事件组、证书)一个字没重复讲,因为它和连上 Wi-Fi 一模一样;把图 POST 上去、内存/超时更敏感这件事,l4-esp32cam 讲透了——图比文字大得多,POST 时的内存和上传耗时会更突出,Key 别硬编码进代码,那节的注意事项这里一字不差地适用。到这里,你没写多少全新的东西:抓帧是 esp_camera 的、联网是 l3-wifi 的、POST 是 esp_http_client 的,你干的是把"抓帧 → 识别 → 冷却 → 推送"这四件事组织成一条自主链路——这个"组织",就是 project 比 guide 多出来的那层功夫。
第四步:调试——对不上就查这张表
分步烧的好处是,哪步崩你已经缩小了范围。真出岔子,照这张表从"电和排线"往"逻辑"查:
| 现象 | 最可能的原因 | 怎么办 |
|---|---|---|
串口刷 Brownout detector was triggered、反复重启 |
供电不足 | 换能给 5V 的下载器、换短而粗的好 USB 线;先过供电这关 |
摄像头初始化失败 0x… / Camera init failed |
排线没插好 / PSRAM 缺失 / 引脚表选错板 | 重插摄像头排线(蓝面朝卡扣、插到底);确认买的是带 PSRAM 的板;核对引脚表对应你这款 |
| 初始化过了但抓帧总返回 NULL | 忘了 fb_return,缓冲被占满;或分辨率太高内存不够 |
确认每次 get 后都有 return;把 frame_size 调小、fb_count 降到 1 试试 |
| 画面/推出去的图花屏、雪花 | 供电不稳;jpeg_quality 太激进;排线接触不良 |
先稳供电;jpeg_quality 数字往大调(如 15);重插排线 |
编译报 esp_camera.h: No such file |
没加 esp32-camera 组件 | 跑 idf.py add-dependency "espressif/esp32-camera" 再编 |
刷不进 / 一连 Failed to connect |
没进下载模式,或 TX/RX 接反 | IO0 接 GND 后再上电;检查 TX 接 U0R、RX 接 U0T(交叉) |
| 刷进去了但不跑你的程序 | 烧完没断开 IO0 | 拔掉 IO0 与 GND 的连线,按一下 RST |
| 运动检测一直误报 | 阈值太敏感 / 环境光在抖 | 抬高 MOTION_PIX_THRESH 和 MOTION_CNT_THRESH;避开忽明忽暗的光源 |
| 触发了但推送失败 | URL/端口不对、没联网、服务器没起 | 核对 ALERT_URL;确认板子连上 Wi-Fi;先用能收 POST 的测试端点验证 |
| 一推图就重启 | 推流瞬间电流冲高,供电扛不住 | 还是供电——加强 5V,或把分辨率/质量降一档 |
排查总原则:先怀疑供电,再查排线和下载模式,最后才看代码。 ESP32-CAM 一多半的"灵异问题"根子都在电不够和排线没插好。一次只改一处再烧,同时改三个地方还是不对,你根本分不清是哪个改动的锅。
第五步:从"能报警的 demo"做成"真管用的东西"
到这它已经是台能自主抓图、识别、告警的摄像头了。但"能用"和"真管用"之间还差几步——正好通向后面的阶梯,先给你指条路。
让识别更聪明:接边缘 AI 模型
运动检测只会说"有东西动了",不会说"动的是人还是猫还是被风吹动的窗帘"。想让板子自己认出"是不是人",得把一个量化后的小检测模型跑在板上——这就是边缘 AI。ESP32-CAM 算力紧,跑得吃力;S3 摄像头板带向量指令,跑量化小模型从容得多。这条路怎么走、模型怎么塞进这颗小芯片,看把 AI 跑在板子上的边缘智能——它讲清了端侧模型的门槛和取舍。
让判断更准:把图甩给云端理解
要认出"这是张三""画面里有个包裹""有人翻墙"这种复杂内容,端侧扛不住,思路和硬件调大模型一样:板子只管省着拍、按需传,理解交给云端多模态大模型。 运动触发后把那帧 JPEG POST 给云端视觉接口,拿回一段文字结果再决定报不报——你在步 3 已经把"POST 一帧图上去"这条链路打通了,换个接收端就是它。完整的压缩、流式上传、成本控制属于更深的话题,L4 这一级往下走有体系化的实战。
做成真正的成品
面包板加飞线只能算原型。想挂门口天天用甚至送人,得走产品化:稳定 5V 供电(别再靠下载器吊着)、电路挪到洞洞板或小 PCB、配个带镜头孔的外壳、固件量产烧录。这些是更后面阶梯的活儿,多做几个项目、手感稳了再来啃——这台 AI 摄像头正是做第一件"带镜头实体产品"的好对象。
小结 · 你做出了什么、下一步去哪
- 你做出了一台会自己抓帧、在板上做运动检测、有情况才把画面推到手机/服务器的 AI 摄像头,从选型、排线/下载模式/供电三关接线、可跑的 ESP-IDF esp_camera 代码到花屏/初始化失败/内存排查,走完了一件成品的全流程。
- 你第一次把硬件的"眼睛"接进自己的代码,用
esp_camera_fb_get / return抓帧、灰度差分做端侧识别、冷却 +esp_http_client推送,把"抓帧 → 识别 → 判断 → 推送"组织成一条自主链路——这个"组织零件"的功夫,就是 project 比 guide 多出来的那层。 - 你摸清了几个成品级命门:帧缓冲必须放 PSRAM、get/return 必须成对、识别用小灰度图省算力、告警要加冷却别刷屏、图比文字大所以内存和上传更敏感。
下一步:想让它认得出"是什么",看把 AI 跑在板子上的边缘智能;想把摄像头和其他传感器揉一起做决策,回 L4 这一级往下走。回看全部实战项目见项目总览。这台会看又会判断的小摄像头,是你从"控制引脚"跨到"感知世界"最有分量的一件成品——留着它。