基于ESP32_CAM与Qt Creator的智能视频监控项目(代码开源)

基于ESP32_CAM与Qt Creator的智能视频监控项目(代码开源)

前言:本文为手把手教学的基于 ESP32_CAM 与 Qt Creator 的智能视频监控项目,项目使用的 MCU 为乐鑫的 ESP32_CAM 搭配 Qt Creator 制作上位机,Qt 的版本为 Qt 5.9.0。本项目的智能 ESP32 Camera 拥有多种视频格式解码、WIFI 灯源控制、WIFI Camera 和智能预警等功能。项目分为上位机与下位机两部分的代码编程,也包含简单的图像算法设计,算是一个 ESP32 很好的练手项目。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!

硬件与软件:ESP32_CAM、iKun ESP32 Camera Studio、Arduino IDE、Qt 5.9.0

项目结果图:

一、ESP32_CAM 智能监控项目

1.1 ESP32_CAM 概述

ESP32_CAM 是一款超小型、低成本、集成 WiFi + 蓝牙 + 摄像头 的物联网视觉模组,由 AI-Thinker(安信可)主导设计,核心基于乐鑫 ESP32 SoC;无板载 USB-TTL,需外接串口模块烧录;采用 DIP-16 封装,可直插面包板 / 专用底板,是嵌入式视觉原型与量产小批量方案的热门选择。乐鑫官方为 ESP32_CAM 提供了标准的摄像头例程,研发工程师可以根据自己的需求进行摄像头相关的开发。

核心硬件规格:主控:ESP32(常见 WROVER-E 版本,双核 Xtensa LX6,最高 240MHz,600 DMIPS;520KB SRAM + 4MB Flash + 4MB PSRAM);支持 2.4GHz WiFi(802.11b/g/n)、蓝牙 4.2(BLE/BR/EDR);支持 STA/AP/STA+AP,SmartConfig/AirKiss 配网,OTA 升级摄像头:标配 OV2640 200 万像素(UXGA 1600×1200),DVP 接口;输出 YUV422、RGB565、JPEG;板载 LED 补光灯;部分兼容 OV7670尺寸与供电:27×40.5×4.5 mm;推荐 5V DC 供电(3.3V 易导致摄像头工作异常);深度睡眠功耗约 6mA扩展:MicroSD 卡槽本地存储;IO 资源因 DVP 占用较多,剩余引脚有限

1.2 ESP32_CAM 智能监控项目概述

本篇博客项目中作者制作的 ikun ESP32 Camera 拥有上位机与下位机,上位机为 Qt Creator 编写的 ikun ESP32 Camera Studio 软件,下位机为乐鑫官方发布的 ESP32_CAM。本项目基于 ESP32_CAM 与 Qt Creator的智能视频监控项目共有 4 项功能,包括:1、WIFI Camera 功能;2、多种视频流解码;3、远程 HTTP 控灯;4、视频流智能预警;

1、WIFI Camera:利用  Qt Creator 的 QNetworkAccessManager 函数来拉取 ESP32_CAM 发送过来的视频流;

2、多种视频流解码:使用 Qt Creator 提供的多种多媒体视频解码库兼容 ESP32_CAM 端发送过来的各种视频流;

3、远程 HTTP 控灯:ESP32CAM 作为 HTTP 服务器,监听指定端口,ESP32_CAM 利用 HTTP 请求,发送给 ESP32 的 IP 地址;

4、视频流智能预警:针对视频流进行像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化,并保存预警照片;智能预警功能可以有效地监控家中出现未知异动之后都情况,亦如陌生人闯入或者火灾等情况,个人认为还是比较有用的功能!

二、ESP32_CAM 例程代码引用

乐鑫官方给 ESP32_CAM 提供了很多编写开发的 IDE 选择,包括:Arduino IDE(ESP32 库)和 ESP-IDF(乐鑫官方 SDK)。作者这里使用 Arduino IDE 进行 ESP_CAM 的开发。

1、本篇博客工程代码的基本框架可以从 Arduino IDE 的示例教程中获取,后续的项目代码在此基础上进行修改删减即可。读者朋友在正确导入ESP32工程库后,按照下图去创建项目的基础框架:

2、使用 Arduino IDE 对示例进行编译,确保当前编译调试环境是 OK 的;

3、将 Arduino IDE 示例代码进行保存,方便进行后续代码编写;

作者补充:

ESP32_CAM 开发板必须将 GPIO0 接地进入下载模式;串口 TX/RX 交叉、GND 共地;供电不足是烧录失败 / 复位的高频原因

三、ESP32 CAM Video Sur 代码与解析

3.1 代码库文件引入与变量定义

#include <WebServer.h>                引入 WebServer 库,方便 ESP32 启用 HTTP

#define CAMERA_MODEL_AI_THINKER // Has PSRAM        启用乐鑫的 AI_THINKER

#define LED_pin 4                          ESP32_CAM 的 LED 是GPIO4(乐鑫官方提供原理图)

const char* ssid = "TP-LINK_E386";                ESP32_CAM 连接的 WIFI 名称

const char* password = "13852200640";         ESP32_CAM 连接的 WIFI 密码

WebServer server(80);                创建Web服务器对象,监听80端口(HTTP默认端口)

3.2 WIFI ESP32 Camera 代码

