DIY 定时摄影相机
使用经济实惠的ESP32平台拍摄周期性照片,制作延时摄影视频。
引言
ESP32平台为实现物联网(Internet of Things)应用提供了强大的平台,用于感知数据和控制现实世界中的事物。在此应用中,我们使用相机拍摄周期性照片,并将其存储在SD卡上,或通过WiFi发送到某个远程服务器。
如果需要,还可以远程处理这些图像以识别事件,或将它们合并成视频(例如,搜索“从图像创建视频”会发现Python和OpenCV中有许多示例)。
示例延时摄影视频 以2分钟间隔拍摄的照片。
背景
该应用程序在启动时通过读取存储在ESP32闪存文件系统或SD卡上的配置文件来配置自身。一旦获得配置信息,ESP32将进入深度睡眠模式,偶尔唤醒拍照,然后将其本地存储或通过WiFi上的FTP上传到远程服务器。
配置文件包含以下信息:
- 如何连接到WiFi的信息(SSID和密码)
- 如何连接到FTP主机的信息(如果需要)(主机名、用户名、密码和FTP文件夹)
- 拍照频率的信息 - 每天拍摄多少张照片
- 关于是将照片本地存储在SD卡上,还是FTP传输到某个服务器的信息
架构该应用程序的一个驱动因素是支持电池供电模式。为了降低功耗,ESP32系统大部分时间处于深度睡眠模式,仅在需要拍照时偶尔唤醒。
另一个考虑因素是支持没有可用WiFi的模式(类似野外相机模式),照片存储在本地SD卡上。在这种模式下,设备可以一次运行数天或数周,每天只拍几张照片。例如,可以将其放置在森林中的某个偏远位置,甚至放置在防水容器中进行水下观察,以监测缓慢变化的现象。
硬件架构
硬件基于ESP32平台,该平台非常紧凑且价格低廉。亚马逊上有包含相机模块的开发板,两块约20美元。这些板子同时包含相机和用于存储照片的SD卡插槽。搜索亚马逊上的“ESP32 Camera”应该就能找到。还有许多其他来自不同来源的ESP32相机模块。
该应用不需要对板子进行任何硬件添加或修改 - 只需将代码编程到模块中,并确保启动时有一个有效的配置文件在闪存中可供读取。如果需要,请安装SD卡来存储图像。
供电方面,这些板子需要5V电源 - 可以直接从USB端口供电,也可以从常见的充电宝中供电,这些充电宝通常用于给手机充电。
有人告诉我,ESP32-CAM板子的设计经常被低成本制造商克隆 - 所以要注意,一些制造商可能存在质量问题。我从亚马逊购买了两块上述板子 - 幸运的是,它们似乎都能正常工作。
软件架构
由于此应用程序使用了ESP32的“深度睡眠”模式,它在某种程度上与普通的ESP32应用程序不同。众所周知,这些应用程序通常有一个setup()
函数,该函数在启动时运行一次,以及一个loop()
函数,该函数在启动后连续运行。
在深度睡眠应用程序中,loop()
函数中不放置任何代码,因为ESP32在没有照片需要拍摄时会处于深度睡眠模式。所以loop()
函数是空的。
然而,所有的操作都发生在setup()
函数中。该函数在首次启动时执行,并在ESP32从深度睡眠模式唤醒时每次执行。
深度睡眠模式
ESP32平台包含多种“模式”,在运行时使用不同程度的功耗。正常或活动模式意味着ESP32持续运行其主CPU。这是功耗最高的模式,因为芯片的大部分功能都处于运行状态 - CPU、内存、闪存、定时器、GPIO/端口,甚至可能包括无线电(WiFi或蓝牙)。
为了节约电力,可以使用一些低功耗模式 - 这基本上意味着芯片的一部分被断电,并且某些功能和特性无法使用。
在活动模式下,通过在不需要无线电功能时将其关闭,可以实现一定的节电。此外,CPU时钟可以“降低”或以较慢的速率运行。这会导致芯片执行速度变慢,但会降低活动模式下的功耗。
除了活动模式,还有大约五种不同的省电模式 - 太多了,无法在此一一描述。芯片制造商在此 提供了这些模式的良好描述。
对于本应用,我们选择了(几乎)最低功耗的模式 - 约10微安(仅芯片本身) - 称为深度睡眠模式。(**注意**:我测量到在深度睡眠模式下,整个板子从5V电源供电时功耗为20ua)。在此模式下,几乎所有芯片的功能都已断电。唯一仍然激活的部分是
- RTC控制器
- ULP协处理器
- RTC快速内存
- RTC慢速内存
这意味着CPU、RAM、几乎所有外设都已断电。在此深度睡眠模式下,功耗降低到微瓦级别。
在此模式下“唤醒”的唯一设备是一小部分特殊RAM内存和一些电路,用于跟踪当前时间以及芯片已进入深度睡眠多长时间 - 还有一个用于外部信号的GPIO。
要从深度睡眠模式恢复,我们需要“唤醒”CPU - 这意味着重新进入活动模式。然而,在深度睡眠模式下,CPU和内存系统已断电 - 因此不会执行任何指令,也不会保留任何RAM内存内容。当然,存储程序本身的闪存会得到保留,尽管在深度睡眠模式下它已断电。
“唤醒”意味着将CPU从完全断电模式重新启动的过程。在此启动过程中,代码需要知道它是由于所谓的冷启动 - 意味着系统刚刚通电并首次启动,还是由于低功耗模式(如深度睡眠)的唤醒事件而启动。
幸运的是,制造商提供了特殊的IO寄存器,可以读取以确定正在发生哪种类型的启动。然后,代码可以根据从此启动原因寄存器读取的内容以不同的方式处理启动过程。
下面,您将看到在每次启动或唤醒周期执行的setup()
函数开头的代码。这里有一个对函数esp_sleep_get_wakeup_cause()
的调用,以获取描述正在进行的唤醒类型的代码。稍后在setup
函数中,我们将使用此代码来确定要执行的唤醒处理类型。
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
//----------------------------------------------------------------------
// Start out with things we want to do every time we wake up or boot up
//----------------------------------------------------------------------
// let's see the reason that the ESP32 woke up
esp_sleep_wakeup_cause_t wakeup_reason; // to see why we booted
// (either power on, or wake up)
wakeup_reason = esp_sleep_get_wakeup_cause();
#ifdef WANTSERIAL
Serial.begin(115200);
print(SIGNON);
#endif
注意:在使用5V电源时,我在深度睡眠模式下测得整个板子的电流消耗为20 ua(0.02ma)。
冷启动处理
对于冷启动情况,此架构需要执行一些不同的操作。特别是,我们需要读取配置文件并将值存储在能够承受深度睡眠模式的特殊内存中。大多数内存内容在从深度睡眠唤醒时会丢失,因为RAM内存系统已关闭以节省电力。然而,芯片组提供了一小部分内存(我猜是8192字节),在深度睡眠事件期间可以保留其内容。
因此,冷启动期间的首要任务是从配置文件中读取所需值,并将其存储在此深度睡眠保护内存中。这并非完全必要,因为配置文件存储在闪存中,每次唤醒时都可以读取,但这会导致CPU每次从深度睡眠唤醒时执行可能数百万条指令。因此,为了节省电力,代码将在冷启动时一次性读取配置文件,并将所需值存储在此保护内存中,以便在从深度睡眠唤醒时可以轻松访问。
我们还尝试在冷启动序列中确定正确的当前时间。这意味着我们需要连接到互联网并查询网络时间协议(NTP)服务器以获取当前时间和日期。然后将这些数据编程到芯片上的实时时钟(RTC)中 - 幸运的是,RTC在深度睡眠模式期间保持运行,所以我们实际上只需要在冷启动时执行一次此操作。
这里的想法是,对于“离网”应用(没有WiFi可用),我们可以在有WiFi的区域冷启动一次,然后系统就会知道时间,即使它被移出WiFi范围。
这是执行冷启动任务的代码 - 您会看到它正在从配置文件读取键,并且它还正在连接到WiFi以尝试通过NTP读取当前时间/日期。
//-------------------------------------------------------------
// Boot up tasks - Stuff to do on an initial boot from power
// on or hard reset
//-------------------------------------------------------------
if ((bootCount == 0) || (wakeup_reason != ESP_SLEEP_WAKEUP_TIMER))
{
// read needed data from the config file - stored in RTC memory
// so it survives deep sleep
readKey(CONFIGFN,"SSID=",ssid,127);
readKey(CONFIGFN,"PASSWORD=",password,127);
readKey(CONFIGFN,"SERVER=",uploadHost,127);
readKey(CONFIGFN,"FTPUSER=",ftpUser,127);
readKey(CONFIGFN,"FTPPASSWORD=",ftpPswd,127);
readKey(CONFIGFN,"FTPFOLDER=",ftpFolder,127);
readKey(CONFIGFN,"PHOTOSPERDAY=",buf,30);
delayBetweenPhotosMs=24*60*60*1000/atol(buf);
if (delayBetweenPhotosMs < 60*1000)
delayBetweenPhotosMs = 60*1000;
sprintf(buf,"Delay=%d",delayBetweenPhotosMs);
print(buf);
// initialize the RTC and read network time if possible
rtc.setTime(0,0,0,1,1,2023); // default time 00:00:00 1/1/2023
if (connectToWiFi() == 1)
{
needNtp = true;
getNtpTime(); // get NTP time if possible
}
turnOffWiFi(); // now we can turn off the WiFi modem to save power
}
唤醒处理
当从深度睡眠模式唤醒时,我们不需要读取配置文件,因为从中需要的数据已存储在深度睡眠保护内存(也称为RTC内存)中。在此步骤中,我们要做的就是拍照,然后将其存储在SD卡上,或将其上传到请求的FTP服务器进行存储。
相机通过多针连接连接到ESP32 - 在此过程中使用了处理器的许多IO引脚。对于不同的ESP32开发板,实际使用的引脚可能会有所不同 - 因此,虽然设置相机、拍照和检索照片的代码对于同一相机可能是相同的,但实际连接到ESP32芯片的引脚可能会有所不同。此示例中的代码是为Aideepen相机连接实现设置的,如果您使用不同类型的板子,可能需要对代码进行一些更改,以适应相机连接方式的差异。
您将在下面的代码段中看到,如果wakeup_reason
代码是深度睡眠唤醒,我们将设置相机并拍照(下面的代码中的setupMeasurement()
和makeMeasurement()
)。
//-------------------------------------------------------------
// *** wakeup from deep sleep tasks ***
//-------------------------------------------------------------
if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER)
{
// do wakeup tasks here (IoT Loop)
setupMeasurement();
makeMeasurement();
}
进入深度睡眠模式
有一些IO寄存器可以将ESP32带回深度睡眠模式。有几种方法可以从深度睡眠模式唤醒,这些被称为唤醒触发器。例如,ESP32可以配置为在一定时间后唤醒,或者通过向IO引脚施加外部信号唤醒,或者有人触摸屏幕唤醒。
对于本应用,我们希望在下次拍照时间到来之前休眠,因此我们将ESP32编程为休眠一段时间。时间段是两次拍照之间的时间减去ESP32唤醒并拍照的上次时间。幸运的是,ESP32提供了一种方法来查看自上次启动或唤醒以来经过了多少毫秒。因此,我们可以取所需的拍照间隔(例如一小时或其他时间),减去ESP32上次唤醒后激活的时间。这个时间是以微秒计算的,然后编程到RTC中,然后再进入深度睡眠模式,以便ESP32在下次拍照时间到来时唤醒。当代码最终写入寄存器进入深度睡眠模式时,CPU将停止,直到发生唤醒事件,不会执行任何其他代码。
在下面的代码段中,您会看到我们计算启动次数(代码会记录启动/唤醒次数)。关闭LED以指示我们即将返回睡眠。
然后,我们计算需要睡眠的毫秒数直到下一次拍照时间,并告诉ESP32进入深度睡眠模式(esp_deep_sleep_start()
)。ESP32处理器将永远不会从这个调用返回,因为它已经进入深度睡眠模式。当它被唤醒时,它基本上会重启,并且setup()
函数会再次执行。所以,这标志着本周期应用程序执行的结束。
对于冷启动后的第一个睡眠周期,第一个睡眠时间进行了调整,以便ESP32能够以每天请求的照片数量的偶数间隔唤醒。例如,如果配置文件请求每天拍摄24张照片,那就是每小时一张。第一个睡眠周期将被调整,以便ESP在整点时唤醒并拍摄第一张照片。
//-----------------------------------
// *** and go back to sleep here
//-----------------------------------
long milliseconds = millis(); // how long have we been awake?
sprintf(buf,"Awake for %d ms",milliseconds);
print(buf);
long tts = (delayBetweenPhotosMs - milliseconds) * ms_TO_S_FACTOR;
esp_sleep_enable_timer_wakeup(tts); // how long to sleep
esp_deep_sleep_start(); // good night! Have a nice nap.
// and, the ESP32 never executes any code past the deep_sleep_start!
// on next wakeup or reset, the startup() function will be called again.
杂项支持代码
以下库在代码中用于处理应用程序功能:
- NTP库 - 网络时间协议库,用于从网络查询当前时间和日期
- WIFI库 - 用于作为客户端连接到WiFi接入点、获取IP地址和访问网络
- FTP库 - 用于通过WiFi网络连接将照片发送到远程服务器
- Camera库 - 用于配置相机和捕获照片。
- RTC和时间/日期 - 实时时钟支持,用于设置/获取时间和日期,并随着时间的推移保持时间和日期的准确性
- 文件系统 - 访问SD卡上的文件、读取配置文件、写入图片文件
源代码可以在 github 上找到。
用于从图像创建AVI的Python代码
以下Python代码可以读取此项目生成的闪存卡上的所有图像,并使用Python OpenCV库将它们转换为视频(AVI)。
# -*- coding: utf-8 -*-
"""
Create Video file based on sequence of images.
credit:
https://theailearner.com/2018/10/15/creating-video-from-images-using-opencv-python/
Created on Mar 9, 2023
updated Jan 13, 2024
@author: deangi
"""
import cv2
import glob
jpg_folder='h:\\*.jpg' # point this to the folder which has the jpg images
output_avi='myvideo.avi' # point this to where you want the AVI written
# find file names of all the images
names=glob.glob(jpg_folder)
print(len(names)," files in folder ",jpg_folder)
# create avi writer
img = cv2.imread(names[0]) # read first image to get the size
height, width, layers = img.shape
size = (width,height)
out = cv2.VideoWriter(output_avi,cv2.VideoWriter_fourcc(*'DIVX'), 15, size)
# read each image and write it to the avi stream
for imgname in names:
img = cv2.imread(imgname)
out.write(img); # write to avi stream
out.release() # finally, lets close the avi writer
print('AVI written, Video size',size)
未来更新
未来,我想将其变成一个“野外相机”——即一个传感器可以检测动物(无论是否为人类)的存在,然后唤醒拍照。有几种检测生物的方法 - 被动红外传感器、雷达传感器、激光雷达传感器。
此外,我们还可以轻松添加代码来感知其他量,如光照水平、温度、压力、湿度 - 如果用户应用感兴趣,这些都可以被记录下来。
另一个想法是在配置文件中添加一个键来指定“排除的小时” - 例如,如果您正在拍摄白天现象,那么在夜间拍照可能没有用。
历史
- 版本1.0,2024年1月5日 - 初始版本
- 版本1.1,2024年1月10日 - 测量到Aideepen板在深度睡眠模式下整个板子的电流消耗为20ua