1.ESP32-CAM WiFi获取视频流以及保存图像到TF卡
1.1 驱动ESP32-CAM
笔者使用Arduino编写ESP32-CAM的驱动程序,版本为1.8.19。在较新的版本中,Arduino的UI风格发生了变化,不过下面配置的功能基本保留,读者注意辨别其中的异同之处。
1.1.1 在Arduino中配置开发环境
首先,我们需要在Arduino中配置ESP32开发板的开发环境。打开Arduino,按如下路径依次点击:“文件” → \rightarrow → “首选项”,找到“附加开发板管理器网址”,如图1.1所示。
按照界面上“一行一个”的指示,将下面两个网址输入进去:
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.1.2 在Arduino中配置开发板
接下来我们需要配置开发板。按如下路径依次点击:“工具” → \rightarrow → “开发板” → \rightarrow → “开发板管理器”,弹出界面如图1.3:
在显示有“对搜索进行过滤…”字样的搜索框内输入“ESP32”,显示界面如图1.4:
选择版本2.0.2,然后点击“安装”,等待其安装好即可。
安装好后,按如下路径点击:“工具” → \rightarrow → “开发板” → \rightarrow → “ESP32 Arduino” → \rightarrow → “AI Thinker ESP32 CAM”。至此,我们就配置好了ESP32的开发环境和开发板,可以进行下一步的开发了。
1.1.3 驱动ESP32-CAM
由于官方的库并不能驱动ESP32-CAM,因此在此处我参考了CSDN用户“ShemuelHe”的博客。博客链接为本节最后的参考资料当中的第二个链接。在此处,我们需要使用GitHub上大神yoursunny用户所提供的库。下载链接为本节参考资料的第一个链接。点进他的主页后,如图1.5所示:
点击“Download ZIP”,将代码压缩包下载下来。然后回到Arduino,按如下路径点击:“项目” → \rightarrow → “加载库” → \rightarrow → “添加.ZIP库”,弹出界面如图1.6所示:
找到刚才下载的库的路径,找到.ZIP文件(该压缩包不需要解压),选中后点击“打开”。这样,这个库就添加好了。其他项目中,如果要添加非官方库,也可以通过这样的方式。
1.1.4 完整的驱动代码(WiFi接入热点、将视频流通过WiFi发送、识别TF卡、存储照片到TF卡)
完整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 Cardvoid 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读取URL获取视频流
图中“图像显示”的程序是通过Python编写,这一部分涉及PyQt5的使用,将在1.2节详细介绍。
保存拍摄的照片到TF卡
注意: ESP32-CAM的TF卡槽最多支持4G容量的TF卡,超过此容量的TF卡均不能被成功识别。
此部分参考资料如下:
GitHub - yoursunny/esp32cam: OV2640 camera on ESP32-CAM, Arduino library / https://github.com/yoursunny/esp32cam
获取视频流的ESP32代码包
【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实现视频流获取
(ESP32学习16)ESP32_CAM获取图片并且保存文件名为当前时间_bird1999625的博客-CSDN博客 / https://blog.csdn.net/ailta/article/details/106866261
CAM摄像头拍摄图像并保存至TF卡
1.2 Python PyQt 5编写图像显示的.exe程序
在1.1.4节中介绍了电脑端读取URL图像,获取视频流的效果。该节中的参考链接2中提供了OpenCV的方式来读取视频流。而由于我们需要将程序移植到不同的电脑上使用,因此需要将Python脚本打包成.exe执行文件。这一节将介绍如何使用Python读取URL图像并将整个程序打包成.exe可执行文件,使其在没有安装Python开发环境的电脑上也能运行。
1.2.1 安装Python
此部分内容在网上有很多详细的资料,此处不再赘述,可参考本节末尾的参考资料当中的第一个链接,相当详细,将每一步都列举了出来,按操作即可成功安装。
1.2.2 安装PyQt5包
打开PyCharm,新建工程和py脚本,然后按照如下路径依次点击:“文件” → \rightarrow → “设置” → \rightarrow → “项目” → \rightarrow → “Python解释器”,界面如图1.9所示:
点击红圈当中的“+”按钮,进入下面的界面:
在左上角画红色线的搜索栏中输入“PyQt5”;然后选中右下角橙色线处“指定版本”,选择最新的版本;最后点击左下角红圈圈住的“安装软件包”,等待安装这个包即可。
同样,在此工程中,需要安装“PyQt5-tools”包。操作方法同上。
安装好了之后,找到这两个包的安装位置。笔者的位置如下:
这两个包安装在路径“\UITest\venv\Lib\site-packages”中。
1.2.3 添加外部工具
按如下路径依次点击:“文件” → \rightarrow → “设置” → \rightarrow → “项目” → \rightarrow → “工具” → \rightarrow → “外部工具” ,进入如下界面:
点击上图中左上角的“+”,弹出如下窗口:
在“名称”栏中,输入外部工具的名字,在这里我们将其命名为“QtDesigner”;在“程序栏”中,输入“designer.exe”的路径;在“工作目录”栏中,输入“$FileDir$”。其中,“designer.exe”的路径如下:
输入好后,界面如下:
同样,我们需要添加外部工具“pyuic5.exe”程序。该程序将QtDesigner中设计好的UI界面转化成Python脚本,供我们编程开发使用。在“外部工具”中,再点一次“+”,将该工具添加进来:
我们将该工具命名为PyUIC,“程序”一栏添加pyuic5.exe文件的路径;“实参”一栏添加如下信息:$FileName$ -o $FileNameWithoutExtension$.py;“工作目录”一栏添加:$ProjectFileDir$。然后点击“确定”。这样,我们就添加好了我们所需要的外部工具。
1.2.4 QtDesigner的使用
在1.2.3节中,我们下载好了开发.exe文件所需要的软件包,并添加好了外部工具。至此,准备工作已经全部完成,我们可以开始使用QtDesigner来开发我们的软件了。
打开Qt:在PyCharm顶端的菜单栏中,按照如下顺序点击:“工具” → \rightarrow → “External tools” → \rightarrow → “QtDesigner”:
打开后,界面如图1.20所示:
左侧为常用的一些控件树;中间的部分为设计工具提供给我们的一些模板;右侧为控件树和当前选中的控件的一些属性。在这里,我们选择“Main Window”,点击“Create”,界面如图1.21所示:
这个时候,我们就可以添加各种控件并给它们配置属性,以达到我们的目的。
关于如何布局,读者可参考白月黑羽的教程,相当详细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进行布局。
1.2.5 美化控件(QSS)
在QtDesigner中,我们可以通过QSS的方式来美化控件。此处我以按钮控件为例,简要介绍QSS的使用。
选中按钮控件,在属性栏中,找到“Qwidget” → \rightarrow → “Stylesheet”,点击“StyleSheet”右侧的三点按钮:
弹出界面如图1.24所示:
在这个编辑框里,我们可以输入如下格式的代码:
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;//按下鼠标时背景颜色为白色}
然后点击“OK”即可。这个时候,控件的外观就会按照我们代码所设定的样子显示出来。
1.2.6 将UI转化成.py文件
设计好UI界面后,我们就需要将UI文件转化成.py文件,在PyCharm编辑器中编写程序了。
在左侧的文件预览器中,找到我们的.ui文件,右键单击,然后左键依次点击:“External tools” → \rightarrow → “PyUIC”:
点击后,会生成一个和.ui文件同名的.py文件。控制台和文件树如下所示:
双击打开VideoShow.py文件,这时候编辑器就会显示我们所创建的UI对应的.py文件代码了。
1.2.7 运行出ui
当我们有了.ui文件对应的.py文件后,我们就需要将这个ui使用代码运行起来,最终实现我们想要的功能。在这一节,我将以项目中的图像显示为例,简要介绍如何将我们创建的ui在PyCharm中运行出来。
在.ui文件和刚才生成的.py文件同一个文件夹下新建一个.py文件,在这里,我将其命名为“Test.py”。
添加如下代码:
import sysimport requestsimport VideoShowfrom PyQt5.QtWidgets import QMainWindow, QApplicationfrom PyQt5.QtGui import QImage, QPixmap, QIconclass 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_())
运行上述代码,我们就可以看到我们刚才设计的UI界面了:
1.2.8 信号和槽函数
在1.2.7节,我们成功地将我们的UI界面运行了起来。但是,这个时候这个界面是没有什么用的——我们还没有给相关控件添加功能代码,使它们发挥各自的作用。在这里,我们就需要给控件编写信号和槽函数的相关代码了。关于信号和槽的基本概念,在1.2.4节的链接中也有较为详细的介绍。简单来说,当我们点击按钮时,发送一个信号,这个信号被连接到一个槽函数当中,该函数就会执行相应的功能代码。在这个工程中,我们需要给三个按钮编写槽函数,并且开启一个定时器,每隔一段时间读取一次URL。在这一节中,我将简单介绍如何编写功能代码。
控件命名
在QtDesigner中的控件树中,我们可以给我们的控件命名:
例如此处,我将按钮控件分别命名为“CloseAppButton”、“CloseVideoButton”、“OpenVideoButton”。在PyUIC生成的代码中,我们如果想要调用这几个控件,就需要调用这些变量名。
槽函数的编写
首先,我们编写一下“打开视频”这个按钮的槽函数。代码如下:
def onOpenVideoButtonClicked(self): self.timer.start(20) # 设置计时间隔并启动,间隔20ms self.VideoShowLabel.setScaledContents(True)
在这个函数中,我们将定时器启动,并设置读取时间间隔为20ms;设置显示图像的Label控件为自适应图片大小。那么,这个槽函数名就叫做“onOpenVideoButtonClicked”,“打开视频”按钮被按下后,程序就会执行该函数当中的代码。
将信号连接到槽函数上
编写好了槽函数后,我们需要将信号连接到槽函数上。比如,我们需要将“打开视频”按钮被点击的信号连接到刚才我们编写的槽函数上,以使槽函数执行功能。连接信号的代码如下:
self.OpenVideoButton.clicked.connect(self.onOpenVideoButtonClicked)
这样,当我们运行UI后,点击这些控件,程序就会执行相应的功能了。
整个App的代码如下:
import sysimport requestsimport VideoShowfrom PyQt5.QtGui import QImage, QPixmap, QIconfrom PyQt5.QtWidgets import QMainWindow, QApplicationfrom PyQt5.QtCore import QTimerclass 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.2.9 封装打包
和1.2.2节安装PyQt5包一样,我们需要将“pyinstaller”这个包下载下来:
点击“安装软件包”,等待这个包安装好即可。
在控制台下端找到“终端”并点击,出现如下界面:
然后在上面的窗口中输入如下指令:
pyinstaller -F -w -i icon.ico Test.py
其中,“-F”表示打包后只生成一个.exe文件(也可以理解为覆盖掉之前产生的同名.exe文件);“-w”表示不使用控制台;“-i”表示改变生成的.exe文件的图标,后面要跟上图标文件的文件名和格式。一般这里支持.ico格式,读者可以在PhotoShop中制作好自己的图标并将其放在工程文件夹下。最后,添加上我们要打包的.py文件名。
常用选项及说明如下:
- **-F:**打包后只生成单个exe格式文件;
- **-D:**默认选项,创建一个目录,包含exe文件以及大量依赖文件;
- **-c:**默认选项,使用控制台(就是类似cmd的黑框);
- **-w:**不使用控制台;
- **-p:**添加搜索路径,让其找到对应的库;
- **-i:**改变生成程序的icon图标。
按下回车,等待打包完毕:
打开我们的工程文件夹下的“dist”文件夹,如下图所示:
可以看到我们刚才生成的.exe文件了。将其复制到我们存放.ico文件的文件夹中(否则图标将不会显示)并双击打开,程序就可以正常运行了:
至此,我们就制作完成了图像传输的简单.exe程序。该程序可以在没有安装python环境的计算机中运行。
此部分参考资料如下:
PyCharm2021安装教程_学习H的博客-CSDN博客_pycharm2021安装教程
将python程序打包成exe_蹦跶的小羊羔的博客-CSDN博客_python打包成exe
Python Qt 图形界面编程 - PySide2 PyQt5 PyQt PySide_哔哩哔哩_bilibili
Python Qt 简介 | 白月黑羽 (byhy.net)
来源地址:https://blog.csdn.net/Zhuwany/article/details/128989573