WIFI ESP32 Camera 这部分功能代码作者直接使用了乐鑫官方提供的代码,读者朋友根据本篇博客的第 2 章节去进行操作即可!作者这边针对官方提供的源码进行简单的解析!
乐鑫官方提供的 ESP32_CAM 例程代码核心就是 setup() 函数,包括:WIFI Camera 的 GPIO 引脚配置、摄像头的视频流格式、摄像头的数据格式初始化设置和 WIFI 连接等
void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某"); //设置LED1引脚为输出模式 pinMode(LED_pin, OUTPUT); //LED1引脚输出低电平,灯灭 digitalWrite(LED_pin, LOW); 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; // for streaming //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 12; config.fb_count = 1; // if PSRAM IC present, init with UXGA resolution and higher JPEG quality // for larger pre-allocated frame buffer. if(config.pixel_format == PIXFORMAT_JPEG){ if(psramFound()){ config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { // Limit the frame size when PSRAM is not available config.frame_size = FRAMESIZE_SVGA; config.fb_location = CAMERA_FB_IN_DRAM; } } else { // Best option for face detection/recognition config.frame_size = FRAMESIZE_240X240; #if CONFIG_IDF_TARGET_ESP32S3 config.fb_count = 2; #endif } #if defined(CAMERA_MODEL_ESP_EYE) pinMode(13, INPUT_PULLUP); pinMode(14, INPUT_PULLUP); #endif // camera init esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x", err); return; } sensor_t * s = esp_camera_sensor_get(); // initial sensors are flipped vertically and colors are a bit saturated if (s->id.PID == OV3660_PID) { s->set_vflip(s, 1); // flip it back s->set_brightness(s, 1); // up the brightness just a bit s->set_saturation(s, -2); // lower the saturation } // drop down frame size for higher initial frame rate if(config.pixel_format == PIXFORMAT_JPEG){ s->set_framesize(s, FRAMESIZE_QVGA); } #if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM) s->set_vflip(s, 1); s->set_hmirror(s, 1); #endif #if defined(CAMERA_MODEL_ESP32S3_EYE) s->set_vflip(s, 1); #endif WiFi.begin(ssid, password); WiFi.setSleep(false); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); startCameraServer(); Serial.print("Camera Ready! Use 'http://"); Serial.print(WiFi.localIP()); Serial.println("' to connect"); // LED灯控制 server.on("/control", handleControlRequest); // LED控制接口 // 启动服务器 server.begin(); Serial.println("HTTP服务器已启动"); }

3.3 ESP32_CAM 的 WIFI 控制灯代码

乐鑫官方提供的 ESP32_CAM 代码可以稳定运行 WIFI Camera 功能,并且预留了很多网络协议的库,这边作者引入 WebServer.h 库进行 HTTP 协议的 LED 小灯控制!

根据乐鑫官方提供的 ESP32_CAM 原理图,我们可以发送控制板载的 LED 的 GPIO 引脚为 GPIO4,我们只需要在 Qt 端上位机发送指令去控制 ESP32_CAM 的 GPIO 引脚高低电平即可。

作者编写了一个简单的 HTTP 服务器,监听 /control 接口,解析 var=led&val=40 或 var=led&val=41 参数,并控制 LED 灯。代码的关键核心:

1、WiFi 配置:你需要把 ssid 和 password 替换成自己的 WiFi 名称和密码,这样 ESP32-CAM 才能接入局域网;

2、LED 引脚:ESP32-CAM 的板载 LED 通常接在 GPIO4,且是低电平点亮(LOW 亮、HIGH 灭),如果你的硬件不同,需要调整 digitalWrite 的 HIGH/LOW。

3、HTTP 接口处理:服务器监听 /control 路径,对应 QT 端的 http://IP/control?var=led&val=40 请求;通过 server.arg("val") 获取 QT 发送的参数值(40/41)。
// 处理/control接口的请求 void handleControlRequest() { // 检查是否有var参数,且值为led if (server.hasArg("var") && server.arg("var") == "led") { // 检查是否有val参数 if (server.hasArg("val")) { String valStr = server.arg("val"); int val = valStr.toInt(); // 根据参数值控制LED if (val == 40) { // val=40 关闭LED digitalWrite(LED_pin, LOW); // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW Serial.printf("LED已关闭 (val=%d)\n", val); server.send(200, "text/plain", "LED OFF"); // 返回成功响应 } else if (val == 41) { // val=41 打开LED digitalWrite(LED_pin, HIGH); Serial.printf("LED已打开 (val=%d)\n", val); server.send(200, "text/plain", "LED ON"); // 返回成功响应 } else { // 无效的参数值 Serial.printf("无效的LED参数值: %d\n", val); server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应 } } else { // 缺少val参数 server.send(400, "text/plain", "Missing 'val' parameter"); } } else { // 不是控制LED的请求 server.send(400, "text/plain", "Not a LED control request"); } } void loop() { // 处理客户端的HTTP请求 server.handleClient(); delay(10); }

