【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镜像。需要请留言。
相关文章:
【ESP32cam人脸识别开门及服务器端实战源码】
本项目实现了一个基于ESP32-CAM的实时人脸识别系统,能够通过WiFi进行视频流传输,并在检测到人脸时触发开门指令。系统由两个主要部分组成:video.py(后端服务器)和 ESP32-CAM.ino(ESP32-CAM固件)…...
【自开发工具介绍】SQLSERVER的ImpDp和ExpDp工具04
SQLSERVER的ImpDp和ExpDp工具演示 1、指定某些表作为导出对象外 (-exclude_table) 验证用:导出的表,导入到新的数据库 2、指定某些表作为导出对象外 (-exclude_table) 支持模糊检索,可以使用星号 以s开头的表作为导出对象外,…...
什么是中间件中间件有哪些
什么是中间件? 中间件(Middleware)是指在客户端和服务器之间的一层软件组件,用于处理请求和响应的过程。 中间件是指介于两个不同系统之间的软件组件,它可以在两个系统之间传递、处理、转换数据,以达到协…...
WebSocket connection failed 解决
WebSocket connection failed 解决 前言 这里如果是新手小白不知道 WebSocket 是什么的? 怎么使用的?或者想深入了解的 那可以 点击这里 几分钟带你快速了解并使用,已经一些进阶讲解; WebSocket,多应用于需要双向数据…...
C语言:取出32位数据的高十六位
目录 背景 目标 操作步骤 1. 右移 16 位 2. 掩码操作(可选) 代码实现 解释: 输出: 总结: 背景 假设我们有一个 32 位的无符号整数,通常它是由 4 个字节组成的。每个字节由 8 位构成,4…...
JUnit 5 条件测试注解详解
JUnit 5 条件测试注解详解 JUnit 5 提供了一系列条件测试注解,允许开发者根据运行时环境、配置或自定义逻辑动态决定是否执行测试。这些注解能有效减少误报,提升测试的灵活性和适应性。以下是所有条件测试注解的详细介绍及示例: 一、条件测试…...
1 Java 基础面试题(上)
文章目录 前言1. Java 中的序列化和反序列化是什么?1.1 序列化(Serialization)1.2 反序列化(Deserialization)1.3 serialVersionUID1.4 序列化的应用场景1.5 Transient 关键字 2. 为什么 Java 里面不支持多重继承&…...
个人笔记---关于详解threadlocal 上下文环境存储的最佳数据类型
个人原因很久没有写代码,对于一些基础的数据类型有一些忘记,可以根据gpt和我当时的问答进行复习 关于拦截器,由于在请求的到达controller处理器之前,拦截器(当然过滤器也可以实现,我感觉都差不多)就把上下文设置在了线程副本中,那么这个请求到处理器的这些代码进行查询出来的上…...
JVM监控和管理工具
基础故障处理工具 jps jps(JVM Process Status Tool):Java虚拟机进程状态工具 功能 1:列出正在运行的虚拟机进程 2:显示虚拟机执行主类(main()方法所在的类) 3:显示进程ID(PID,Process Identifier) 命令格式 jps […...
【数据结构】树哈希
目录 一、树的同构1. 定义2. 具体理解(1) 结点对应(2) 孩子相同(3) 递归性质 3. 示例 二、树哈希1.定义2.哈希过程(1)叶节点哈希(2)非叶节点哈希(3)组合哈希值 3.性质(1) 唯一性 \re…...
UE5 蓝图学习计划 - Day 12:存储与加载
在游戏开发中,存储(Save)与加载(Load) 系统至关重要,玩家需要能够保存游戏进度、角色状态、道具数据等信息,并在下次启动游戏时恢复它们。UE5 提供了 SaveGame 蓝图类,帮助开发者快速…...
18爬虫:关于playwright相关内容的学习
1.如何在python中安装playwright 打开pycharm,进入终端,输入如下的2个命令行代码即可自动完成playwright的安装 pip install playwright ——》在python中安装playwright第三方模块 playwright install ——》安装playwright所需的工具插件和所支持的…...
图解BWT(Burrows-Wheeler Transform) 算法
Burrows-Wheeler Transform (BWT) 是一种数据转换算法, 主要用于数据压缩领域. 它由 Michael Burrows 和 David Wheeler 在 1994 年提出, 广泛应用于无损数据压缩算法(如 bzip2)中. BWT 的核心思想是通过重新排列输入数据, 使得相同的字符更容易聚集在一起, 从而提高后续压缩算…...
CMake轻松实现把编译生成文件分类输出到指定路径,同时又拷贝一份到别的指定路径(Window/Linux通用)
使用CMake管理的C项目工程你是否有以下需求: 1.项目编译时将生成的文件分类自动输出到指定位置; 2.除了上面输出到指定位置以外,还要拷贝一份到指定位置(包含头文件,配置文件,第三方依赖库文件等…...
AJAX笔记原理篇
黑马程序员视频地址: AJAX-Day03-01.XMLHttpRequest_基本使用https://www.bilibili.com/video/BV1MN411y7pw?vd_source0a2d366696f87e241adc64419bf12cab&spm_id_from333.788.videopod.episodes&p33https://www.bilibili.com/video/BV1MN411y7pw?vd_sour…...
C32.【C++ Cont】静态实现双向链表及STL库的list
目录 1.知识回顾 2.静态实现演示图 3.静态实现代码 1.初始双向链表 2.头插 3.遍历链表 4.查找某个值 4.任意位置之后插入元素 5.任意位置之前插入元素 6.删除任意位置的元素 4.STL库的list 1.知识回顾 96.【C语言】数据结构之双向链表的初始化,尾插,打印和尾删 97.【C…...
【Elasticsearch】terms聚合误差问题
Elasticsearch中的聚合查询在某些情况下确实可能存在误差,尤其是在处理分布式数据和大量唯一值时。这种误差主要来源于以下几个方面: 1.分片数据的局部性 Elasticsearch的索引通常被分成多个分片,每个分片独立地计算聚合结果。由于数据在分…...
2-kafka服务端之延时操作实现原理
文章目录 背景案例延时生产实现原理延时拉取实现原理 总结 背景 上篇我们说到了kafka时间轮是延时操作内部实现的重要数据结构,这篇我们来说下kafka内部的延时操作实现原理。这里我们以延时生产和延时拉取为例说明延时操作的实现原理。 案例 延时生产 我们知道如…...
UE求职Demo开发日志#22 显示人物信息,完善装备的穿脱
1 创建一个人物信息显示的面板,方便测试 简单弄一下: UpdateInfo函数: 就是获取ASC后用属性更新,就不细看了 2 实现思路 在操作目标为装备栏,或者操作起点为装备栏时,交换前先判断能否交换(只…...
【DeepSeek论文精读】6. DeepSeek R1:通过强化学习激发大语言模型的推理能力
欢迎关注[【youcans的AGI学习笔记】](https://blog.csdn.net/youcans/category_12244543.html)原创作品 【DeepSeek论文精读】1. 从 DeepSeek LLM 到 DeepSeek R1 【DeepSeek论文精读】2. DeepSeek LLM:以长期主义扩展开源语言模型 【DeepSeek论文精读】…...
顺丰大数据开发面试题及参考答案
Flink 的提交过程是怎样的? Flink 的提交过程通常包含以下步骤: 代码编写与打包:开发人员首先使用 Flink 提供的 API 编写数据处理逻辑,包括定义数据源、转换操作和数据 sink 等。完成代码编写后,将项目打包成可执行的 JAR 文件,其中包含了所有依赖的库和资源。选择提交方…...
C# 函数多个返回值
有时候需要从C#函数中返回多个返回值,而且返回值的类型又不一样,这个时候又不能用数组或者list。 其实C#函数是支持多个不同类型的返回值的,请参看下面的code. //多返回值函数定义 (string name, int age) GetNameAge(int id) {return (&qu…...
Deepseek 接入Word处理对话框(隐藏密钥)
硅基流动邀请码:1zNe93Cp 邀请链接:网页链接 亲测deepseek接入word,自由调用对话,看截图有兴趣的复用代码(当然也可以自己向deepseek提问,帮助你完成接入,但是提问逻辑不一样给出的答案是千差万…...
RabbitMQ深度探索:简单实现 MQ
基于多线程队列实现 MQ : 实现类: public class ThreadMQ {private static LinkedBlockingDeque<JSONObject> broker new LinkedBlockingDeque<JSONObject>();public static void main(String[] args) {//创建生产者线程Thread producer n…...
Baklib赋能数字内容体验个性化推荐提升用户体验的未来之路
内容概要 随着数字化时代的不断发展,用户对内容消费的需求日益多样化,个性化推荐成为提升用户体验的重要手段。Baklib以其先进的技术手段,在数字内容领域内积极推动个性化推荐的实施,从而满足用户在信息获取和内容消费中的独特需…...
使用 TensorRT 和 Python 实现高性能图像推理服务器
在现代深度学习和计算机视觉应用中,高性能推理是关键。本文将介绍如何使用 TensorRT 和 Python 构建一个高性能的图像推理服务器。该服务器能够接收客户端发送的图像数据,使用 TensorRT 进行推理,并将结果返回给客户端。 1. 概述 1.1 项目目…...
[MySQL#1] database概述 常见的操作指令 MySQL架构 存储引擎
#1024程序员节|征文# 目录 一. 数据库概念 0.连接服务器 1. 什么是数据库 口语中的数据库 为什么数据不直接以文件形式存储,而需要使用数据库呢? 总结 二. ??基础操作 三. 主流数据库 四. 基础知识 服务器,数据库&…...
WebAssembly:前后端开发的未来利器
引言 在互联网的世界里,前端和后端开发一直是两块重要的领域。而 JavaScript 长期以来是前端的霸主,后端则有各种语言诸如 Java、Python、Node.js、Go 等等。然而,近年来一个名为 WebAssembly (Wasm) 的技术正在逐渐改变这一格局。它的高性能…...
Spring Task之Cron表达式
🌟 Spring Task高能预警:你以为的Cron表达式可能都是错的!【附实战避坑指南】 开篇暴击:为什么你的定时任务总在凌晨3点翻车? “明明设置了0 0 2 * * ?,为什么任务每天凌晨3点执行?” —— 来…...
deepseek API 调用-python
【1】创建 API keys 【2】安装openai SDK pip3 install openai 【3】代码: https://download.csdn.net/download/notfindjob/90343352...
数字滤波器的分类
数字滤波器可以根据不同的标准进行分类,以下是几种常见的分类方式: 1. 按实现结构分类 FIR滤波器(有限脉冲响应滤波器) - 特点:系统的脉冲响应在有限时间内衰减到零。 - 优点:线性相位特性(保…...
iOS 老项目适配 #Preview 预览功能
前言 iOS 开发者 最憋屈的就是UI 布局慢,一直以来没有实时预览功能,虽然swiftUI 早就支持了,但是目前主流还是使用UIKit在布局,iOS 17 苹果推出了 #Preview 可以支持UIKit 实时预览,但是仅仅是 iOS 17,老项目怎么办呢?于是就有了这篇 老项目适配 #Preview 预览 的文章,…...
高等代数笔记—域与一元多项式
域与环 数域 F F F:至少包含两个元素且对加减乘除运算封闭的复数集合 F F F,其中作除运算时除数不为0。 封闭:集合 F F F中的两个元素作某一运算的结果仍属于集合 F F F,则称 F F F对该运算封闭。 Q , R , C \mathbb{Q}, \mathbb…...
【C语言设计模式学习笔记1】面向接口编程/简单工厂模式/多态
面向接口编程可以提供更高级的抽象,实现的时候,外部不需要知道内部的具体实现,最简单的是使用简单工厂模式来进行实现,比如一个Sensor具有多种表示形式,这时候可以在给Sensor结构体添加一个enum类型的type,…...
2.Python基础知识:注释、变量以及数据类型、标识符和关键字、输入函数、输出函数、运算符、程序类型转换
1. 注释 注释是用来解释代码,增强代码可读性的部分。在 Python 中,注释分为单行注释和多行注释。 单行注释:以 # 开头,后面的内容都被视为注释。 # 这是一个单行注释 print("Hello, World!") # 输出 "Hello, Wor…...
介绍10个比较优秀好用的Qt相关的开源库
记录下比较好用的一些开源库 1. Qt中的日志库“log4qt” log4qt 是一个基于 Apache Log4j 设计理念的 Qt 日志记录库,它为 Qt 应用程序提供了强大而灵活的日志记录功能。Log4j 是 Java 领域广泛使用的日志框架,log4qt 借鉴了其优秀的设计思想ÿ…...
利用Muduo库实现简单且健壮的Echo服务器
一、muduo网络库主要提供了两个类: TcpServer:用于编写服务器程序 TcpClient:用于编写客户端程序 二、三个重要的链接库: libmuduo_net、libmuduo_base、libpthread 三、muduo库底层就是epoll线程池,其好处是…...
渗透测试之文件包含漏洞 超详细的文件包含漏洞文章
目录 说明 通常分为两种类型: 本地文件包含 典型的攻击方式1: 影响: 典型的攻击方式2: 包含路径解释: 日志包含漏洞: 操作原理 包含漏洞读取文件 文件包含漏洞远程代码执行漏洞: 远程文件包含…...
高性能 :DeepSeek-V3 inference 推理时反量化实现 fp8_cast_bf16
FP8 (8 bits) & FP16 (16 bits) FP8 和 BF16 都是浮点数格式(floating-point formats),float通过科学计数法表示数据,float [符号位指数位系数位] FP8 (8 bits):SEEEMMMMFP16 (16 bits):SEEEEEMMMMM…...
kakailio官网推荐的安装流程ubuntu 22.04
https://kamailio.org/docs/tutorials/6.0.x/kamailio-install-guide-git/ # 非必须项 wget -O- https://deb.kamailio.org/kamailiodebkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/kamailio.gpg在/etc/apt/sources.list文件追加以下内容 deb [signed-by/usr/sh…...
能否通过蓝牙建立TCP/IP连接来传输数据
前言: 最近在做一个项目时,产生了一个疑问:能否通过蓝牙建立TCP/IP连接来传输数据 查阅了一些文章,可以得出结论:不行 下面是我截取的两篇个人认可的文章的回答: 文章一: 蓝牙是一种短距离无…...
git基础使用--1--版本控制的基本概念
文章目录 git基础使用--1--版本控制的基本概念1.版本控制的需求背景,即为啥需要版本控制2. 集中式版本控制SVN3. 分布式版本控制 Git4. SVN和Git的比较 git基础使用–1–版本控制的基本概念 1.版本控制的需求背景,即为啥需要版本控制 先说啥叫版本&…...
高端入门:Ollama 本地高效部署DeepSeek模型深度搜索解决方案
目录 一、Ollama 介绍 二、Ollama下载 2.1 官网下载 2.2 GitHub下载 三、模型库 四、Ollmal 使用 4.1 模型运行(下载) 4.2 模型提问 五、Ollama 常用命令 相关推荐 一、Ollama 介绍 Ollama是一个专为在本地机器上便捷部署和运行大型语言模型&…...
高级java每日一道面试题-2025年01月30日-框架篇[SpringBoot篇]-如何理解 Spring Boot 配置加载顺序 ?
如果有遗漏,评论区告诉我进行补充 面试官: 如何理解 Spring Boot 配置加载顺序 ? 我回答: 在 Java 高级面试中讨论 Spring Boot 配置加载顺序时,理解其机制对于有效管理和调试应用程序配置至关重要。Spring Boot 通过一系列预定义的规则来确定如何加载和覆盖配置…...
代码随想录day06
242.有效的字母异位词 刚学哈希表想着使用unordered_set来实现,结果无法通过,原因是对字母异位词理解有问题,字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。对字母出现的次数有要求&am…...
C#常用744单词
1.visual 可见的 2.studio 工作室 3.dot 点 4.net 网 5.harp 尖端的,锋利的。 6.amework 骨架,构架,框架 7.beta 测试版,试用版 8.XML(全称:eXtensible Markup Language)…...
14.PPT:中国注册税务师协会宣传【26】
目录 NO12 NO3/4/5 NO678 【文本框水平/垂直居中】【文本框内容水平/垂直居中】 NO12 坑:注意❗Word文档的PPt素材.docx的标题大纲是混乱的,虽然他设置了,所以我们需要重新设置 设计→主题视图→幻灯片母版→删除版式插入logo NO3/4…...
Python大数据可视化:基于Python的王者荣耀战队的数据分析系统设计与实现_flask+hadoop+spider
开发语言:Python框架:flaskPython版本:python3.7.7数据库:mysql 5.7数据库工具:Navicat11开发软件:PyCharm 系统展示 管理员登录 管理员功能界面 比赛信息管理 看板展示 系统管理 摘要 本文使用Python与…...
简单3步部署本地国产大模型DeepSeek大模型
简单3步部署本地国产大模型DeepSeek大模型 DeepSeek是最近非常火的开源大模型,国产大模型 DeepSeek 凭借其优异的性能和对硬件资源的友好性,受到了众多开发者的关注。 无奈,在使用时候deepseek总是提示服务器繁忙,请稍后再试。 …...
Redis常见数据类型与编码方式
⭐️前言⭐️ 本小节围绕Redis中常见的数据类型与编码方式展开。 🍉欢迎点赞 👍 收藏 ⭐留言评论 🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言 🍉博客中涉及源码及博主日常练习代码均已上传GitHu…...