欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > 【ESP32cam人脸识别开门及服务器端实战源码】

【ESP32cam人脸识别开门及服务器端实战源码】

2025/2/8 18:46:07 来源:https://blog.csdn.net/lnwqh/article/details/145495325  浏览:    关键词:【ESP32cam人脸识别开门及服务器端实战源码】

本项目实现了一个基于ESP32-CAM的实时人脸识别系统,能够通过WiFi进行视频流传输,并在检测到人脸时触发开门指令。系统由两个主要部分组成:`video.py`(后端服务器)和 `ESP32-CAM.ino`(ESP32-CAM固件)。

## 2. 主要功能

### 2.1 `video.py`python源码服务器端。

from flask import Flask, request, jsonify, Response
import cv2
import numpy as np
import face_recognition
import logging
import requests
import time
import threading
import os
from werkzeug.utils import secure_filenameapp = Flask(__name__)# 配置日志
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s'
)# 配置
SAMPLE_DIR = './samples'
ESP32_URL = 'http://192.168.1.104/open_door'
LED_URL = 'http://192.168.1.104/toggle_flash'
FACE_THRESHOLD = 0.45  # 放宽人脸匹配阈值(原值0.55)
FRAME_INTERVAL = 0.5   # 处理帧的间隔(秒)
MIN_FACE_SIZE = 40     # 降低最小人脸尺寸(原值60)以适应更远距离
MAX_FACE_SIZE = 160    # 添加最大人脸尺寸限制
FRAME_TIMEOUT = 3.0  # 无视频流超时时间(秒)
preview_thread = None
preview_running = False
display_thread_obj = None  # 重命名变量
last_flash_time = 0
last_door_time = 0
FLASH_COOLDOWN = 2.0  # 补光冷却时间(秒)
DOOR_COOLDOWN = 3.0   # 开门冷却时间(秒)
last_match_id = None  # 用于跟踪最后一次匹配的人脸ID
MATCH_RESET_TIME = 5.0  # 重置匹配状态的时间(秒)# 全局变量
last_process_time = 0
latest_frame = None
sample_encodings = None
debug_window = False  # 调试窗口开关
frame_lock = threading.Lock()  # 帧锁
last_frame_time = 0  # 最后接收到帧的时间# 允许的文件扩展名
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}IMAGES_DIR = './images'# 确保必要的目录存在
if not os.path.exists(IMAGES_DIR):os.makedirs(IMAGES_DIR)logging.info(f"创建人脸图像目录: {IMAGES_DIR}")def toggle_debug_window():"""切换调试窗口显示状态"""global debug_window, display_thread_obj, preview_runningtry:debug_window = not debug_windowlogging.info(f"调试窗口: {'开启' if debug_window else '关闭'}")if debug_window:# 启动预览线程if display_thread_obj is None or not display_thread_obj.is_alive():preview_running = Truedisplay_thread_obj = threading.Thread(target=display_preview, daemon=True)  # 改用新的函数名display_thread_obj.start()else:# 停止预览线程preview_running = Falseif display_thread_obj and display_thread_obj.is_alive():display_thread_obj.join(timeout=1.0)cv2.destroyAllWindows()except Exception as e:logging.error(f"切换调试窗口错误: {e}")return Falsereturn Truedef display_preview():"""显示预览线程"""global latest_frame, last_frame_timetry:fps_time = time.time()while preview_running:try:current_time = time.time()# 检查是否有活跃的视频流if latest_frame is not None and current_time - last_frame_time < FRAME_TIMEOUT:if debug_window:with frame_lock:frame_to_show = latest_frame.copy()# # 计算FPS# time_diff = current_time - fps_time# fps = 1.0 / time_diff if time_diff > 0 else 0.0# fps_time = current_time# # 显示帧率和状态# cv2.putText(frame_to_show, f"FPS: {fps:.1f}", (10, 30),#             cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)cv2.putText(frame_to_show, "Stream Active", (10, 70),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)# 显示图像cv2.imshow('Debug Window', frame_to_show)# 检查键盘输入key = cv2.waitKey(1) & 0xFFif key == ord('q'):toggle_debug_window()breakelse:# 清除缓存with frame_lock:if latest_frame is not None:latest_frame = Nonelogging.info("视频流超时,清除缓存")if debug_window:# 显示等待画面blank_frame = np.zeros((480, 640, 3), np.uint8)cv2.putText(blank_frame, "Waiting for video stream...", (150, 240),cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)cv2.imshow('Debug Window', blank_frame)cv2.waitKey(1)time.sleep(0.01)  # 控制刷新率except Exception as e:logging.error(f"显示线程错误: {e}")time.sleep(1)if not debug_window:  # 如果窗口被关闭,退出循环breakexcept Exception as e:logging.error(f"预览线程错误: {e}")finally:cv2.destroyAllWindows()logging.info("预览线程已退出")def load_sample_encodings():"""加载样本特征(程序启动时加载一次)"""global sample_encodingstry:# 确保samples目录存在if not os.path.exists(SAMPLE_DIR):os.makedirs(SAMPLE_DIR)logging.warning(f"创建样本目录: {SAMPLE_DIR}")logging.warning("请将人脸样本图片放入samples目录")return# 获取所有样本图片sample_files = [f for f in os.listdir(SAMPLE_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]if not sample_files:logging.warning("samples目录中没有找到图片文件")logging.warning("请将人脸样本图片放入samples目录")return# 加载第一个有效的样本图片encodings = []for sample_file in sample_files:try:sample_path = os.path.join(SAMPLE_DIR, sample_file)sample_image = face_recognition.load_image_file(sample_path)encoding = face_recognition.face_encodings(sample_image)[0]encodings.append(encoding)logging.info(f"成功加载样本: {sample_file}")except Exception as e:logging.error(f"处理样本 {sample_file} 失败: {str(e)}")continueif encodings:sample_encodings = encodingslogging.info(f"共加载 {len(encodings)} 个样本特征")else:logging.error("没有成功加载任何样本特征")except Exception as e:logging.error(f"加载样本特征失败: {e}")sample_encodings = Nonedef send_open_door_command():"""发送开门指令,带冷却时间控制"""global last_door_timecurrent_time = time.time()# 检查是否在冷却时间内if current_time - last_door_time < DOOR_COOLDOWN:logging.debug("开门指令冷却中...")return Falsetry:response = requests.get(ESP32_URL, timeout=2)if response.status_code == 200:last_door_time = current_time  # 更新最后触发时间logging.info("开门指令已发送")return Trueelse:logging.error(f"开门指令发送失败,状态码: {response.status_code}")except Exception as e:logging.error(f"开门指令发送失败: {e}")return Falsedef trigger_flash():"""触发补光LED,带冷却时间控制"""global last_flash_timecurrent_time = time.time()# 检查是否在冷却时间内if current_time - last_flash_time < FLASH_COOLDOWN:logging.debug("补光LED冷却中...")return Falsetry:response = requests.get(LED_URL, timeout=2)if response.status_code == 200:last_flash_time = current_time  # 更新最后触发时间logging.info("补光LED已触发")return Trueelse:logging.error(f"补光LED触发失败,状态码: {response.status_code}")except Exception as e:logging.error(f"补光LED触发失败: {e}")return Falsedef process_frame(frame):"""处理视频帧"""global last_process_time, latest_frame, last_match_idtry:# 1. 调整输入图像大小,保持较高分辨率frame_height, frame_width = frame.shape[:2]if frame_width > 1280:  # 如果分辨率太高,适当降低scale = 1280 / frame_widthframe = cv2.resize(frame, (0, 0), fx=scale, fy=scale)# 2. 图像增强,提高清晰度frame_enhanced = cv2.convertScaleAbs(frame, alpha=1.3, beta=5)  # 轻微提高对比度frame_enhanced = cv2.GaussianBlur(frame_enhanced, (3, 3), 0)  # 轻微降噪# 3. 使用更大的检测图像small_frame = cv2.resize(frame_enhanced, (0, 0), fx=0.75, fy=0.75)  # 改为3/4缩放rgb_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)# 4. 优化人脸检测参数face_locations = face_recognition.face_locations(rgb_frame,model="hog",number_of_times_to_upsample=1)frame_with_face = frame.copy()match_found = Falsemin_distance = 1.0current_face_id = Noneif face_locations:valid_faces = []# 5. 根据人脸大小过滤for face_location in face_locations:top, right, bottom, left = face_locationface_height = bottom - topmin_size = MIN_FACE_SIZE * (small_frame.shape[0] / 480)max_size = MAX_FACE_SIZE * (small_frame.shape[0] / 480)if min_size <= face_height <= max_size:valid_faces.append(face_location)if valid_faces:face_encodings = face_recognition.face_encodings(rgb_frame, valid_faces,num_jitters=3)for (top, right, bottom, left), face_encoding in zip(valid_faces, face_encodings):scale = 1 / 0.75top = int(top * scale)right = int(right * scale)bottom = int(bottom * scale)left = int(left * scale)if sample_encodings is not None:distances = face_recognition.face_distance(sample_encodings, face_encoding)current_min_distance = np.min(distances)avg_distance = np.mean(distances[:3])if current_min_distance <= FACE_THRESHOLD and avg_distance <= FACE_THRESHOLD + 0.1:match_found = Truecolor = (0, 255, 0)face_data = np.concatenate([face_encoding, [current_min_distance, avg_distance]])current_face_id = hash(face_data.tobytes())if current_face_id != last_match_id:if trigger_flash():time.sleep(0.1)save_face_image(frame, (top, right, bottom, left), current_face_id)if send_open_door_command():logging.info(f"人脸匹配成功! 距离: {current_min_distance:.4f}, 平均距离: {avg_distance:.4f}")last_match_id = current_face_idelse:color = (0, 165, 255)else:color = (0, 0, 255)cv2.rectangle(frame_with_face, (left, top), (right, bottom), color, 2)cv2.putText(frame_with_face, f"D: {current_min_distance:.2f}", (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)# 显示帧率和状态current_time = time.time()fps = 1.0 / (current_time - last_process_time)  # 计算FPSlast_process_time = current_time  # 更新最后处理时间cv2.putText(frame_with_face, f"FPS: {fps:.1f}", (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)status_text = "Match!" if match_found else "No Match"status_color = (0, 255, 0) if match_found else (0, 0, 255)cv2.putText(frame_with_face, status_text, (frame.shape[1] - 200, 70),cv2.FONT_HERSHEY_SIMPLEX, 1, status_color, 2)return match_found, frame_with_faceexcept Exception as e:logging.error(f"处理帧错误: {e}")return False, framedef verify_match(frame, original_encoding):"""二次验证人脸匹配"""try:# 使用相同的预处理步骤frame_enhanced = cv2.convertScaleAbs(frame, alpha=1.2, beta=10)small_frame = cv2.resize(frame_enhanced, (0, 0), fx=0.5, fy=0.5)rgb_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)# 检测人脸face_locations = face_recognition.face_locations(rgb_frame, model="hog", number_of_times_to_upsample=2)if face_locations:# 获取新的人脸编码new_encodings = face_recognition.face_encodings(rgb_frame, face_locations, num_jitters=2)if new_encodings:# 比较与原始编码的距离distance = face_recognition.face_distance([original_encoding], new_encodings[0])[0]return distance <= FACE_THRESHOLDexcept Exception as e:logging.error(f"二次验证错误: {e}")return Falsedef save_face_image(frame, face_location, face_id):"""保存识别到的人脸图像"""top, right, bottom, left = face_locationface_image = frame[top:bottom, left:right]  # 截取人脸区域filename = os.path.join(IMAGES_DIR, f"face_{face_id}.jpg")  # 生成文件名cv2.imwrite(filename, face_image)  # 保存图像logging.info(f"保存人脸图像: {filename}")@app.route('/video_stream', methods=['POST'])
def video_stream():"""处理视频流"""global last_process_time, latest_frame, last_frame_timetry:# 控制处理频率current_time = time.time()if current_time - last_process_time < FRAME_INTERVAL:return "skip\n"last_process_time = current_timelast_frame_time = current_time# 解码图像数据data = request.get_data()nparr = np.frombuffer(data, np.uint8)frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)if frame is None:logging.error("无法解码图像数据")return "error\n"# 更新最新帧with frame_lock:latest_frame = frame.copy()# 处理帧进行人脸识别match_result, processed_frame = process_frame(frame)# 更新处理后的帧with frame_lock:latest_frame = processed_frame# 如果匹配成功,发送开门指令if match_result:if send_open_door_command():logging.info("人脸识别成功,已发送开门指令")return "open_door\n"else:logging.warning("人脸识别成功,但开门指令发送失败")return "no_action\n"except Exception as e:logging.error(f"处理视频流错误: {e}")return "error\n"@app.route('/toggle_debug', methods=['GET'])
def toggle_debug():"""切换调试窗口的HTTP端点"""success = toggle_debug_window()return jsonify({'status': 'success' if success else 'error','debug_window': debug_window,'message': '切换成功' if success else '切换失败'})@app.route('/trigger_flash', methods=['GET'])
def flash_control():"""手动触发补光的HTTP端点"""success = trigger_flash()return jsonify({'status': 'success' if success else 'error','message': '补光已触发' if success else '补光触发失败'})@app.route('/upload_sample', methods=['POST'])
def upload_sample():"""处理样本图片上传"""if 'sample_image' not in request.files:return jsonify({'status': 'error', 'message': '没有文件上传'}), 400file = request.files['sample_image']if file.filename == '':return jsonify({'status': 'error', 'message': '未选择文件'}), 400if file and allowed_file(file.filename):filename = secure_filename(file.filename)file_path = os.path.join(SAMPLE_DIR, filename)file.save(file_path)# 重新加载样本特征load_sample_encodings()return jsonify({'status': 'success', 'message': '样本图片上传成功'}), 200else:return jsonify({'status': 'error', 'message': '文件类型不支持'}), 400def allowed_file(filename):"""检查文件扩展名是否允许"""return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONSdef generate_frames():"""生成视频流帧"""try:while True:try:if latest_frame is not None:with frame_lock:frame_to_show = latest_frame.copy()# 转换图像格式ret, buffer = cv2.imencode('.jpg', frame_to_show)if not ret:continue# 生成帧数据frame_data = buffer.tobytes()yield (b'--frame\r\n'b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n')else:# 如果没有帧,生成空白帧blank_frame = np.zeros((480, 640, 3), np.uint8)cv2.putText(blank_frame, "Waiting for video...", (150, 240),cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)ret, buffer = cv2.imencode('.jpg', blank_frame)frame_data = buffer.tobytes()yield (b'--frame\r\n'b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n')time.sleep(0.01)  # 控制帧率except Exception as e:logging.error(f"生成帧错误: {e}")time.sleep(1)except GeneratorExit:logging.info("视频流生成器正常退出")except Exception as e:logging.error(f"视频流生成器错误: {e}")@app.route('/video_feed')
def video_feed():"""视频流路由"""try:logging.info("新的客户端连接到视频流")return Response(generate_frames(),mimetype='multipart/x-mixed-replace; boundary=frame')except Exception as e:logging.error(f"视频流路由错误: {e}")return "Video stream error", 500@app.route('/')
def index():"""网页界面"""return '''<html><head><title>视频流监控</title><style>body { font-family: Arial, sans-serif;margin: 20px;text-align: center;}.container {max-width: 800px;margin: 0 auto;}.video-container {margin: 20px 0;}.controls {margin: 20px 0;}button {padding: 10px 20px;margin: 0 10px;font-size: 16px;cursor: pointer;}#status {margin: 20px 0;padding: 10px;border-radius: 5px;}</style></head><body><div class="container"><h1>ESP32-CAM 视频流监控</h1><div class="video-container"><img src="/video_feed" width="640" height="480"></div><div class="controls"><form id="uploadForm" enctype="multipart/form-data" method="POST" action="/upload_sample" onsubmit="return uploadSample(event)"><input type="file" name="sample_image" accept="image/*" required><button type="submit">上传样本图片</button></form><br> <!-- 增加间隔 --><button onclick="toggleDebug()">切换调试显示</button><button onclick="triggerFlash()">触发补光</button></div><div id="status"></div></div><script>function toggleDebug() {fetch('/toggle_debug').then(response => response.json()).then(data => {document.getElementById('status').innerHTML = `调试窗口: ${data.debug_window ? '开启' : '关闭'}`;});}function triggerFlash() {fetch('/trigger_flash').then(response => response.json()).then(data => {document.getElementById('status').innerHTML = data.message;});}function uploadSample(event) {event.preventDefault();  // 防止表单默认提交const formData = new FormData(document.getElementById('uploadForm'));fetch('/upload_sample', {method: 'POST',body: formData}).then(response => response.json()).then(data => {document.getElementById('status').innerHTML = data.message;  // 显示消息}).catch(error => {document.getElementById('status').innerHTML = '上传失败,请重试。';console.error('上传错误:', error);});}</script></body></html>'''if __name__ == '__main__':try:# 确保必要的目录存在if not os.path.exists(SAMPLE_DIR):os.makedirs(SAMPLE_DIR)logging.info(f"创建样本目录: {SAMPLE_DIR}")# 加载样本特征load_sample_encodings()# 检查是否成功加载了样本if sample_encodings is None or len(sample_encodings) == 0:logging.warning("未能加载任何样本特征,程序将继续运行但无法进行人脸匹配")logging.warning(f"请将人脸样本图片放入目录: {os.path.abspath(SAMPLE_DIR)}")# 启动服务器logging.info("启动服务器...")from waitress import serveserve(app, host='0.0.0.0', port=5000, threads=4)except KeyboardInterrupt:logging.info("程序正常退出")except Exception as e:logging.error(f"程序异常退出: {e}")finally:preview_running = False  # 确保线程能够退出cv2.destroyAllWindows()