3.4 ESP32_CAM 的完整代码

 /********************************** (C) COPYRIGHT ******************************* * File Name : CameraWebServer.ino * Author : 混分巨兽龙某某 * Version : V1.0.0 * Date : 2026/02/21 * Description : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator ********************************************************************************/ #include "esp_camera.h" #include <WiFi.h> #include <WebServer.h> #define CAMERA_MODEL_AI_THINKER // Has PSRAM #include "camera_pins.h" #define LED_pin 4 const char* ssid = "TP-LINK_E386"; const char* password = "xxxxxxxx"; // 创建Web服务器对象,监听80端口(HTTP默认端口) WebServer server(80); void startCameraServer(); // 处理/control接口的请求 void handleControlRequest() { // 检查是否有var参数,且值为led if (server.hasArg("var") && server.arg("var") == "led") { // 检查是否有val参数 if (server.hasArg("val")) { String valStr = server.arg("val"); int val = valStr.toInt(); // 根据参数值控制LED if (val == 40) { // val=40 关闭LED digitalWrite(LED_pin, LOW); // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW Serial.printf("LED已关闭 (val=%d)\n", val); server.send(200, "text/plain", "LED OFF"); // 返回成功响应 } else if (val == 41) { // val=41 打开LED digitalWrite(LED_pin, HIGH); Serial.printf("LED已打开 (val=%d)\n", val); server.send(200, "text/plain", "LED ON"); // 返回成功响应 } else { // 无效的参数值 Serial.printf("无效的LED参数值: %d\n", val); server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应 } } else { // 缺少val参数 server.send(400, "text/plain", "Missing 'val' parameter"); } } else { // 不是控制LED的请求 server.send(400, "text/plain", "Not a LED control request"); } } void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某"); //设置LED1引脚为输出模式 pinMode(LED_pin, OUTPUT); //LED1引脚输出低电平,灯灭 digitalWrite(LED_pin, LOW); 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; // for streaming //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 12; config.fb_count = 1; // if PSRAM IC present, init with UXGA resolution and higher JPEG quality // for larger pre-allocated frame buffer. if(config.pixel_format == PIXFORMAT_JPEG){ if(psramFound()){ config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { // Limit the frame size when PSRAM is not available config.frame_size = FRAMESIZE_SVGA; config.fb_location = CAMERA_FB_IN_DRAM; } } else { // Best option for face detection/recognition config.frame_size = FRAMESIZE_240X240; #if CONFIG_IDF_TARGET_ESP32S3 config.fb_count = 2; #endif } #if defined(CAMERA_MODEL_ESP_EYE) pinMode(13, INPUT_PULLUP); pinMode(14, INPUT_PULLUP); #endif // camera init esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x", err); return; } sensor_t * s = esp_camera_sensor_get(); // initial sensors are flipped vertically and colors are a bit saturated if (s->id.PID == OV3660_PID) { s->set_vflip(s, 1); // flip it back s->set_brightness(s, 1); // up the brightness just a bit s->set_saturation(s, -2); // lower the saturation } // drop down frame size for higher initial frame rate if(config.pixel_format == PIXFORMAT_JPEG){ s->set_framesize(s, FRAMESIZE_QVGA); } #if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM) s->set_vflip(s, 1); s->set_hmirror(s, 1); #endif #if defined(CAMERA_MODEL_ESP32S3_EYE) s->set_vflip(s, 1); #endif WiFi.begin(ssid, password); WiFi.setSleep(false); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); startCameraServer(); Serial.print("Camera Ready! Use 'http://"); Serial.print(WiFi.localIP()); Serial.println("' to connect"); // LED灯控制 server.on("/control", handleControlRequest); // LED控制接口 // 启动服务器 server.begin(); Serial.println("HTTP服务器已启动"); } void loop() { // 处理客户端的HTTP请求 server.handleClient(); delay(10); } 

四、ESP32 Camera Studio

4.1 WIFI Camera 部分代码

作者这边利用 Qt 的多媒体库从而可以轻松去解码各种视频流协议,支持的格式如下:
/** * @file mainwindow.cpp * @brief ESP32-CAM QT客户端主窗口实现 * * 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能 */ /** * @def WINDOW_TITLE * @brief 窗口标题宏定义 */ #define WINDOW_TITLE "iKun ESP32 Camera Studio" /** * @brief MainWindow构造函数 * @param parent 父窗口指针 * * 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态 */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_ledState(40) , m_safetyEnabled(false) , m_previousImage() , m_lastCaptureTime(QDateTime::currentDateTime()) { // 初始化UI界面 ui->setupUi(this); // 设置窗口标题 setWindowTitle(WINDOW_TITLE); // 分辨率选项列表 QStringList vPixelList = { "96x96", "QQVGA(160x120)", "QCIF(176x144)", "HQVGA(240x176)", "240x240", "QVGA(320x240)", "CIF(400x296)", "HVGA(480x320)", "VGA(640x480)", "SVGA(800x600)", "XGA(1024x768)", "HD(1280x720)", "SXGA(1280x1024)", "UXGA(1600x1200)", }; // 添加分辨率选项到下拉框 ui->cbxPixel->addItems(vPixelList); // 请求ESP32-CAM当前状态 RequestStatus(); } /** * @brief MainWindow析构函数 * * 释放UI资源 */ MainWindow::~MainWindow() { delete ui; } 

开始取流的按键代码:

