笔者使用Arduino编写ESP32-CAM的驱动程序,版本为1.8.19。在较新的版本中,Arduino的UI风格发生了变化,不过下面配置的功能基本保留,读者注意辨别其中的异同之处。
1.首先,我们需要在Arduino中配置ESP32开发板的开发环境。打开Arduino,按如下路径依次点击:“文件” → \rightarrow → “首选项”,找到“附加开发板管理器网址”,如图1.1所示。
3.按照界面上“一行一个”的指示,将下面两个网址输入进去:
https://dl.espressif.com/dl/package_esp32_index.json
https://github.com/espressif/arduino-esp32/releases/download/2.0.2/package_esp32_dev_index.json
然后点击“好”即可。
1.接下来我们需要配置开发板。按如下路径依次点击:“工具” → \rightarrow → “开发板” → \rightarrow → “开发板管理器”,弹出界面如图1.3:
2.在显示有“对搜索进行过滤…”字样的搜索框内输入“ESP32”,显示界面如图1.4:
3.选择版本2.0.2,然后点击“安装”,等待其安装好即可。
4.安装好后,按如下路径点击:“工具” → \rightarrow → “开发板” → \rightarrow → “ESP32 Arduino” → \rightarrow → “AI Thinker ESP32 CAM”。至此,我们就配置好了ESP32的开发环境和开发板,可以进行下一步的开发了。
1.由于官方的库并不能驱动ESP32-CAM,因此在此处我参考了CSDN用户“ShemuelHe”的博客。博客链接为本节最后的参考资料当中的第二个链接。在此处,我们需要使用GitHub上大神yoursunny用户所提供的库。下载链接为本节参考资料的第一个链接。点进他的主页后,如图1.5所示:
2.点击“Download ZIP”,将代码压缩包下载下来。然后回到Arduino,按如下路径点击:“项目” → \rightarrow → “加载库” → \rightarrow → “添加.ZIP库”,弹出界面如图1.6所示:
3.找到刚才下载的库的路径,找到.ZIP文件(该压缩包不需要解压),选中后点击“打开”。这样,这个库就添加好了。其他项目中,如果要添加非官方库,也可以通过这样的方式。
完整Arduino代码(经过测试,直接复制可用)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "cJSON.h"
#include "FS.h"
#include "esp_camera.h"
//以下改成要连接的WiFi名称和密码
const char* WIFI_SSID = "******";
const char* WIFI_PASS = "******";
WebServer server(80);
static auto loRes = esp32cam::Resolution::find(320, 240);
static auto hiRes = esp32cam::Resolution::find(1280, 1024);
//UXGA:分辨率为1600*1200的输出格式,SXGA(1280*1024)、XVGA(1280*960)、WXGA(1280*800)、XGA(1024*768)、SVGA(800*600)、VGA(640*480)、CIF(352*288)和QQVGA(160*120)等。
char path[] = "/1.jpg";
int order = 1;
void handleBmp()
{
if (!esp32cam::Camera.changeResolution(loRes)) {
Serial.println("SET-LO-RES FAIL");
}
auto frame = esp32cam::capture();
if (frame == nullptr) {
Serial.println("CAPTURE FAIL");
server.send(503, "", "");
return;
}
Serial.printf("CAPTURE OK %dx%d %db\n", frame->getWidth(), frame->getHeight(),
static_cast(frame->size()));
if (!frame->toBmp()) {
Serial.println("CONVERT FAIL");
server.send(503, "", "");
return;
}
Serial.printf("CONVERT OK %dx%d %db\n", frame->getWidth(), frame->getHeight(),
static_cast(frame->size()));
server.setContentLength(frame->size());
server.send(200, "image/bmp");
WiFiClient client = server.client();
frame->writeTo(client);
}
void serveJpg()
{
auto frame = esp32cam::capture();
if (frame == nullptr) {
Serial.println("CAPTURE FAIL");
server.send(503, "", "");
return;
}
Serial.printf("CAPTURE OK %dx%d %db\n", frame->getWidth(), frame->getHeight(),
static_cast(frame->size()));
server.setContentLength(frame->size());
server.send(200, "image/jpeg");
WiFiClient client = server.client();
frame->writeTo(client);
}
void handleJpgLo()
{
if (!esp32cam::Camera.changeResolution(loRes)) {
Serial.println("SET-LO-RES FAIL");
}
serveJpg();
}
void handleJpgHi()
{
if (!esp32cam::Camera.changeResolution(hiRes)) {
Serial.println("SET-HI-RES FAIL");
}
serveJpg();
}
void handleJpg()
{
server.sendHeader("Location", "/cam-hi.jpg");
server.send(302, "", "");
}
void handleMjpeg()
{
if (!esp32cam::Camera.changeResolution(hiRes)) {
Serial.println("SET-HI-RES FAIL");
}
Serial.println("STREAM BEGIN");
WiFiClient client = server.client();
auto startTime = millis();
int res = esp32cam::Camera.streamMjpeg(client);
if (res <= 0) {
Serial.printf("STREAM ERROR %d\n", res);
return;
}
auto duration = millis() - startTime;
Serial.printf("STREAM END %dfrm %0.2ffps\n", res, 1000.0 * res / duration);
}
// Init SD Card
void sd_init()
{
//The argument ("/sdcard",true) means closing LED light on the board
if (!SD_MMC.begin("/sdcard",true)) {
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD_MMC.cardType();
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");
}
//Get the size of SD card, unit: MB
uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
Serial.printf("SD 卡容量大小: %lluMB\n", cardSize);
}
void setup()
{
Serial.begin(115200);
Serial.println();
{
using namespace esp32cam;
Config cfg;
cfg.setPins(pins::AiThinker);
cfg.setResolution(hiRes);
cfg.setBufferCount(2);
cfg.setJpeg(80);
bool ok = Camera.begin(cfg);
Serial.println(ok ? "CAMERA OK" : "CAMERA FAIL");
}
sd_init();
delay(5000);
WiFi.persistent(false);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
Serial.print("http://");
Serial.println(WiFi.localIP());
Serial.println(" /cam.bmp");
Serial.println(" /cam-lo.jpg");
Serial.println(" /cam-hi.jpg");
Serial.println(" /cam.mjpeg");
server.on("/cam.bmp", handleBmp);
server.on("/cam-lo.jpg", handleJpgLo);
server.on("/cam-hi.jpg", handleJpgHi);
server.on("/cam.jpg", handleJpg);
server.on("/cam.mjpeg", handleMjpeg);
server.begin();
}
void loop()
{
server.handleClient();
camera_fb_t * fb = esp_camera_fb_get();
sprintf(path,"/%d.jpg",order);
if (fb == NULL)
{
Serial.println( "Get picture failed");
}
else
{
fs::FS &fs = SD_MMC;
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file)
{
Serial.println("Failed to Create a File!");
}
else
{
file.write(fb->buf , fb->len);
}
esp_camera_fb_return(fb);
order += 1;
}
}
最终跑出来的效果如图1.7、1.8所示:
图中“图像显示”的程序是通过Python编写,这一部分涉及PyQt5的使用,将在1.2节详细介绍。
注意: ESP32-CAM的TF卡槽最多支持4G容量的TF卡,超过此容量的TF卡均不能被成功识别。
此部分参考资料如下:
1.GitHub - yoursunny/esp32cam: OV2640 camera on ESP32-CAM, Arduino library / https://github.com/yoursunny/esp32cam
获取视频流的ESP32代码包
2.【ESP32-CAM】使用opencv获取ESP32-CAM视频流(一)_ShemuelHe的博客-CSDN博客_esp32移植opencv / https://blog.csdn.net/ShemuelHe/article/details/121365730?utm_source=app&app_version=5.0.1&code=app_1562916241&uLinkId=usr1mkqgl919blen
WiFi获取视频流,python openCV实现视频流获取
3.(ESP32学习16)ESP32_CAM获取图片并且保存文件名为当前时间_bird1999625的博客-CSDN博客 / https://blog.csdn.net/ailta/article/details/106866261
CAM摄像头拍摄图像并保存至TF卡
在1.1.4节中介绍了电脑端读取URL图像,获取视频流的效果。该节中的参考链接2中提供了OpenCV的方式来读取视频流。而由于我们需要将程序移植到不同的电脑上使用,因此需要将Python脚本打包成.exe执行文件。这一节将介绍如何使用Python读取URL图像并将整个程序打包成.exe可执行文件,使其在没有安装Python开发环境的电脑上也能运行。
此部分内容在网上有很多详细的资料,此处不再赘述,可参考本节末尾的参考资料当中的第一个链接,相当详细,将每一步都列举了出来,按操作即可成功安装。
1.打开PyCharm,新建工程和py脚本,然后按照如下路径依次点击:“文件” → \rightarrow → “设置” → \rightarrow → “项目” → \rightarrow → “Python解释器”,界面如图1.9所示:
3.在左上角画红色线的搜索栏中输入“PyQt5”;然后选中右下角橙色线处“指定版本”,选择最新的版本;最后点击左下角红圈圈住的“安装软件包”,等待安装这个包即可。
4.同样,在此工程中,需要安装“PyQt5-tools”包。操作方法同上。
这两个包安装在路径“\UITest\venv\Lib\site-packages”中。
1.按如下路径依次点击:“文件” → \rightarrow → “设置” → \rightarrow → “项目” → \rightarrow → “工具” → \rightarrow → “外部工具” ,进入如下界面:
3.在“名称”栏中,输入外部工具的名字,在这里我们将其命名为“QtDesigner”;在“程序栏”中,输入“designer.exe”的路径;在“工作目录”栏中,输入“$FileDir$”。其中,“designer.exe”的路径如下:
4.同样,我们需要添加外部工具“pyuic5.exe”程序。该程序将QtDesigner中设计好的UI界面转化成Python脚本,供我们编程开发使用。在“外部工具”中,再点一次“+”,将该工具添加进来:
我们将该工具命名为PyUIC,“程序”一栏添加pyuic5.exe文件的路径;“实参”一栏添加如下信息:$FileName$ -o $FileNameWithoutExtension$.py;“工作目录”一栏添加:$ProjectFileDir$。然后点击“确定”。这样,我们就添加好了我们所需要的外部工具。
在1.2.3节中,我们下载好了开发.exe文件所需要的软件包,并添加好了外部工具。至此,准备工作已经全部完成,我们可以开始使用QtDesigner来开发我们的软件了。
1.打开Qt:在PyCharm顶端的菜单栏中,按照如下顺序点击:“工具” → \rightarrow → “External tools” → \rightarrow → “QtDesigner”:
左侧为常用的一些控件树;中间的部分为设计工具提供给我们的一些模板;右侧为控件树和当前选中的控件的一些属性。在这里,我们选择“Main Window”,点击“Create”,界面如图1.21所示:
这个时候,我们就可以添加各种控件并给它们配置属性,以达到我们的目的。
2.关于如何布局,读者可参考白月黑羽的教程,相当详细Python Qt 图形界面编程 - PySide2 PyQt5 PyQt PySide_哔哩哔哩_bilibili / https://www.bilibili.com/video/BV1cJ411R7bP?spm_id_from=333.999.0.0,此文档中不再赘述。读者需要尤其注意Layout的使用。本项目中需要使用PyQt5开发的程序较为简单,就是读取URL获取视频流,因此,笔者所设计的UI布局如图1.22所示:
在这个界面中,我使用了一个Label控件用来显示图像,三个按钮控件来触发事件,一个文本框用来输入URL地址。控件添加完成后,使用Layout进行布局。
在QtDesigner中,我们可以通过QSS的方式来美化控件。此处我以按钮控件为例,简要介绍QSS的使用。
1.选中按钮控件,在属性栏中,找到“Qwidget” → \rightarrow → “Stylesheet”,点击“StyleSheet”右侧的三点按钮:
2.在这个编辑框里,我们可以输入如下格式的代码:
QPushButton {// 按钮一般属性
background-color: white ;
font-size:16px;
color:black;
border-radius: 15px;//圆角半径
font-family:微软雅黑;//字体
background:rgb(255, 255, 255);//背景颜色
border:2px solid black;//边框宽度
}
QPushButton:hover{ //鼠标悬浮在按钮上时按钮的属性
background:rgb(237, 108, 0, 150);//鼠标悬浮时背景颜色为rgb(237,108,0,150)
}
QPushButton:pressed{ //鼠标按下时按钮的属性
background:white;//按下鼠标时背景颜色为白色
}
3.然后点击“OK”即可。这个时候,控件的外观就会按照我们代码所设定的样子显示出来。
设计好UI界面后,我们就需要将UI文件转化成.py文件,在PyCharm编辑器中编写程序了。
1.在左侧的文件预览器中,找到我们的.ui文件,右键单击,然后左键依次点击:“External tools” → \rightarrow → “PyUIC”:
2.点击后,会生成一个和.ui文件同名的.py文件。控制台和文件树如下所示:
3.双击打开VideoShow.py文件,这时候编辑器就会显示我们所创建的UI对应的.py文件代码了。
当我们有了.ui文件对应的.py文件后,我们就需要将这个ui使用代码运行起来,最终实现我们想要的功能。在这一节,我将以项目中的图像显示为例,简要介绍如何将我们创建的ui在PyCharm中运行出来。
1.在.ui文件和刚才生成的.py文件同一个文件夹下新建一个.py文件,在这里,我将其命名为“Test.py”。
2.添加如下代码:
import sys
import requests
import VideoShow
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtGui import QImage, QPixmap, QIcon
class MainWindows(QMainWindow, VideoShow.Ui_ShowVideo):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainWindows()
# 设置软件窗口的名字
main.setWindowTitle('Submarine 图像显示')
# 设置软件的图标。注意,"icon_16.png"文件需要和工程在同一文件夹下。
main.setWindowIcon(QIcon("icon_16.png"))
# 显示UI
main.show()
sys.exit(app.exec_())
在1.2.7节,我们成功地将我们的UI界面运行了起来。但是,这个时候这个界面是没有什么用的——我们还没有给相关控件添加功能代码,使它们发挥各自的作用。在这里,我们就需要给控件编写信号和槽函数的相关代码了。关于信号和槽的基本概念,在1.2.4节的链接中也有较为详细的介绍。简单来说,当我们点击按钮时,发送一个信号,这个信号被连接到一个槽函数当中,该函数就会执行相应的功能代码。在这个工程中,我们需要给三个按钮编写槽函数,并且开启一个定时器,每隔一段时间读取一次URL。在这一节中,我将简单介绍如何编写功能代码。
1.控件命名
在QtDesigner中的控件树中,我们可以给我们的控件命名:
例如此处,我将按钮控件分别命名为“CloseAppButton”、“CloseVideoButton”、“OpenVideoButton”。在PyUIC生成的代码中,我们如果想要调用这几个控件,就需要调用这些变量名。
2.槽函数的编写
首先,我们编写一下“打开视频”这个按钮的槽函数。代码如下:
def onOpenVideoButtonClicked(self):
self.timer.start(20) # 设置计时间隔并启动,间隔20ms
self.VideoShowLabel.setScaledContents(True)
在这个函数中,我们将定时器启动,并设置读取时间间隔为20ms;设置显示图像的Label控件为自适应图片大小。那么,这个槽函数名就叫做“onOpenVideoButtonClicked”,“打开视频”按钮被按下后,程序就会执行该函数当中的代码。
3.将信号连接到槽函数上
编写好了槽函数后,我们需要将信号连接到槽函数上。比如,我们需要将“打开视频”按钮被点击的信号连接到刚才我们编写的槽函数上,以使槽函数执行功能。连接信号的代码如下:
self.OpenVideoButton.clicked.connect(self.onOpenVideoButtonClicked)
这样,当我们运行UI后,点击这些控件,程序就会执行相应的功能了。
4.整个App的代码如下:
import sys
import requests
import VideoShow
from PyQt5.QtGui import QImage, QPixmap, QIcon
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QTimer
class MainWindows(QMainWindow, VideoShow.Ui_ShowVideo):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
# 初始化一个定时器
self.timer = QTimer(self)
# 计时结束调用operate()方法
self.timer.timeout.connect(self.operate)
# 将开启信号与槽函数关联
self.OpenVideoButton.clicked.connect(self.onOpenVideoButtonClicked)
self.CloseVideoButton.clicked.connect(self.onCloseVideoButtonClicked)
self.CloseAppButton.clicked.connect(self.onCloseAppButtonClicked)
def onOpenVideoButtonClicked(self):
self.timer.start(20) # 设置计时间隔并启动,间隔20ms
self.VideoShowLabel.setScaledContents(True)
def onCloseVideoButtonClicked(self):
self.timer.stop() # 关闭定时器
def onCloseAppButtonClicked(self):
self.timer.stop() # 关闭定时器
self.close() # 关闭应用程序
# 定时器的处理函数
def operate(self):
url = self.URLEdit.text() # 获取URL
# print(url)
res = requests.get(url)
img = QImage.fromData(res.content)
self.VideoShowLabel.setPixmap(QPixmap.fromImage(img))
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainWindows()
main.setWindowTitle('Submarine 图像显示')
main.setWindowIcon(QIcon("icon_16.png"))
main.show()
sys.exit(app.exec_())
这样,我们就编写好了一个简单的UI界面。
1.和1.2.2节安装PyQt5包一样,我们需要将“pyinstaller”这个包下载下来:
点击“安装软件包”,等待这个包安装好即可。
3.然后在上面的窗口中输入如下指令:
pyinstaller -F -w -i icon.ico Test.py
其中,“-F”表示打包后只生成一个.exe文件(也可以理解为覆盖掉之前产生的同名.exe文件);“-w”表示不使用控制台;“-i”表示改变生成的.exe文件的图标,后面要跟上图标文件的文件名和格式。一般这里支持.ico格式,读者可以在PhotoShop中制作好自己的图标并将其放在工程文件夹下。最后,添加上我们要打包的.py文件名。
常用选项及说明如下:
5.打开我们的工程文件夹下的“dist”文件夹,如下图所示:
可以看到我们刚才生成的.exe文件了。将其复制到我们存放.ico文件的文件夹中(否则图标将不会显示)并双击打开,程序就可以正常运行了:
至此,我们就制作完成了图像传输的简单.exe程序。该程序可以在没有安装python环境的计算机中运行。
此部分参考资料如下:
1.PyCharm2021安装教程_学习H的博客-CSDN博客_pycharm2021安装教程
2. 将python程序打包成exe_蹦跶的小羊羔的博客-CSDN博客_python打包成exe
3.Python Qt 图形界面编程 - PySide2 PyQt5 PyQt PySide_哔哩哔哩_bilibili
4.Python Qt 简介 | 白月黑羽 (byhy.net)