- **功能**:

  - 提供视频流服务,允许客户端通过HTTP请求获取实时视频流。

  - 处理人脸识别,识别到人脸后发送开门指令。

  - 支持样本图片上传,用于人脸匹配。

  - 提供调试窗口,显示FPS和流状态。

- **实现逻辑**:

  - 使用Flask框架搭建HTTP服务器,处理客户端请求。

  - 使用OpenCV进行视频流处理和人脸识别。

  - 通过HTTP请求与ESP32-CAM进行通信,控制LED和开门指令。

- **使用方法**:

  1. 启动Flask服务器。

  2. 访问 `/` 路由以查看视频流。

  3. 使用 `/upload_sample` 路由上传人脸样本。

  4. 使用 `/toggle_debug` 路由切换调试窗口。

### 2.2 `ESP32-CAM.ino`

- **功能**:

  - 连接WiFi并与后端服务器进行通信。

  - 实现视频流的捕获和发送。

  - 进行运动检测,触发视频流的启动和停止。

  - 控制LED以提供补光。

  - 保存人脸识别成功后的人脸截图到./images目录下

#include "esp_camera.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>  // 添加WebServer库// 配置区 ============================================
const char *ssid = "你的wifi";          // WiFi名称
const char *password = "密码";    // WiFi密码
const char *serverIP = "192.168.1.101"; // 服务器IP
const int serverPort = 5000;            // 服务器端口
const int ledPin = 4;                   // LED引脚// 摄像头配置 ========================================
#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"// 分辨率配置
#define LOW_RES_FRAME_SIZE FRAMESIZE_QQVGA // 低分辨率(160x120)
#define HIGH_RES_FRAME_SIZE FRAMESIZE_SVGA // 高分辨率(800x600)// 运动检测参数
#define MOTION_THRESHOLD 5000           // 降低阈值,提高灵敏度(原值10000)
#define COOLDOWN_TIME 6000             // 减少冷却时间(原值10000)
#define CHECK_INTERVAL 30              // 缩短检测间隔(原值50)
#define DETECT_REGION_X 40             // 保持不变
#define DETECT_REGION_Y 30             // 保持不变
#define DETECT_REGION_WIDTH 80         // 保持不变
#define DETECT_REGION_HEIGHT 60        // 保持不变WiFiClient tcpClient;
uint8_t *prevFrame = nullptr;// 添加全局变量
bool isStreaming = false;           // 视频流状态
unsigned long lastMotionTime = 0;   // 最后检测到运动的时间
#define STREAM_TIMEOUT 5000        // 无运动后持续摄像时间(ms)10000
#define STREAM_INTERVAL 100         // 视频流帧间隔(ms)WebServer server(80);  // 创建HTTP服务器实例,端口80// 初始化摄像头
void setupCamera(framesize_t frameSize, pixformat_t pixelFormat, uint32_t xclkFreq) {esp_camera_deinit(); // 先释放摄像头资源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_sccb_sda = SIOD_GPIO_NUM;config.pin_sccb_scl = SIOC_GPIO_NUM;config.pin_pwdn = PWDN_GPIO_NUM;config.pin_reset = RESET_GPIO_NUM;config.xclk_freq_hz = xclkFreq;config.frame_size = frameSize;config.pixel_format = pixelFormat;config.jpeg_quality = 10;            // JPEG质量(1-63,值越小质量越高)config.fb_count = 3;config.fb_location = CAMERA_FB_IN_PSRAM;esp_err_t err = esp_camera_init(&config);if (err != ESP_OK) {Serial.printf("Camera init failed: 0x%x\n", err);ESP.restart();}sensor_t *sensor = esp_camera_sensor_get();sensor->set_vflip(sensor, 1);         // 垂直翻转sensor->set_hmirror(sensor, 1);       // 水平镜像
}// 初始化WiFi
void setupWiFi() {Serial.print("Connecting to WiFi...");WiFi.begin(ssid, password);WiFi.setSleep(false);int retries = 0;while (WiFi.status() != WL_CONNECTED && retries < 20) {delay(500);Serial.print(".");retries++;}if (WiFi.status() != WL_CONNECTED) {Serial.println("\nWiFi connection failed!");ESP.restart();}Serial.printf("\nWiFi Connected\nIP Address: %s\n", WiFi.localIP().toString().c_str());// 设置HTTP服务器路由server.on("/open_door", HTTP_GET, handleOpenDoor);server.on("/open_door", HTTP_POST, handleOpenDoor);// 添加补光LED控制路由server.on("/toggle_flash", HTTP_GET, handleToggleFlash);server.on("/toggle_flash", HTTP_POST, handleToggleFlash);server.on("/", HTTP_GET, []() {server.send(200, "text/plain", "ESP32-CAM Server Running");});server.onNotFound([]() {server.send(404, "text/plain", "Not found");});server.begin();Serial.println("HTTP server started");
}// 开门处理函数
void handleOpenDoor() {digitalWrite(ledPin, HIGH);delay(200);digitalWrite(ledPin, LOW);// 发送CORS头,允许跨域请求server.sendHeader("Access-Control-Allow-Origin", "*");server.sendHeader("Access-Control-Allow-Methods", "GET, POST");server.send(200, "text/plain", "Door activated");Serial.println("Door signal received");
}// 修改补光LED控制函数
void handleToggleFlash() {// 闪烁补光LEDdigitalWrite(ledPin, HIGH);delay(100);  // 闪烁100msdigitalWrite(ledPin, LOW);// 发送CORS头和响应server.sendHeader("Access-Control-Allow-Origin", "*");server.sendHeader("Access-Control-Allow-Methods", "GET, POST");server.send(200, "text/plain", "Flash triggered");Serial.println("Flash LED triggered");
}// 修改发送图像函数,添加补光
void sendImage(camera_fb_t *fb) {
static int connectionAttempts = 0; // 静态变量,用于记录连接尝试次数if (!tcpClient.connected()) {tcpClient.stop();if (!tcpClient.connect(serverIP, serverPort)) {Serial.println("Connection to server failed");connectionAttempts++; // 增加连接尝试次数if (connectionAttempts >= 5) { // 如果连接失败10次Serial.println("Switching to low resolution mode.");setupCamera(LOW_RES_FRAME_SIZE, PIXFORMAT_GRAYSCALE, 5000000); // 切换到低分辨率模式connectionAttempts = 0; // 重置计数器}return;}} else {connectionAttempts = 0; // 如果连接成功,重置计数器}// 发送HTTP头String header = "POST /video_stream HTTP/1.1\r\n""Host: " + String(serverIP) + "\r\n""Content-Type: image/jpeg\r\n""Content-Length: " + String(fb->len) + "\r\n\r\n";tcpClient.print(header);Serial.printf("Sending image: %d bytes\n", fb->len);// 分块发送图像数据const size_t chunk_size = 4096;size_t remaining = fb->len;size_t offset = 0;while (remaining > 0) {size_t toWrite = min(chunk_size, remaining);size_t written = tcpClient.write(fb->buf + offset, toWrite);if (written > 0) {remaining -= written;offset += written;} else {Serial.println("Write failed");break;}delay(1);}// 处理响应unsigned long timeout = millis() + 1000;while (millis() < timeout && !tcpClient.available()) {delay(1);}if (tcpClient.available()) {String response = tcpClient.readStringUntil('\n');if (response.indexOf("open_door") != -1) {digitalWrite(ledPin, HIGH);delay(200);digitalWrite(ledPin, LOW);}}// 清理接收缓冲区while (tcpClient.available()) {tcpClient.read();}
}// 修改运动检测函数,添加补光
bool detectMotion() {static unsigned long lastCheck = 0;static unsigned long lastTriggerTime = 0;if (millis() - lastCheck < CHECK_INTERVAL) return false;lastCheck = millis(); camera_fb_t *fb = esp_camera_fb_get();if (!fb || fb->format != PIXFORMAT_GRAYSCALE) {if (fb) esp_camera_fb_return(fb);return false;}uint32_t diff = 0;const int step = 2;  // 减小采样步长,提高精度(原值4)for (int y = DETECT_REGION_Y; y < DETECT_REGION_Y + DETECT_REGION_HEIGHT; y += step) {for (int x = DETECT_REGION_X; x < DETECT_REGION_X + DETECT_REGION_WIDTH; x += step) {int index = y * fb->width + x;if (index < fb->len) {int delta = abs(fb->buf[index] - prevFrame[index]);if (delta > 15) diff += delta;  // 降低像素变化阈值(原值20)}}}memcpy(prevFrame, fb->buf, fb->len);esp_camera_fb_return(fb);if (diff > MOTION_THRESHOLD && (millis() - lastTriggerTime) > COOLDOWN_TIME) {lastTriggerTime = millis();return true;}return false;
}// 获取芯片温度
float getChipTemperature() {return temperatureRead();
}void setup() {Serial.begin(115200);// 设置LED引脚pinMode(ledPin, OUTPUT);digitalWrite(ledPin, LOW);setupCamera(LOW_RES_FRAME_SIZE, PIXFORMAT_GRAYSCALE, 10000000);setupWiFi();// 只安装一次GPIO中断服务if (gpio_install_isr_service(0) == ESP_OK) {// 安装中断服务}// 分配内存用于运动检测camera_fb_t *fb = esp_camera_fb_get();if (fb) {prevFrame = (uint8_t*)malloc(fb->len);if (!prevFrame) {Serial.println("Failed to allocate memory for prevFrame");ESP.restart();}memcpy(prevFrame, fb->buf, fb->len);esp_camera_fb_return(fb);}
}void loop() {// 确保及时处理HTTP请求server.handleClient();static unsigned long lastFrameTime = 0;unsigned long currentTime = millis();// 检查WiFi连接if (WiFi.status() != WL_CONNECTED) {Serial.println("WiFi disconnected. Reconnecting...");WiFi.reconnect();delay(5000);return;}// 检查温度float temperature = getChipTemperature();if (temperature > 70.0) {Serial.println("板载温度过高,强行进入冷却。当前温度:" + String(temperature) + "°C");isStreaming = false;setupCamera(LOW_RES_FRAME_SIZE, PIXFORMAT_GRAYSCALE, 5000000);delay(10000);return;}if (!isStreaming) {// 低分辨率运动检测模式if (detectMotion()) {Serial.println("Motion detected, starting video stream");isStreaming = true;lastMotionTime = currentTime;setupCamera(HIGH_RES_FRAME_SIZE, PIXFORMAT_JPEG, 20000000);}} else {// 视频流模式if (currentTime - lastFrameTime >= STREAM_INTERVAL) {lastFrameTime = currentTime;// 在视频流模式下也检测运动if (detectMotion()) {lastMotionTime = currentTime;}// 获取并发送高清图像camera_fb_t *fb = esp_camera_fb_get();if (fb) {sendImage(fb);esp_camera_fb_return(fb);}// 检查是否需要停止视频流if (currentTime - lastMotionTime >= STREAM_TIMEOUT) {Serial.println("未检测到运动,停止视频流。  当前板载温度:"+ String(temperature) + "°C");isStreaming = false;setupCamera(LOW_RES_FRAME_SIZE, PIXFORMAT_GRAYSCALE, 10000000);}}}delay(1);  // 防止看门狗复位
}

