我一直对间谍小工具和微型电子产品非常着迷。我一直想创造一个可以装在口袋里的微型相机,能够悄无声息地捕捉精彩瞬间。随着技术的进步和像 Xiao ESP32S3 Sense 这样功能强大的微控制器的出现,我终于有机会实现这个梦想了。
使用实际部署模型的设备为 tinyML 项目收集图像数据通常也是一项挑战。因此,这款相机也是远程图像数据收集的有用设备。
这个名为 “The Smallest DIY Spy Cam ”的项目是一个可以自己制作的小型摄像头。它既简单又经济实惠,是进入嵌入式电子世界的绝佳方式。
我们使用的 Xiao ESP32S3 Sense 具有体积小、功耗低、可扩展摄像头模块(sense)和功能强大等特点,非常适合制作最小的 DIY 间谍摄像头。
图 1 DIY 微型摄像头
硬件类
Seeed Studio XIAO ESP32S3 Sens
Arduino IDE
工具类
Solder Wire, Lead Free
Soldering iron (generic)
图 2 XIAO
特点:
外形小巧: 这款相机采用 Xiao ESP32S3 Sense,体积非常小巧,易于隐藏。
按下按钮即可捕捉图像: 专用按钮可让您立即拍照。
自动图像命名: 每张图片都以顺序文件名(image1、image2、image3 等)保存,因此您永远不会丢失捕获的图片。
SD 卡存储: 图像可直接保存到 SD 卡中,便于传输到电脑上。
省电模式: 长按按钮可使设备进入深度睡眠状态,耗电量极低。当再次检测到长按按钮时,它会唤醒并打开 LED 指示灯。
组件:
图 3 组件
Xiao ESP32S3 Sense: 操作的大脑,提供处理能力和连接性。
XiaoCamera 模块:捕捉高质量图像。
Micro SD 卡模块:存储拍摄的图像。
按钮:用于图像捕捉和电源控制。
锂电池:为相机供电。
电线: 将各部件连接在一起所必需的电线。
首先将摄像头模块连接到 Xiao ESP32S3 Sense。
然后,连接 SD 卡模块。确保接线正确,以免在数据存储过程中出现任何问题。
图 4 XIAO 部分引脚
将捕捉按钮连接到 Xiao 的 GPIO 引脚 D0。
将锂电池焊接到 Xiao 的电池焊盘上,以获得便携式电源。
图 5 组装部件
因为我用的是廉价烙铁,所以焊接得不是很整齐,但这并不重要。您需要用 3D 打印相机的外壳。我附上了打印用的 STL 文件。这是完全组装好的相机与 20 KSH(可能只有一分钱大小)的对比。
图 6 微型相机与一分钱硬币对比图
如果还没有 Arduino IDE,请下载。
克隆此项目的 GitHub 仓库。
在 Arduino IDE 中打开项目,并从板管理器中选择 Xiao ESP32S3 板。
以下是代码
这个 camera.ino 代码只是在按下按钮时捕捉图像,并按顺序将图像保存在 SD 卡中。
//camera.ino
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// 使用 XIAO_ESP32S3 模型摄像头,具备 PSRAM
// 摄像头引脚定义
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#define LED_GPIO_NUM 21
#define capturePin D0//采集按键引脚
unsigned long lastCaptureTime = 0; // 上次拍摄时间
int imageCount = 1; // 图片文件计数
bool camera_sign = false; // 摄像头状态标志
bool sd_sign = false; // SD卡状态标志
bool captureFlag = false;
// 保存图片到SD卡
void photo_save(const char * fileName) {
// 拍摄照片
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Failed to get camera frame buffer");
return;
}
// 将照片保存到文件
writeFile(SD, fileName, fb->buf, fb->len);
// 释放图像缓冲区
esp_camera_fb_return(fb);
Serial.println("Photo saved to file");
}
// 写入数据到SD卡
void writeFile(fs::FS &fs, const char * path, uint8_t * data, size_t len){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.write(data, len) == len){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
pinMode(capturePin, INPUT_PULLUP);
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.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG; // 设置为JPEG格式
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// 如果存在 PSRAM IC,则以 UXGA 分辨率和更高的 JPEG 质量启动
// 用于更大的预分配帧缓冲区。
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// 没有PSRAM时降低分辨率
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
// 人脸检测时使用的分辨率
config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
config.fb_count = 2;
#endif
}
//初始化摄像头
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
camera_sign = true; // 摄像头初始化成功标志
// 初始化SD卡
if(!SD.begin(21)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
// 检测SD卡类型
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
sd_sign = true; // SD卡初始化成功标志
Serial.println("*** XIAO ESP32S3 Spy Camera ***");
Serial.println("Press button to capture and save an image\n");
}
void loop() {
take_pic();
}
void take_pic()
{
if(camera_sign && sd_sign){
if (digitalRead(capturePin) == 0) { //检测按键是否按下
delay(200); //消抖延时
Serial.println("\nImage Captured");
char filename[32];
sprintf(filename, "/image%d.jpg", imageCount);
photo_save(filename);
Serial.printf("Saved picture:%s\n", filename);
Serial.println("");
imageCount++;
}
}
}
高效捕捉图像
在这个增强版代码中:
单击:设备捕捉图像并将其保存到 SD 卡中。每张图像都按顺序命名,可避免在相机打开(从深度睡眠模式唤醒)时覆盖之前的图像。
长按:使设备进入深度休眠状态,通过关闭不必要的进程有效节约电能。再次长按将唤醒设备,使其能够继续捕捉图像。
这一功能使设备特别适用于长期使用或在偏远地区进行图像数据采集,因为在这些地区持续供电是不现实的。这对 TinyML 图像数据采集尤其有价值,它能使设备在需要时保持休眠状态,从而大大延长了设备在野外的电池寿命。
深度睡眠模式代码
//DeepSleep.ino
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <Preferences.h>
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#define LED_GPIO_NUM 21
#define capturePin D0
#define LEDAnode D5
#define statusLED D6
#define captureLED D4
Preferences preferences; // 用于存储非易失性数值的首选项对象
unsigned long lastPressTime = 0; // 最后一次按下按钮的开始时间
unsigned long pressDuration = 0; // 按下按钮的持续时间
int imageCount = 1; // 文件计数器
bool camera_sign = false; // 检查摄像机状态
bool sd_sign = false; // 检查SD卡状态
void photo_save(const char * fileName) {
// 拍摄照片
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Failed to get camera frame buffer");
return;
}
// 将照片保存到文件
writeFile(SD, fileName, fb->buf, fb->len);
// 释放图像缓冲区
esp_camera_fb_return(fb);
Serial.println("Photo saved to file");
}
void writeFile(fs::FS &fs, const char * path, uint8_t * data, size_t len){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.write(data, len) == len){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
pinMode(capturePin, INPUT_PULLUP);
pinMode(statusLED, OUTPUT); // 将 LED 引脚初始化为输出
pinMode(LEDAnode, OUTPUT);
pinMode(captureLED, OUTPUT);
digitalWrite(LEDAnode, HIGH); //我使用的是共阳极 RGB LED
digitalWrite(captureLED, HIGH);
// 初始化首选项
preferences.begin("camera", false);
// 从非易失性存储器中读取存储的图像计数
imageCount = preferences.getInt("imageCount", 1); // 如果未设置,默认为 1
// 初始化相机
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.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
config.frame_size = FRAMESIZE_240X240;
}
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
camera_sign = true;
// Initialize SD card
if(!SD.begin(21)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
sd_sign = true;
Serial.println("*** XIAO ESP32S3 Spy Camera ***");
Serial.println("Press and hold the button to enter deep sleep.");
Serial.println("Press the button briefly to capture an image.");
esp_sleep_enable_ext0_wakeup(static_cast<gpio_num_t>(capturePin), 0);
digitalWrite(statusLED, LOW);
digitalWrite(captureLED, HIGH);
}
void loop() {
handleButtonPress();
}
void handleButtonPress() {
int buttonState = digitalRead(capturePin);
if (buttonState == LOW) {
if (lastPressTime == 0) {
lastPressTime = millis(); // 记录按压开始时间
}
pressDuration = millis() - lastPressTime;
if (pressDuration > 2000) { // 长按检测(>2 秒)
Serial.println("Long press detected: Going to deep sleep");
digitalWrite(statusLED, HIGH);
delay(1000); // 深度睡眠前去抖动的延迟
goToDeepSleep(); // 进入深度睡眠
}
} else {
if (lastPressTime > 0 && pressDuration > 100 && pressDuration < 1000) { // 短按检测
Serial.println("Short press detected: Taking picture");
digitalWrite(captureLED, LOW);
captureImage();
digitalWrite(captureLED, HIGH);
}
lastPressTime = 0; // 释放按钮时重置计时
pressDuration = 0;
}
}
void captureImage() {
char filename[32];
sprintf(filename, "/image%d.jpg", imageCount);
photo_save(filename); // 捕捉并记录图像
Serial.printf("Saved picture: %s\n", filename);
imageCount++;
// 将新的图像计数存储在非易失性存储器中
preferences.putInt("imageCount", imageCount);
}
void goToDeepSleep() {
// 睡眠前存储图像计数
preferences.putInt("imageCount", imageCount);
preferences.end(); // 关闭首选项
digitalWrite(statusLED, HIGH);
esp_deep_sleep_start(); // 进入深度睡眠
}
将代码上传到您的 Xiao ESP32S3 Sense。
初始化:在设置过程中对摄像机、SD 卡和 LED 进行初始化。图像计数从非易失性存储器中存储和检索,确保按顺序保存图像,而不会覆盖现有文件。
图像捕捉:只需按下按钮即可捕捉图像并将其保存到 SD 卡中。
深度睡眠和唤醒:使用定时机制检测长按。如果按住按钮超过 2 秒钟,设备就会进入深度睡眠状态。当设备唤醒时,LED 灯亮起,表明它已准备好再次捕捉图像。
深度睡眠功能
// 深度睡眠功能
void goToDeepSleep() {
// 保存图像计数和其他数据
preferences.putInt("imageCount", imageCount);
preferences.end(); // 关闭首选项
// Turn off LED before going to sleep
digitalWrite(LED_GPIO_NUM, LOW);
esp_deep_sleep_start(); //进入深度睡眠
}
处理按键:按钮逻辑检查短按(捕捉图像)和长按(进入深度睡眠)。
cpp
Copy code
void handleButtonPress() {
if (isButtonPressed()) {
if (lastPressTime == 0) {
lastPressTime = millis(); // 记录按压开始时间
}
pressDuration = millis() - lastPressTime;
if (pressDuration > 2000) { // 长按检测(>2 秒)
Serial.println("Long press detected: Going to deep sleep");
delay(500); // 深度睡眠前去抖延迟
// 睡眠前关闭 LED
digitalWrite(LED_GPIO_NUM, LOW);
goToDeepSleep(); // 进入深度睡眠
}
} else {
if (lastPressTime > 0 && pressDuration > 200 && pressDuration < 1000) { // 短按检测
Serial.println("Short press detected: Taking picture");
captureImage();
}
lastPressTime = 0; // 释放按钮时重置计时
pressDuration = 0;
}
}
运行情况:
上传代码并连接好一切后,使用电源按钮打开相机。按下捕捉按钮拍照图像将以唯一的顺序文件名保存在 SD 卡上。
以下是摄像机运行时串行监视器的输出结果
图 7 输出结果
图像将这样保存:
图 8 图片保存样例
图库:
图 9 微型摄像机外型图片(a)
图 10 微型摄像机外型图片(b)
相机拍摄的图像:
图 11 微型摄像机拍摄图片(a)
图 12 微型摄像机拍摄图片(b)
未来改进:
虽然该项目功能齐全,但仍有改进的余地。以下是对未来版本的一些设想:
视频录制:拓展功能,捕捉视频短片。
无限传输:整合一项功能,以无线方式将图像发送到智能手机或者电脑
移动侦测:添加移动侦测功能,在检测到移动时自动捕捉图像
图 13 微型摄像机左侧外壳
图 14 微型摄像机右侧外壳(b)
原理图
图 15 XIAO 原理图
粉丝福利
柴火创客的母公司 Seeed Studio 给大家带来了XIAO系列开发板每款产品的最低价,人民币25元即可获取xiao RP2040 可扫码查看更多惊喜
创客项目秀|基于树莓派“亚当斯一家”机械手宠物
创客项目秀|基于XIAO 的图像分类处理项目
创客项目秀|基于 Raspberry Pi 5 的蜂窝物联网
创客项目秀|基于Raspberry Pi Zero的魔法报纸
创客项目秀|基于LVGL驱动的OLED屏的FFT声音实时可视化
创客项目秀|基于XIAO ESP32C3 的智能家居四路控制器
创客项目秀|基于Grove Vision AI2模块的边缘计算机视觉项目
创客项目秀|基于XIAOESP32S3 Sense的在线语音助手
创客项目秀|基于Seeed XIAO ESP32S3 sense的HA自动化鱼缸
创客项目秀|基于XIAO ESP32 sense的宠物猫检测项目
新一代信息技术赋能|人才升级|产业创新
Seeed Studio物联网设备试用中心落地柴火!
Chaihuo x.factory|深圳,河北