/** * @brief 开始取流按钮点击事件处理 * * 连接到ESP32-CAM的视频流,开始获取视频数据 */ void MainWindow::on_btnStart_clicked() { // 如果已经连接,直接返回 if (m_pvClient != nullptr) { return; } // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { QMessageBox::warning(this, "提示", "请输入IP地址"); return; } // 构建视频流URL const QString sURL = "http://" + sIPAddr + ":81/stream"; // 创建网络访问管理器 QNetworkAccessManager *pvManager = new QNetworkAccessManager(); // 创建网络请求 QNetworkRequest vRequest; vRequest.setUrl(sURL); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求获取视频流 m_pvClient = pvManager->get(vRequest); // 连接readyRead信号,处理接收到的数据 connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead); } 

结束取流的按键代码:

/** * @brief 结束取流按钮点击事件处理 * * 断开与ESP32-CAM的连接,停止获取视频数据 */ void MainWindow::on_btnStop_clicked() { if (m_pvClient != nullptr) { // 断开信号连接 disconnect(m_pvClient); // 中止请求 m_pvClient->abort(); // 延迟删除 m_pvClient->deleteLater(); // 重置指针 m_pvClient = nullptr; // 清空缓冲区 m_buffer.clear(); } } 

视频流分辨率的解码:

/** * @brief 请求ESP32-CAM状态 * * 获取ESP32-CAM的当前状态,包括分辨率等信息 */ void MainWindow::RequestStatus() { // 清空缓冲区 m_buffer.clear(); // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { return; } // 构建状态请求URL const QString sUrl = "http://" + sIPAddr + "/status"; qDebug() << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; QNetworkReply *reply; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求 reply = manager.get(vRequest); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() == QNetworkReply::NoError) { // 读取响应数据 QString jsonString = reply->readAll(); // 解析JSON数据 QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8()); if (!jsonDocument.isNull()) { if (jsonDocument.isObject()) { QJsonObject jsonObject = jsonDocument.object(); // 获取分辨率索引 int framesize = jsonObject["framesize"].toInt(); // 设置下拉框当前索引 ui->cbxPixel->setCurrentIndex(framesize); } } } else { // 输出错误信息 qDebug() << "Error: " << reply->errorString(); } // 释放回复对象 reply->deleteLater(); } /** * @brief 分辨率下拉框选择变化事件处理 * @param index 选择的分辨率索引 * * 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率 */ void MainWindow::on_cbxPixel_currentIndexChanged(int index) { // 静态变量,用于跳过初始化时的第一次触发 static bool isFirstSend = true; if (isFirstSend) { isFirstSend = false; return; } // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { return; } // 构建分辨率更改请求URL const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index); qDebug() << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; QNetworkReply *reply; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求 reply = manager.get(vRequest); // 清空缓冲区 m_mutex.lock(); m_buffer.clear(); m_mutex.unlock(); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理错误 if (reply->error() != QNetworkReply::NoError) { qDebug() << "Error: " << reply->errorString(); } // 释放回复对象 reply->deleteLater(); } /** * @brief 网络数据就绪事件处理 * * 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧 */ void MainWindow::readyRead() { // 如果没有网络回复对象,直接返回 if (m_pvClient == nullptr) { return; } // 读取网络数据到缓冲区 m_mutex.lock(); m_buffer.append(m_pvClient->readAll()); m_mutex.unlock(); // MJPEG流标记 const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:"; const QString sEndKey = "\r\n--123456789000000000000987654321\r\n"; // 循环处理缓冲区中的数据 while (true) { // 查找JPEG图像的开始标记 int nStartPos = m_buffer.indexOf(sStartKey); if (nStartPos < 0) { break; } // 查找JPEG图像的结束标记 int nEndPos = m_buffer.indexOf(sEndKey, nStartPos); if (nEndPos < 0) { break; } // 调整开始位置 nStartPos = m_buffer.indexOf("\r\n"); if (nStartPos < 0) { break; } // 计算结束位置 nEndPos += sEndKey.size(); // 提取JPEG图像数据 QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos); if (vBuf.size() == 0) { continue; } // 解析并显示图像 ParseFrame(vBuf); // 从缓冲区中移除已处理的数据 m_mutex.lock(); m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos); m_mutex.unlock(); } }

4.2 控灯部分代码

控制 LED 灯的方法有很多,作者这边利用了 QNetworkAccessManager 发送 HTTP GET 请求,使用QEventLoop等待网络请求完成。Qt 端上位机捕获并处理网络请求错误,在成功后切换LED状态,实现40和41的循环。作者补充:本人为了防止出现 HTTP 传输异常的情况,默认连续使用 2 次的 QNetworkAccessManager 发送 HTTP GET 请求,保证了 Qt 端数据的可以稳定控制下位机 ESP32_CAM 的 LED,读者朋友们平时也可以这样操作!
/** * @brief LED控制按钮点击事件处理 * * 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ... * 仅发送一次请求,不进行重试 */ void MainWindow::on_btnLED_clicked() { // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { QMessageBox::warning(this, "提示", "请输入IP地址"); return; } // 构建LED控制URL,使用当前m_ledState值 const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState); qDebug() << "LED control URL:" << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); vRequest.setRawHeader("Accept", "*/*"); // 发送GET请求 QNetworkReply *reply = manager.get(vRequest); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() != QNetworkReply::NoError) { // 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败) QNetworkAccessManager manager1; // 创建网络请求 QNetworkRequest vRequest1; vRequest1.setUrl(QUrl(sUrl)); vRequest1.setRawHeader("Connection", "Keep-Alive"); vRequest1.setRawHeader("Cache-Control", "no-cache"); vRequest1.setRawHeader("Pragma", "no-cache"); vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); vRequest1.setRawHeader("Accept", "*/*"); // 发送GET请求 QNetworkReply *reply = manager1.get(vRequest1); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() != QNetworkReply::NoError) { qDebug() << "LED control error: " << reply->errorString(); QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString()); reply->deleteLater(); }else{ qDebug() << "LED control successful, val:" << m_ledState; // 切换LED状态:40 -> 41 -> 40 -> ... m_ledState = (m_ledState == 40) ? 41 : 40; reply->deleteLater(); } } else { qDebug() << "LED control successful, val:" << m_ledState; // 切换LED状态:40 -> 41 -> 40 -> ... m_ledState = (m_ledState == 40) ? 41 : 40; reply->deleteLater(); } } 

4.3 智能预警部分代码

作者针对这种很实用的智能预警功能进行了很巧妙的图像算法设计(智能预警:可以用于监控家中是否进入陌生人或者火灾等异常状况)。
智能预警的功能实现:

- 智能预警检测 :当 boxSafety 复选框被勾选时,启用智能预警功能
- 图像变化检测 :每隔 1 秒比较前后两帧图像,检测是否发生变化
- 自动保存 :当检测到图像变化时,自动保存当前画面到桌面的 "ikun ESP32 Camera" 文件夹
- 文件夹自动创建 :每次运行时会自动创建保存文件夹,确保文件能够正确保存

智能预警的技术核心:

- 图像比较算法 :使用像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化
- 时间间隔控制 :使用QDateTime记录时间戳,确保每1秒进行一次比较
- 文件系统操作 :使用QDir创建文件夹,QStandardPaths获取桌面路径
- 多线程安全 :使用QMutex保护共享资源
/** * @brief 比较两个图像是否发生了变化 * @param img1 第一个图像 * @param img2 第二个图像 * @return 是否发生了变化 */ bool MainWindow::compareImages(const QImage &img1, const QImage &img2) { // 检查图像是否有效 if (img1.isNull() || img2.isNull()) { return false; } // 检查图像大小是否相同 if (img1.size() != img2.size()) { return true; } // 计算图像差异 int diffCount = 0; int threshold = 10; // 像素差异阈值 int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%) for (int y = 0; y < img1.height(); y++) { for (int x = 0; x < img1.width(); x++) { QRgb pixel1 = img1.pixel(x, y); QRgb pixel2 = img2.pixel(x, y); // 计算RGB差异 int rDiff = qAbs(qRed(pixel1) - qRed(pixel2)); int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2)); int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2)); // 如果差异超过阈值,增加差异计数 if (rDiff > threshold || gDiff > threshold || bDiff > threshold) { diffCount++; // 如果差异超过最大允许值,直接返回true if (diffCount > maxDiff) { return true; } } } } return diffCount > maxDiff; } /** * @brief 保存图像到指定文件夹 * @param img 要保存的图像 */ void MainWindow::saveImage(const QImage &img) { // 创建保存目录 QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QString saveDir = desktopPath + "/ikun ESP32 Camera"; QDir dir(saveDir); if (!dir.exists()) { if (!dir.mkpath(saveDir)) { qDebug() << "Failed to create save directory"; return; } } // 生成文件名(基于时间戳) QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz"); QString filename = saveDir + "/capture_" + timestamp + ".jpg"; // 保存图像 if (img.save(filename)) { qDebug() << "Image saved to:" << filename; } else { qDebug() << "Failed to save image"; } } /** * @brief 解析视频帧数据 * @param vBuf 包含JPEG图像的数据 * * 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上 * 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面 */ void MainWindow::ParseFrame(const QByteArray &vBuf) { // 定义常量 const QString sBodyKey = "\r\n\r\n"; const QString sItemKey = "\r\n"; const QString sLengthKey = "Content-Length"; // 查找body的起始位置 int nBodyIndex = vBuf.indexOf(sBodyKey); if (nBodyIndex <= 0) { return; } // 提取头部信息 QString sHead = vBuf.mid(0, nBodyIndex); QStringList vHeadSplit = sHead.split(sItemKey); // 解析头部信息到映射表 QMap<QString, QString> vHeadMap; for (const QString &vItem : vHeadSplit) { QStringList vItemSplit = vItem.split(": "); if (vItemSplit.size() == 2) { vHeadMap[vItemSplit[0]] = vItemSplit[1]; } } // 获取图像数据长度 uint32_t nLength = 0; if (vHeadMap.count(sLengthKey) > 0) { nLength = vHeadMap[sLengthKey].toUInt(); } if (nLength == 0) { return; } // 提取图像数据 QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength); // 加载图像数据 QPixmap vPixmap; if (!vPixmap.loadFromData(sBody)) { qDebug() << "Failed to load image"; return; } // 调整图像大小以适应显示区域 QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); // 设置标签的大小策略 ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 显示图像 ui->lblVideo->setPixmap(vImage); // 智能预警功能 if (m_safetyEnabled) { // 转换为QImage用于比较 QImage currentImage = vPixmap.toImage(); // 获取当前时间 QDateTime currentTime = QDateTime::currentDateTime(); // 计算与上次捕获的时间差 qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime); // 每隔1秒进行一次图像比较 if (elapsed >= 1000) { // 检查是否有前一秒的图像可比较 if (!m_previousImage.isNull()) { // 比较前后图像是否发生了变化 if (compareImages(m_previousImage, currentImage)) { // 图像发生变化,保存当前画面 saveImage(currentImage); } } // 更新前一秒图像和时间戳 m_previousImage = currentImage; m_lastCaptureTime = currentTime; } } } 

4.4 ESP32 Camera Studio 的完整代码

/********************************** (C) COPYRIGHT ******************************* * File Name : CameraWebServer.ino * Author : 混分巨兽龙某某 * Version : V1.0.0 * Date : 2026/02/21 * Description : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator ********************************************************************************/ #include "mainwindow.h" #include <QtNetwork/QNetworkAccessManager> #include <QtNetwork/QNetworkRequest> #include <QDebug> #include <QPixmap> #include <QMessageBox> #include <QComboBox> #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> #include <QThread> #include <QStandardPaths> #include <QDir> #include <QImage> #include "ui_mainwindow.h" /** * @file mainwindow.cpp * @brief ESP32-CAM QT客户端主窗口实现 * * 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能 */ /** * @def WINDOW_TITLE * @brief 窗口标题宏定义 */ #define WINDOW_TITLE "iKun ESP32 Camera Studio" /** * @brief MainWindow构造函数 * @param parent 父窗口指针 * * 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态 */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_ledState(40) , m_safetyEnabled(false) , m_previousImage() , m_lastCaptureTime(QDateTime::currentDateTime()) { // 初始化UI界面 ui->setupUi(this); // 设置窗口标题 setWindowTitle(WINDOW_TITLE); // 分辨率选项列表 QStringList vPixelList = { "96x96", "QQVGA(160x120)", "QCIF(176x144)", "HQVGA(240x176)", "240x240", "QVGA(320x240)", "CIF(400x296)", "HVGA(480x320)", "VGA(640x480)", "SVGA(800x600)", "XGA(1024x768)", "HD(1280x720)", "SXGA(1280x1024)", "UXGA(1600x1200)", }; // 添加分辨率选项到下拉框 ui->cbxPixel->addItems(vPixelList); // 请求ESP32-CAM当前状态 RequestStatus(); } /** * @brief MainWindow析构函数 * * 释放UI资源 */ MainWindow::~MainWindow() { delete ui; } /** * @brief 开始取流按钮点击事件处理 * * 连接到ESP32-CAM的视频流,开始获取视频数据 */ void MainWindow::on_btnStart_clicked() { // 如果已经连接,直接返回 if (m_pvClient != nullptr) { return; } // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { QMessageBox::warning(this, "提示", "请输入IP地址"); return; } // 构建视频流URL const QString sURL = "http://" + sIPAddr + ":81/stream"; // 创建网络访问管理器 QNetworkAccessManager *pvManager = new QNetworkAccessManager(); // 创建网络请求 QNetworkRequest vRequest; vRequest.setUrl(sURL); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求获取视频流 m_pvClient = pvManager->get(vRequest); // 连接readyRead信号,处理接收到的数据 connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead); } /** * @brief 结束取流按钮点击事件处理 * * 断开与ESP32-CAM的连接,停止获取视频数据 */ void MainWindow::on_btnStop_clicked() { if (m_pvClient != nullptr) { // 断开信号连接 disconnect(m_pvClient); // 中止请求 m_pvClient->abort(); // 延迟删除 m_pvClient->deleteLater(); // 重置指针 m_pvClient = nullptr; // 清空缓冲区 m_buffer.clear(); } } /** * @brief 比较两个图像是否发生了变化 * @param img1 第一个图像 * @param img2 第二个图像 * @return 是否发生了变化 */ bool MainWindow::compareImages(const QImage &img1, const QImage &img2) { // 检查图像是否有效 if (img1.isNull() || img2.isNull()) { return false; } // 检查图像大小是否相同 if (img1.size() != img2.size()) { return true; } // 计算图像差异 int diffCount = 0; int threshold = 10; // 像素差异阈值 int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%) for (int y = 0; y < img1.height(); y++) { for (int x = 0; x < img1.width(); x++) { QRgb pixel1 = img1.pixel(x, y); QRgb pixel2 = img2.pixel(x, y); // 计算RGB差异 int rDiff = qAbs(qRed(pixel1) - qRed(pixel2)); int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2)); int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2)); // 如果差异超过阈值,增加差异计数 if (rDiff > threshold || gDiff > threshold || bDiff > threshold) { diffCount++; // 如果差异超过最大允许值,直接返回true if (diffCount > maxDiff) { return true; } } } } return diffCount > maxDiff; } /** * @brief 保存图像到指定文件夹 * @param img 要保存的图像 */ void MainWindow::saveImage(const QImage &img) { // 创建保存目录 QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QString saveDir = desktopPath + "/ikun ESP32 Camera"; QDir dir(saveDir); if (!dir.exists()) { if (!dir.mkpath(saveDir)) { qDebug() << "Failed to create save directory"; return; } } // 生成文件名(基于时间戳) QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz"); QString filename = saveDir + "/capture_" + timestamp + ".jpg"; // 保存图像 if (img.save(filename)) { qDebug() << "Image saved to:" << filename; } else { qDebug() << "Failed to save image"; } } /** * @brief 解析视频帧数据 * @param vBuf 包含JPEG图像的数据 * * 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上 * 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面 */ void MainWindow::ParseFrame(const QByteArray &vBuf) { // 定义常量 const QString sBodyKey = "\r\n\r\n"; const QString sItemKey = "\r\n"; const QString sLengthKey = "Content-Length"; // 查找body的起始位置 int nBodyIndex = vBuf.indexOf(sBodyKey); if (nBodyIndex <= 0) { return; } // 提取头部信息 QString sHead = vBuf.mid(0, nBodyIndex); QStringList vHeadSplit = sHead.split(sItemKey); // 解析头部信息到映射表 QMap<QString, QString> vHeadMap; for (const QString &vItem : vHeadSplit) { QStringList vItemSplit = vItem.split(": "); if (vItemSplit.size() == 2) { vHeadMap[vItemSplit[0]] = vItemSplit[1]; } } // 获取图像数据长度 uint32_t nLength = 0; if (vHeadMap.count(sLengthKey) > 0) { nLength = vHeadMap[sLengthKey].toUInt(); } if (nLength == 0) { return; } // 提取图像数据 QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength); // 加载图像数据 QPixmap vPixmap; if (!vPixmap.loadFromData(sBody)) { qDebug() << "Failed to load image"; return; } // 调整图像大小以适应显示区域 QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); // 设置标签的大小策略 ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 显示图像 ui->lblVideo->setPixmap(vImage); // 智能预警功能 if (m_safetyEnabled) { // 转换为QImage用于比较 QImage currentImage = vPixmap.toImage(); // 获取当前时间 QDateTime currentTime = QDateTime::currentDateTime(); // 计算与上次捕获的时间差 qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime); // 每隔1秒进行一次图像比较 if (elapsed >= 1000) { // 检查是否有前一秒的图像可比较 if (!m_previousImage.isNull()) { // 比较前后图像是否发生了变化 if (compareImages(m_previousImage, currentImage)) { // 图像发生变化,保存当前画面 saveImage(currentImage); } } // 更新前一秒图像和时间戳 m_previousImage = currentImage; m_lastCaptureTime = currentTime; } } } /** * @brief 请求ESP32-CAM状态 * * 获取ESP32-CAM的当前状态,包括分辨率等信息 */ void MainWindow::RequestStatus() { // 清空缓冲区 m_buffer.clear(); // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { return; } // 构建状态请求URL const QString sUrl = "http://" + sIPAddr + "/status"; qDebug() << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; QNetworkReply *reply; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求 reply = manager.get(vRequest); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() == QNetworkReply::NoError) { // 读取响应数据 QString jsonString = reply->readAll(); // 解析JSON数据 QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8()); if (!jsonDocument.isNull()) { if (jsonDocument.isObject()) { QJsonObject jsonObject = jsonDocument.object(); // 获取分辨率索引 int framesize = jsonObject["framesize"].toInt(); // 设置下拉框当前索引 ui->cbxPixel->setCurrentIndex(framesize); } } } else { // 输出错误信息 qDebug() << "Error: " << reply->errorString(); } // 释放回复对象 reply->deleteLater(); } /** * @brief 分辨率下拉框选择变化事件处理 * @param index 选择的分辨率索引 * * 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率 */ void MainWindow::on_cbxPixel_currentIndexChanged(int index) { // 静态变量,用于跳过初始化时的第一次触发 static bool isFirstSend = true; if (isFirstSend) { isFirstSend = false; return; } // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { return; } // 构建分辨率更改请求URL const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index); qDebug() << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; QNetworkReply *reply; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); // 发送GET请求 reply = manager.get(vRequest); // 清空缓冲区 m_mutex.lock(); m_buffer.clear(); m_mutex.unlock(); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理错误 if (reply->error() != QNetworkReply::NoError) { qDebug() << "Error: " << reply->errorString(); } // 释放回复对象 reply->deleteLater(); } /** * @brief 网络数据就绪事件处理 * * 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧 */ void MainWindow::readyRead() { // 如果没有网络回复对象,直接返回 if (m_pvClient == nullptr) { return; } // 读取网络数据到缓冲区 m_mutex.lock(); m_buffer.append(m_pvClient->readAll()); m_mutex.unlock(); // MJPEG流标记 const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:"; const QString sEndKey = "\r\n--123456789000000000000987654321\r\n"; // 循环处理缓冲区中的数据 while (true) { // 查找JPEG图像的开始标记 int nStartPos = m_buffer.indexOf(sStartKey); if (nStartPos < 0) { break; } // 查找JPEG图像的结束标记 int nEndPos = m_buffer.indexOf(sEndKey, nStartPos); if (nEndPos < 0) { break; } // 调整开始位置 nStartPos = m_buffer.indexOf("\r\n"); if (nStartPos < 0) { break; } // 计算结束位置 nEndPos += sEndKey.size(); // 提取JPEG图像数据 QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos); if (vBuf.size() == 0) { continue; } // 解析并显示图像 ParseFrame(vBuf); // 从缓冲区中移除已处理的数据 m_mutex.lock(); m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos); m_mutex.unlock(); } } /** * @brief LED控制按钮点击事件处理 * * 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ... * 仅发送一次请求,不进行重试 */ void MainWindow::on_btnLED_clicked() { // 获取IP地址 const QString sIPAddr = ui->txtAddr->text(); if (sIPAddr.size() == 0) { QMessageBox::warning(this, "提示", "请输入IP地址"); return; } // 构建LED控制URL,使用当前m_ledState值 const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState); qDebug() << "LED control URL:" << sUrl; // 创建网络访问管理器 QNetworkAccessManager manager; // 创建网络请求 QNetworkRequest vRequest; vRequest.setUrl(QUrl(sUrl)); vRequest.setRawHeader("Connection", "Keep-Alive"); vRequest.setRawHeader("Cache-Control", "no-cache"); vRequest.setRawHeader("Pragma", "no-cache"); vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); vRequest.setRawHeader("Accept", "*/*"); // 发送GET请求 QNetworkReply *reply = manager.get(vRequest); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() != QNetworkReply::NoError) { // 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败) QNetworkAccessManager manager1; // 创建网络请求 QNetworkRequest vRequest1; vRequest1.setUrl(QUrl(sUrl)); vRequest1.setRawHeader("Connection", "Keep-Alive"); vRequest1.setRawHeader("Cache-Control", "no-cache"); vRequest1.setRawHeader("Pragma", "no-cache"); vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"); vRequest1.setRawHeader("Accept", "*/*"); // 发送GET请求 QNetworkReply *reply = manager1.get(vRequest1); // 创建事件循环,等待请求完成 QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // 处理响应 if (reply->error() != QNetworkReply::NoError) { qDebug() << "LED control error: " << reply->errorString(); QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString()); reply->deleteLater(); }else{ qDebug() << "LED control successful, val:" << m_ledState; // 切换LED状态:40 -> 41 -> 40 -> ... m_ledState = (m_ledState == 40) ? 41 : 40; reply->deleteLater(); } } else { qDebug() << "LED control successful, val:" << m_ledState; // 切换LED状态:40 -> 41 -> 40 -> ... m_ledState = (m_ledState == 40) ? 41 : 40; reply->deleteLater(); } } /** * @brief 智能预警复选框状态变化事件处理 * @param arg1 复选框的状态(Qt::Checked或Qt::Unchecked) * * 启用或禁用智能预警功能 */ void MainWindow::on_boxSafety_stateChanged(int arg1) { // 检查复选框是否被选中 if (arg1 == Qt::Checked) { // 启用智能预警功能 m_safetyEnabled = true; qDebug() << "智能预警功能已启用"; // 重置时间戳和前一帧图像 m_lastCaptureTime = QDateTime::currentDateTime(); m_previousImage = QImage(); // 显示提示信息 QMessageBox::information(this, "提示", "智能预警功能已启用\n当检测到图像变化时,会自动保存画面到桌面的'ikun ESP32 Camera'文件夹"); } else { // 禁用智能预警功能 m_safetyEnabled = false; qDebug() << "智能预警功能已禁用"; } } 

五、ESP32_CAM 智能视频监控演示

六、代码开源

代码地址: 基于ESP32-CAM与QtCreator的智能视频监控项目代码资源-ZEEKLOG下载

如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

Read more

我让openclaw做了一个 B 站弹幕分析SKILL:自动抓取 + 词云 + 情感分析 + 舆情报告(开源)

我让openclaw做了一个 B 站弹幕分析SKILL:自动抓取 + 词云 + 情感分析 + 舆情报告(开源)

大家好,最近我让openclaw把我自己在内容运营里常用的一套“弹幕分析流程”做成了一个可复用的小项目: 👉 bilibili-danmaku GitHub:https://github.com/Smartloe/bilibili-danmaku 核心目标很直接: * 给我一个 B 站视频链接 * 自动抓取弹幕 * 自动做分词清洗 * 自动输出词云图 + 情感分析 + 舆情报告 适合做内容复盘、热点观察、用户反馈提炼。 一、这个项目解决了什么问题? 日常做视频复盘时,常见痛点是: 1. 弹幕采集麻烦:每次手动导出/复制,效率很低。 2. 词云质量不稳定:不清洗会被“哈哈哈/666/这边那边”污染。 3. 舆情判断缺标准:没有统一口径,沟通时容易“拍脑袋”。 这个项目把整条链路打通了: 抓取 → 清洗 → 关键词 → 词云 → 情感

By Ne0inhk

Obsidian资源下载终极提速指南:告别GitHub龟速的3个快速解决方案

还在为Obsidian主题和插件下载速度慢到怀疑人生而烦恼吗?每次从GitHub获取awesome-obsidian项目资源时,那个转圈圈的加载动画是不是让你想砸键盘?本文将分享亲测有效的Obsidian加速下载方法,通过国内镜像站点让你体验飞一般的下载速度! 【免费下载链接】awesome-obsidian🕶️ Awesome stuff for Obsidian 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-obsidian 痛点分析:为什么你的Obsidian资源下载这么慢? 网络瓶颈识别: * GitHub国际带宽限制导致国内访问缓慢 * 网络波动造成频繁中断 * 大文件传输时缺乏稳定的CDN支持 速度对比实测: * 原GitHub地址:平均50KB/s,经常断连 * 国内镜像站点:稳定2-5MB/s,一次成功 三大提速方案深度解析 方案一:GitCode全量镜像(推荐新手) 作为国内最稳定的代码托管平台,GitCode提供了完整的awesome-obsidian项目镜像: # 一键克隆完整项

By Ne0inhk
从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 -《已适配开源鸿蒙》

从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 -《已适配开源鸿蒙》

* 个人首页: VON * 鸿蒙系列专栏: 鸿蒙开发小型案例总结 * 综合案例 :鸿蒙综合案例开发 * 鸿蒙6.0:从0开始的开源鸿蒙6.0.0 * 鸿蒙5.0:鸿蒙5.0零基础入门到项目实战 * Electron适配开源鸿蒙专栏:Electron for OpenHarmony * 本文章所属专栏:Flutter for OpenHarmony * 文章AtomGit地址:Template_V2.0 v1.0 → v2.0 全代码详解 * 从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 * 🧱 第一阶段:v1.0 —— 干净的基础骨架 * ✅ 目标 * 📁 项目结构 * 1. `lib/main.dart`

By Ne0inhk
Git下载及安装保姆级教程(内附快速下载方法)

Git下载及安装保姆级教程(内附快速下载方法)

一、下载Git 1、Git的下载地址 Git-2.47.1-64-bit https://git-scm.com/downloads 选择相应的操作系统下载,这里给出的是当前最新版本2.47.1,如需下载之前的版本,可在图片显示的红框内,点击Older releases即可。 PS:由于一些原因,Git安装包下载速度较慢,可以复制资源链接到迅雷等第三方下载工具下载或直接下载本文的资源即可 2、等待安装 找到下载的安装包双击进行安装。 二、Git的安装 1、阅读说明 点击Next进行下一步。 2、选择安装路径 默认安装路径为C:\Program Files\Git,如需修改,点击①Browse选择文件夹,无需修改点击②Next进行下一步。 3、选择安装组件 ①为在桌面上显示Git图标,可以勾选。其余默认选项不建议取消勾选,以免安装出现意外问题。如确认无误,点击②

By Ne0inhk