- **实现逻辑**:

  - 使用ESP32-CAM模块捕获视频帧,并通过TCP连接将图像数据发送到后端服务器。

  - 通过GPIO中断检测运动,并根据运动状态切换视频流的分辨率。

  - 监测板载温度,防止过热。

  - 通过HTTP请求与后端服务器进行通信,控制LED和开门指令。

  - 超温保护:70度,自动降频,防止过热。esp32cam最头疼的就是板子过热烫手。

- **使用方法**:

  1. 将代码上传到ESP32-CAM。

  2. 确保WiFi连接正常。

  3. 通过运动检测触发视频流,或手动触发补光。

  4. 通过后端服务器控制开门指令。

  注:

 # 运行后端服务器端,需先安装python环境,python3.10以上。编译器使用VScode,其它也可以

    from flask import Flask, request, jsonify, Response

    import cv2

    import numpy as np

    import face_recognition

    import logging

    import requests

    import time

    import threading

    import os

    from werkzeug.utils import secure_filename

    所需库说明

    Flask:用于创建Web应用和处理HTTP请求。

    OpenCV (cv2):用于图像处理和视频流操作。

    3. NumPy (np):用于处理数组和矩阵运算。

    4. face_recognition:用于人脸识别功能。

    logging:用于记录日志信息。

    requests:用于发送HTTP请求。

    7. time:用于时间相关的操作。

    threading:用于多线程处理。

    9. os:用于文件和目录操作。

    werkzeug.utils:用于安全文件名处理。

    将这些库的导入部分保留在你的项目中,以确保代码能够正常运行。

    然后执行  python video.py

    2025-02-07 12:36:40,358 - INFO - 启动服务器...

    2025-02-07 12:36:40,394 - INFO - Serving on http://0.0.0.0:5000

   

    浏览器地址栏输入: http://127.0.0.1:5000/

   

 # 触发一次补光闪烁

    curl http://192.168.1.104/toggle_flash     格式:【http://ESP32-CAM的IP/toggle_flash】

# 触发一次开门

    curl http://192.168.1.104/open_door      格式:【http://ESP32-CAM的IP/open_door】

## 3. 总结

本项目结合了人脸识别、视频流传输和运动检测技术,提供了一个完整的智能门禁解决方案。通过合理的代码结构和模块化设计,确保了系统的可扩展性和可维护性。

python服务器端可生成exe文件,可docker镜像。需要请留言。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com