眨眼频率、眼动分析、心率、视频录制

This commit is contained in:
邓智航
2026-01-25 15:19:01 +08:00
parent 6e882d2aa4
commit b3997c2646
7 changed files with 977 additions and 188 deletions

View File

@@ -0,0 +1,139 @@
import numpy as np
import collections
from scipy import signal
class HeartRateMonitor:
def __init__(self, fps=30, window_size=300):
self.fps = fps
self.buffer_size = window_size
# 存储 RGB 三个通道的均值
self.times = np.zeros(window_size)
self.r_buffer = collections.deque(maxlen=window_size)
self.g_buffer = collections.deque(maxlen=window_size)
self.b_buffer = collections.deque(maxlen=window_size)
# 滤波器状态
self.bp_b, self.bp_a = self._create_bandpass_filter(0.75, 2.5, fps) # 45-150 BPM
# 平滑结果用的
self.bpm_history = collections.deque(maxlen=10)
def _create_bandpass_filter(self, lowcut, highcut, fs, order=5):
"""创建巴特沃斯带通滤波器"""
nyq = 0.5 * fs
low = lowcut / nyq
high = highcut / nyq
b, a = signal.butter(order, [low, high], btype='band')
return b, a
def _pos_algorithm(self, r, g, b):
"""
POS (Plane-Orthogonal-to-Skin) 算法
比单纯的绿色通道法强在一个地方:抗运动干扰
"""
# 1. 归一化 (除以均值)
# 加上 1e-6 防止除零
r_n = r / (np.mean(r) + 1e-6)
g_n = g / (np.mean(g) + 1e-6)
b_n = b / (np.mean(b) + 1e-6)
# 2. 投影到色度平面 (Matplotlib 里的经典公式)
# S1 = G - B
# S2 = G + B - 2R
s1 = g_n - b_n
s2 = g_n + b_n - 2 * r_n
# 3. Alpha 微调 (Alpha Tuning)
# 这一步是为了消除镜面反射带来的运动噪声
alpha = np.std(s1) / (np.std(s2) + 1e-6)
# 4. 融合信号
h = s1 + alpha * s2
return h
def process_frame(self, frame, face_loc):
"""
输入: 原始无损 frame (BGR), 人脸框 (top, right, bottom, left)
输出: BPM 数值 或 None (数据不够时)
"""
top, right, bottom, left = face_loc
# --- 1. ROI 提取与保护 ---
h_img, w_img = frame.shape[:2]
# 缩小 ROI 范围:只取脸中心 50% 区域 (避开背景和边缘)
h_box = bottom - top
w_box = right - left
# 修正 ROI 坐标
roi_top = int(max(0, top + h_box * 0.3))
roi_bottom = int(min(h_img, bottom - h_box * 0.3))
roi_left = int(max(0, left + w_box * 0.3))
roi_right = int(min(w_img, right - w_box * 0.3))
roi = frame[roi_top:roi_bottom, roi_left:roi_right]
if roi.size == 0:
return None
# --- 2. 提取 RGB 均值 ---
# OpenCV 是 BGR
b_mean = np.mean(roi[:, :, 0])
g_mean = np.mean(roi[:, :, 1])
r_mean = np.mean(roi[:, :, 2])
self.r_buffer.append(r_mean)
self.g_buffer.append(g_mean)
self.b_buffer.append(b_mean)
# 数据不够,返回 None
if len(self.r_buffer) < self.buffer_size:
progress = int(len(self.r_buffer) / self.buffer_size * 100)
return None # 或者返回 progress 表示进度
# --- 3. 信号处理 (核心升级部分) ---
r = np.array(self.r_buffer)
g = np.array(self.g_buffer)
b = np.array(self.b_buffer)
# A. 使用 POS 算法融合三通道 (抗干扰)
pulse_signal = self._pos_algorithm(r, g, b)
# B. 消除直流分量 (Detrending)
# 这一步去掉了光线缓慢变化的干扰
pulse_signal = signal.detrend(pulse_signal)
# C. 带通滤波 (Bandpass Filter)
# 只保留 0.75Hz - 2.5Hz 的信号
pulse_signal = signal.filtfilt(self.bp_b, self.bp_a, pulse_signal)
# --- 4. 频域分析 (FFT) ---
# 加汉宁窗 (减少频谱泄露)
window = np.hanning(len(pulse_signal))
pulse_signal_windowed = pulse_signal * window
# FFT
fft_res = np.fft.rfft(pulse_signal_windowed)
freqs = np.fft.rfftfreq(len(pulse_signal), 1.0/self.fps)
mag = np.abs(fft_res)
# D. 寻找峰值
# 限制频率范围 (45 BPM - 180 BPM)
interest_idx = np.where((freqs >= 0.75) & (freqs <= 3.0))
valid_freqs = freqs[interest_idx]
valid_mags = mag[interest_idx]
if len(valid_mags) == 0:
return None
max_idx = np.argmax(valid_mags)
peak_freq = valid_freqs[max_idx]
bpm = peak_freq * 60.0
# --- 5. 结果平滑 ---
# 防止数字乱跳
self.bpm_history.append(bpm)
avg_bpm = np.mean(self.bpm_history)
return int(avg_bpm)

View File

@@ -3,17 +3,28 @@ import mediapipe as mp
import time import time
import numpy as np import numpy as np
from collections import deque from collections import deque
from geometry_utils import calculate_ear, calculate_mar_simple, estimate_head_pose, LEFT_EYE, RIGHT_EYE from geometry_utils import (
calculate_ear,
calculate_mar_simple,
calculate_iris_pos,
estimate_head_pose,
LEFT_EYE,
RIGHT_EYE,
LEFT_EYE_GAZE_IDXS,
RIGHT_EYE_GAZE_IDXS,
)
from face_library import FaceLibrary from face_library import FaceLibrary
try: try:
from new_emotion_test import analyze_emotion_with_hsemotion from new_emotion_test import analyze_emotion_with_hsemotion
HAS_EMOTION_MODULE = True HAS_EMOTION_MODULE = True
except ImportError: except ImportError:
print("⚠️ 未找到 new_emotion_test.py情绪功能将不可用") print("⚠️ 未找到 new_emotion_test.py情绪功能将不可用")
HAS_EMOTION_MODULE = False HAS_EMOTION_MODULE = False
class MonitorSystem: class MonitorSystem:
def __init__(self, face_db): def __init__(self, face_db):
# 初始化 MediaPipe # 初始化 MediaPipe
@@ -22,7 +33,7 @@ class MonitorSystem:
max_num_faces=1, max_num_faces=1,
refine_landmarks=True, refine_landmarks=True,
min_detection_confidence=0.5, min_detection_confidence=0.5,
min_tracking_confidence=0.5 min_tracking_confidence=0.5,
) )
# 初始化人脸底库 # 初始化人脸底库
@@ -42,12 +53,13 @@ class MonitorSystem:
self.HISTORY_LEN = 5 self.HISTORY_LEN = 5
self.ear_history = deque(maxlen=self.HISTORY_LEN) self.ear_history = deque(maxlen=self.HISTORY_LEN)
self.mar_history = deque(maxlen=self.HISTORY_LEN) self.mar_history = deque(maxlen=self.HISTORY_LEN)
self.iris_ratio_history = [
deque(maxlen=self.HISTORY_LEN),
deque(maxlen=self.HISTORY_LEN),
]
# 缓存上一次的检测结果 # 缓存上一次的检测结果
self.cached_emotion = { self.cached_emotion = {"label": "detecting...", "va": (0.0, 0.0)}
"label": "detecting...",
"va": (0.0, 0.0)
}
def _get_smoothed_value(self, history, current_val): def _get_smoothed_value(self, history, current_val):
"""内部函数:计算滑动平均值""" """内部函数:计算滑动平均值"""
@@ -69,15 +81,20 @@ class MonitorSystem:
"has_face": False, "has_face": False,
"ear": 0.0, "ear": 0.0,
"mar": 0.0, "mar": 0.0,
"iris_ratio": (0.5, 0.5), # 0最左/上1最右/下
"pose": (0, 0, 0), "pose": (0, 0, 0),
"identity": self.current_user, "identity": self.current_user,
"emotion_label": self.cached_emotion["label"], "emotion_label": self.cached_emotion["label"],
"emotion_va": self.cached_emotion["va"] "emotion_va": self.cached_emotion["va"],
"landmark": (0, w, h, 0),
"frame": frame,
} }
if not results.multi_face_landmarks: if not results.multi_face_landmarks:
self.ear_history.clear() self.ear_history.clear()
self.mar_history.clear() self.mar_history.clear()
self.iris_ratio_history[0].clear()
self.iris_ratio_history[1].clear()
return analysis_data return analysis_data
analysis_data["has_face"] = True analysis_data["has_face"] = True
@@ -95,33 +112,86 @@ class MonitorSystem:
right = np.array([landmarks[308].x * w, landmarks[308].y * h]) right = np.array([landmarks[308].x * w, landmarks[308].y * h])
raw_mar = calculate_mar_simple(top, bottom, left, right) raw_mar = calculate_mar_simple(top, bottom, left, right)
# 计算虹膜位置
left_iris_ratio = calculate_iris_pos(landmarks, LEFT_EYE_GAZE_IDXS, w, h)
right_iris_ratio = calculate_iris_pos(landmarks, RIGHT_EYE_GAZE_IDXS, w, h)
raw_iris_ratio = (
(left_iris_ratio[0] + right_iris_ratio[0]) / 2.0,
(left_iris_ratio[1] + right_iris_ratio[1]) / 2.0,
)
# --- 使用 History 进行数据平滑 --- # --- 使用 History 进行数据平滑 ---
smoothed_ear = self._get_smoothed_value(self.ear_history, raw_ear) smoothed_ear = self._get_smoothed_value(self.ear_history, raw_ear)
smoothed_mar = self._get_smoothed_value(self.mar_history, raw_mar) smoothed_mar = self._get_smoothed_value(self.mar_history, raw_mar)
smoothed_iris_ratio = (
(self._get_smoothed_value(self.iris_ratio_history[0], raw_iris_ratio[0])),
(self._get_smoothed_value(self.iris_ratio_history[1], raw_iris_ratio[1])),
)
# 计算头部姿态 # 计算头部姿态
pitch, yaw, roll = estimate_head_pose(landmarks, w, h) pitch, yaw, roll = estimate_head_pose(landmarks, w, h)
analysis_data.update({ analysis_data.update(
{
"ear": round(smoothed_ear, 4), "ear": round(smoothed_ear, 4),
"mar": round(smoothed_mar, 4), "mar": round(smoothed_mar, 4),
"pose": (int(pitch), int(yaw), int(roll)) "iris_ratio": (
}) round(smoothed_iris_ratio[0], 4),
round(smoothed_iris_ratio[1], 4),
),
"pose": (int(pitch), int(yaw), int(roll)),
}
)
now = time.time()
# --- 身份识别 ---
if now - self.last_identity_check_time > self.IDENTITY_CHECK_INTERVAL:
xs = [l.x for l in landmarks] xs = [l.x for l in landmarks]
ys = [l.y for l in landmarks] ys = [l.y for l in landmarks]
# 计算人脸框 # 计算人脸框
face_loc = ( face_loc = (
int(min(ys) * h), int(max(xs) * w), int(min(ys) * h - 0.1 * h),
int(max(ys) * h), int(min(xs) * w) int(max(xs) * w + 0.1 * w),
int(max(ys) * h + 0.1 * h),
int(min(xs) * w - 0.1 * w),
) )
pad = 20 pad = 30
face_loc = (max(0, face_loc[0]-pad), min(w, face_loc[1]+pad), face_loc = (
min(h, face_loc[2]+pad), max(0, face_loc[3]-pad)) max(0, face_loc[0] - pad),
min(w, face_loc[1] + pad),
min(h, face_loc[2] + pad),
max(0, face_loc[3] - pad),
)
analysis_data["landmark"] = face_loc
# --- ROI处理(对比选择在哪里实现) ---
top = face_loc[0]
right = face_loc[1]
bottom = face_loc[2]
left = face_loc[3]
scale_factor = 10
small_bg = cv2.resize(
frame, (w // scale_factor, h // scale_factor), interpolation=cv2.INTER_LINEAR
)
# 使用 INTER_NEAREST 马赛克效果
# 使用 INTER_LINEAR 毛玻璃模糊效果
blurred_frame = cv2.resize(small_bg, (w, h), interpolation=cv2.INTER_LINEAR)
face_roi = frame[top:bottom, left:right]
blurred_frame[top:bottom, left:right] = face_roi
analysis_data["frame"] = blurred_frame
now = time.time()
# --- 身份识别 ---
if now - self.last_identity_check_time > self.IDENTITY_CHECK_INTERVAL:
# xs = [l.x for l in landmarks]
# ys = [l.y for l in landmarks]
# # 计算人脸框
# face_loc = (
# int(min(ys) * h), int(max(xs) * w),
# int(max(ys) * h), int(min(xs) * w)
# )
# pad = 20
# face_loc = (max(0, face_loc[0]-pad), min(w, face_loc[1]+pad),
# min(h, face_loc[2]+pad), max(0, face_loc[3]-pad))
match_result = self.face_lib.identify(rgb_frame, face_location=face_loc) match_result = self.face_lib.identify(rgb_frame, face_location=face_loc)
if match_result: if match_result:
@@ -131,7 +201,9 @@ class MonitorSystem:
analysis_data["identity"] = self.current_user analysis_data["identity"] = self.current_user
# --- 情绪识别 --- # --- 情绪识别 ---
if HAS_EMOTION_MODULE and (now - self.last_emotion_check_time > self.EMOTION_CHECK_INTERVAL): if HAS_EMOTION_MODULE and (
now - self.last_emotion_check_time > self.EMOTION_CHECK_INTERVAL
):
if results.multi_face_landmarks: if results.multi_face_landmarks:
landmarks = results.multi_face_landmarks[0].landmark landmarks = results.multi_face_landmarks[0].landmark
xs = [l.x for l in landmarks] xs = [l.x for l in landmarks]
@@ -159,7 +231,9 @@ class MonitorSystem:
if emo_results: if emo_results:
top_res = emo_results[0] top_res = emo_results[0]
self.cached_emotion["label"] = top_res.get("emotion", "unknown") self.cached_emotion["label"] = top_res.get(
"emotion", "unknown"
)
self.cached_emotion["va"] = top_res.get("vaVal", (0.0, 0.0)) self.cached_emotion["va"] = top_res.get("vaVal", (0.0, 0.0))
except Exception as e: except Exception as e:

View File

@@ -5,12 +5,18 @@ import cv2
LEFT_EYE = [33, 160, 158, 133, 153, 144] LEFT_EYE = [33, 160, 158, 133, 153, 144]
# 右眼 # 右眼
RIGHT_EYE = [362, 385, 387, 263, 373, 380] RIGHT_EYE = [362, 385, 387, 263, 373, 380]
# 左眼虹膜关键点索引
LEFT_EYE_GAZE_IDXS = [33, 133, 159, 145, 468]
# 右眼虹膜关键点索引
RIGHT_EYE_GAZE_IDXS = [263, 362, 386, 374, 473]
# 嘴唇 (内圈) # 嘴唇 (内圈)
LIPS = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 415, 310, 311, 312, 13] LIPS = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 415, 310, 311, 312, 13]
def _euclidean_distance(point1, point2): def _euclidean_distance(point1, point2):
return np.linalg.norm(point1 - point2) return np.linalg.norm(point1 - point2)
def calculate_ear(landmarks, width, height): def calculate_ear(landmarks, width, height):
"""计算眼睛纵横比 EAR""" """计算眼睛纵横比 EAR"""
# 坐标转换 # 坐标转换
@@ -25,32 +31,71 @@ def calculate_ear(landmarks, width, height):
ear = (v1 + v2) / (2.0 * h) ear = (v1 + v2) / (2.0 * h)
return ear return ear
def calculate_iris_pos(landmarks, indices, width, height):
p_left = np.array([landmarks[indices[0]].x * width, landmarks[indices[0]].y * height])
p_right = np.array([landmarks[indices[1]].x * width, landmarks[indices[1]].y * height])
p_top = np.array([landmarks[indices[2]].x * width, landmarks[indices[2]].y * height])
p_bottom = np.array([landmarks[indices[3]].x * width, landmarks[indices[3]].y * height])
p_iris = np.array([landmarks[indices[4]].x * width, landmarks[indices[4]].y * height])
# 修改为欧几里得距离计算
eye_width = _euclidean_distance(p_left, p_right)
iris_left = _euclidean_distance(p_iris, p_left)
if eye_width < 1e-3:
raw_x = 0.5
else :
raw_x = iris_left / eye_width
eye_height = _euclidean_distance(p_bottom, p_top)
iris_top = _euclidean_distance(p_iris, p_top)
if eye_height < 1e-3:
raw_y = 0.5
else:
raw_y = iris_top / eye_height
# x_min = 0.4
# x_max = 0.6
# ratio_x = abs(raw_x - x_min) / (x_max - x_min)
# y_min = 0.2
# y_max = 0.8
# ratio_y = abs(raw_y - y_min) / (y_max - y_min)
ratio_x = raw_x
ratio_y = raw_y
return max(0.0, min(1.0, ratio_x)), max(0.0, min(1.0, ratio_y))
def calculate_mar(landmarks, width, height): def calculate_mar(landmarks, width, height):
"""计算嘴巴纵横比 MAR""" """计算嘴巴纵横比 MAR"""
points = np.array([(p.x * width, p.y * height) for p in landmarks]) points = np.array([(p.x * width, p.y * height) for p in landmarks])
pass pass
def calculate_mar_simple(top, bottom, left, right): def calculate_mar_simple(top, bottom, left, right):
h = _euclidean_distance(top, bottom) h = _euclidean_distance(top, bottom)
w = _euclidean_distance(left, right) w = _euclidean_distance(left, right)
return h / w return h / w
# geometry_utils.py 中的 estimate_head_pose 函数替换为以下内容 # geometry_utils.py 中的 estimate_head_pose 函数替换为以下内容
def estimate_head_pose(landmarks, width, height): def estimate_head_pose(landmarks, width, height):
""" """
计算头部姿态 (Pitch, Yaw, Roll) 计算头部姿态 (Pitch, Yaw, Roll)
返回单位:角度 (Degree) 返回单位:角度 (Degree)
""" """
# 3D 模型点 (标准人脸模型) # 3D 模型点 (标准人脸模型)
model_points = np.array([ model_points = np.array(
[
(0.0, 0.0, 0.0), # Nose tip (0.0, 0.0, 0.0), # Nose tip
(0.0, -330.0, -65.0), # Chin (0.0, -330.0, -65.0), # Chin
(-225.0, 170.0, -135.0), # Left eye left corner (-225.0, 170.0, -135.0), # Left eye left corner
(225.0, 170.0, -135.0), # Right eye right corner (225.0, 170.0, -135.0), # Right eye right corner
(-150.0, -150.0, -125.0), # Left Mouth corner (-150.0, -150.0, -125.0), # Left Mouth corner
(150.0, -150.0, -125.0) # Right mouth corner (150.0, -150.0, -125.0), # Right mouth corner
]) ]
)
# MediaPipe 对应的关键点索引 # MediaPipe 对应的关键点索引
idx_list = [1, 152, 33, 263, 61, 291] idx_list = [1, 152, 33, 263, 61, 291]
@@ -65,9 +110,8 @@ def estimate_head_pose(landmarks, width, height):
focal_length = width focal_length = width
center = (width / 2, height / 2) center = (width / 2, height / 2)
camera_matrix = np.array( camera_matrix = np.array(
[[focal_length, 0, center[0]], [[focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1]],
[0, focal_length, center[1]], dtype="double",
[0, 0, 1]], dtype="double"
) )
dist_coeffs = np.zeros((4, 1)) dist_coeffs = np.zeros((4, 1))
@@ -83,12 +127,18 @@ def estimate_head_pose(landmarks, width, height):
yaw = angles[1] yaw = angles[1]
roll = angles[2] roll = angles[2]
if pitch < -180: pitch += 360 if pitch < -180:
if pitch > 180: pitch -= 360 pitch += 360
if pitch > 180:
pitch -= 360
pitch = 180 - pitch if pitch > 0 else -pitch - 180 pitch = 180 - pitch if pitch > 0 else -pitch - 180
if yaw < -180: yaw += 360 if yaw < -180:
if yaw > 180: yaw -= 360 yaw += 360
if roll < -180: roll += 360 if yaw > 180:
if roll > 180: roll -= 360 yaw -= 360
if roll < -180:
roll += 360
if roll > 180:
roll -= 360
return pitch, yaw, roll return pitch, yaw, roll

View File

@@ -1,3 +1,4 @@
from calendar import c
import cv2 import cv2
import threading import threading
import time import time
@@ -6,9 +7,13 @@ import socket
import json import json
import urllib.request import urllib.request
import struct import struct
import numpy as np
import mediapipe as mp
from analyzer import MonitorSystem from analyzer import MonitorSystem
from webrtc_server import WebRTCServer
from HeartRateMonitor import HeartRateMonitor
SERVER_HOST = '10.128.50.6' SERVER_HOST = "10.128.50.6"
SERVER_PORT = 65432 SERVER_PORT = 65432
API_URL = "http://10.128.50.6:5000/api/states" API_URL = "http://10.128.50.6:5000/api/states"
CAMERA_ID = "23373333" CAMERA_ID = "23373333"
@@ -18,26 +23,83 @@ BASIC_FACE_DB = {
"Yaoyu": {"name": "Yaoyu Zhang", "age": 20, "image-path": "yaoyu.jpg"}, "Yaoyu": {"name": "Yaoyu Zhang", "age": 20, "image-path": "yaoyu.jpg"},
} }
mp_face_mesh_main = mp.solutions.face_mesh
face_mesh_main = mp_face_mesh_main.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5,
)
def apply_soft_roi(frame):
h, w = frame.shape[:2]
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
black_bg = np.zeros((h, w, 3), dtype=np.uint8)
results = face_mesh_main.process(rgb_frame)
if not results.multi_face_landmarks:
return frame
landmarks = results.multi_face_landmarks[0].landmark
xs = [l.x for l in landmarks]
ys = [l.y for l in landmarks]
# 计算人脸框
face_loc = (
int(min(ys) * h - 0.1 * h),
int(max(xs) * w + 0.1 * w),
int(max(ys) * h + 0.1 * h),
int(min(xs) * w - 0.1 * w),
)
pad = 30
face_loc = (
max(0, face_loc[0] - pad),
min(w, face_loc[1] + pad),
min(h, face_loc[2] + pad),
max(0, face_loc[3] - pad),
)
top = face_loc[0]
right = face_loc[1]
bottom = face_loc[2]
left = face_loc[3]
scale_factor = 10
small_bg = cv2.resize(
frame, (w // scale_factor, h // scale_factor), interpolation=cv2.INTER_LINEAR
)
# 使用 INTER_NEAREST 马赛克效果
# 使用 INTER_LINEAR 毛玻璃模糊效果
blurred_frame = cv2.resize(small_bg, (w, h), interpolation=cv2.INTER_LINEAR)
face_roi = frame[top:bottom, left:right]
blurred_frame[top:bottom, left:right] = face_roi
black_bg[top:bottom, left:right] = face_roi
return blurred_frame
frame_queue = queue.Queue(maxsize=2) frame_queue = queue.Queue(maxsize=2)
video_queue = queue.Queue(maxsize=1) video_queue = queue.Queue(maxsize=10)
data_queue = queue.Queue(maxsize=10) data_queue = queue.Queue(maxsize=10)
show_queue = queue.Queue(maxsize=10)
stop_event = threading.Event() stop_event = threading.Event()
def capture_thread(): def capture_thread():
""" """
采集线程:优化了分发逻辑,对视频流进行降频处理 采集线程:优化了分发逻辑,对视频流进行降频处理
""" """
cap = cv2.VideoCapture(0) cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
print("[Capture] 摄像头启动...") print("[Capture] 摄像头启动...")
frame_count = 0 frame_count = 0
last_time = time.time()
while not stop_event.is_set(): while not stop_event.is_set():
ret, frame = cap.read() ret, frame = cap.read()
@@ -53,21 +115,27 @@ def capture_thread():
except queue.Empty: except queue.Empty:
pass pass
# try:
if frame_count % 2 == 0: # if video_queue.full():
try: # video_queue.get_nowait()
if video_queue.full(): # video_queue.put(frame)
video_queue.get_nowait() # except:
video_queue.put(frame) # pass
except:
pass
frame_count += 1 frame_count += 1
time.sleep(0.01) # time.sleep(1 / 30)
# current_time = time.time()
# if current_time - last_time >= 1.0:
# print(f"[Capture] FPS: {frame_count}")
# frame_count = 0
# last_time = current_time
# print(current_time - last_time)
# last_time = current_time
cap.release() cap.release()
print("[Capture] 线程结束") print("[Capture] 线程结束")
def analysis_thread(): def analysis_thread():
""" """
核心分析线程: 核心分析线程:
@@ -76,7 +144,12 @@ def analysis_thread():
""" """
monitor = MonitorSystem(BASIC_FACE_DB) monitor = MonitorSystem(BASIC_FACE_DB)
print("[Analysis] 分析系统启动...") print("[Analysis] 分析系统启动...")
freq = 0
gap = 60
status = 0 # 0:open 1:close
last_time = time.time()
last_freq = 0
# heart_monitor = HeartRateMonitor()
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
frame = frame_queue.get(timeout=1) frame = frame_queue.get(timeout=1)
@@ -85,6 +158,13 @@ def analysis_thread():
# 核心分析 # 核心分析
result = monitor.process_frame(frame) result = monitor.process_frame(frame)
result["eye_close_freq"] = 0
result["heart_rate_bpm"] = 0
if video_queue.full():
video_queue.get_nowait()
video_queue.put(result["frame"])
payload = { payload = {
"id": CAMERA_ID, "id": CAMERA_ID,
@@ -92,30 +172,58 @@ def analysis_thread():
"name": "", "name": "",
"ear": "", "ear": "",
"mar": "", "mar": "",
"iris_ratio": "",
"eye_close_freq": "",
"pose": "", "pose": "",
"emo_label": "", "emo_label": "",
"emo_va": "" "emo_va": "",
"heart_rate_bpm": "",
} }
if result["has_face"] and result["identity"]: if result["has_face"] and result["identity"]:
payload.update({ payload.update(
{
"name": result["identity"]["name"], "name": result["identity"]["name"],
"ear": result["ear"], "ear": result["ear"],
"mar": result["mar"], "mar": result["mar"],
"iris_ratio": result["iris_ratio"],
"pose": result["pose"], "pose": result["pose"],
"emo_label": result["emotion_label"], "emo_label": result["emotion_label"],
"emo_va": result["emotion_va"] "emo_va": result["emotion_va"],
}) }
)
elif result["has_face"]: elif result["has_face"]:
payload.update({ payload.update(
{
"name": "Unknown", "name": "Unknown",
"ear": result["ear"], "ear": result["ear"],
"mar": result["mar"], "mar": result["mar"],
"iris_ratio": result["iris_ratio"],
"pose": result["pose"], "pose": result["pose"],
"emo_label": result["emotion_label"], "emo_label": result["emotion_label"],
"emo_va": result["emotion_va"] "emo_va": result["emotion_va"],
}) }
)
if result["has_face"] and result["ear"] < 0.2:
if status == 0:
freq += 1
status = 1
elif result["has_face"] and result["ear"] >= 0.2:
if status == 1:
freq += 1
status = 0
if time.time() - last_time >= gap:
last_freq = freq / 2
freq = 0
last_time = time.time()
result["eye_close_freq"] = last_freq
payload["eye_close_freq"] = last_freq
# bpm = heart_monitor.process_frame(frame, result["landmark"])
# if bpm != None:
# result["heart_rate_bpm"] = bpm
# payload["heart_rate_bpm"] = bpm
if data_queue.full(): if data_queue.full():
try: try:
_ = data_queue.get_nowait() _ = data_queue.get_nowait()
@@ -124,59 +232,99 @@ def analysis_thread():
data_queue.put(payload) data_queue.put(payload)
draw_debug_info(frame, result) show_queue.put((result["frame"], result))
cv2.imshow("Monitor Client", frame) # draw_debug_info(frame, result)
if cv2.waitKey(1) & 0xFF == ord('q'): # cv2.imshow("Monitor Client", frame)
stop_event.set()
cv2.destroyAllWindows()
print("[Analysis] 分析线程结束") print("[Analysis] 分析线程结束")
def video_stream_thread(): def video_stream_thread():
""" """
发送线程:优化了 Socket 设置和压缩参数 发送线程:优化了 Socket 设置和压缩参数
""" """
print(f"[Video] 准备连接服务器 {SERVER_HOST}:{SERVER_PORT} ...") print(f"[Video] 准备连接服务器 {SERVER_HOST}:{SERVER_PORT} ...")
server = WebRTCServer(fps=60)
while not stop_event.is_set(): server.start()
try: fourcc = cv2.VideoWriter_fourcc(*'avc1')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: # jetson-nvenc 编码器
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # bitrate = 1000000 # 1 Mbps
# gst_pipeline = (
s.connect((SERVER_HOST, SERVER_PORT)) # f"appsrc ! "
print(f"[Video] 已连接") # f"video/x-raw, format=BGR ! "
# f"queue ! "
camera_id_bytes = CAMERA_ID.encode('utf-8') # f"videoconvert ! "
# f"video/x-raw, format=RGBA ! "
# f"nvvidconv ! "
# f"nvv4l2h264enc bitrate={bitrate} control-rate=1 profile=High ! "
# f"h264parse ! "
# f"qtmux ! "
# f"filesink location={filename} "
# )
# out = cv2.VideoWriter(gst_pipeline, cv2.CAP_GSTREAMER, 0, fps, (width, height))
out1 = cv2.VideoWriter('output1.mp4', fourcc, 30.0, (1280, 720))
out2 = cv2.VideoWriter('output2.mp4', fourcc, 30.0, (1280, 720))
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
frame = video_queue.get(timeout=1) frame = video_queue.get(timeout=1)
# small_frame = cv2.resize(apply_soft_roi(frame), (1280, 720))
small_frame = cv2.resize(frame, (320, 240)) server.provide_frame(frame)
out1.write(frame)
ret, buffer = cv2.imencode('.jpg', small_frame, [cv2.IMWRITE_JPEG_QUALITY, 50]) out2.write(frame)
if not ret: continue
frame_bytes = buffer.tobytes()
header_id_len = len(camera_id_bytes).to_bytes(4, 'big')
header_frame_len = len(frame_bytes).to_bytes(4, 'big')
packet = header_id_len + camera_id_bytes + header_frame_len + frame_bytes
s.sendall(packet)
except queue.Empty: except queue.Empty:
continue continue
except Exception as e: except Exception as e:
print(f"[Video] 发送断开: {e}") print(f"[Video] 发送错误: {e}")
break continue
# while not stop_event.is_set():
# try:
# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except Exception as e: # s.connect((SERVER_HOST, SERVER_PORT))
print(f"[Video] 重连中... {e}") # print(f"[Video] 已连接")
time.sleep(3)
# camera_id_bytes = CAMERA_ID.encode("utf-8")
# while not stop_event.is_set():
# try:
# frame = video_queue.get(timeout=1)
# small_frame = cv2.resize(apply_soft_roi(frame), (1280, 720))
# ret, buffer = cv2.imencode(
# ".jpg", small_frame, [cv2.IMWRITE_JPEG_QUALITY, 50]
# )
# if not ret:
# continue
# frame_bytes = buffer.tobytes()
# header_id_len = len(camera_id_bytes).to_bytes(4, "big")
# header_frame_len = len(frame_bytes).to_bytes(4, "big")
# packet = (
# header_id_len
# + camera_id_bytes
# + header_frame_len
# + frame_bytes
# )
# s.sendall(packet)
# except queue.Empty:
# continue
# except Exception as e:
# print(f"[Video] 发送断开: {e}")
# break
# except Exception as e:
# print(f"[Video] 重连中... {e}")
# time.sleep(3)
out1.release()
out2.release()
print("[Video] 线程结束") print("[Video] 线程结束")
def data_upload_thread(): def data_upload_thread():
""" """
周期性爆发模式 周期性爆发模式
@@ -214,16 +362,18 @@ def data_upload_thread():
try: try:
req = urllib.request.Request( req = urllib.request.Request(
url=API_URL, url=API_URL,
data=json.dumps(data).encode('utf-8'), data=json.dumps(data).encode("utf-8"),
headers={'Content-Type': 'application/json'}, headers={"Content-Type": "application/json"},
method='POST' method="POST",
) )
with urllib.request.urlopen(req, timeout=2) as resp: with urllib.request.urlopen(req, timeout=2) as resp:
pass pass
# 打印日志 # 打印日志
name_info = data['name'] if data['name'] else "NO-FACE" name_info = data["name"] if data["name"] else "NO-FACE"
print(f"[Data Upload {i+1}/{BURST_COUNT}] {name_info} | Time:{data['time']}") print(
f"[Data Upload {i+1}/{BURST_COUNT}] {name_info} | Time:{data['time']}"
)
except Exception as e: except Exception as e:
print(f"[Data] Upload Error: {e}") print(f"[Data] Upload Error: {e}")
@@ -236,31 +386,92 @@ def data_upload_thread():
print("[Data] 数据上报线程结束") print("[Data] 数据上报线程结束")
def draw_debug_info(frame, result): def draw_debug_info(frame, result):
"""在画面上画出即时数据""" """在画面上画出即时数据"""
if not result["has_face"]: if not result["has_face"]:
cv2.putText(frame, "NO FACE", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.putText(
frame, "NO FACE", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2
)
return return
# 显示身份 # 显示身份
id_text = result["identity"]["name"] if result["identity"] else "Unknown" id_text = result["identity"]["name"] if result["identity"] else "Unknown"
color = (0, 255, 0) if result["identity"] else (0, 255, 255) color = (0, 255, 0) if result["identity"] else (0, 255, 255)
cv2.putText(frame, f"User: {id_text}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.putText(
frame, f"User: {id_text}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2
)
# 显示数据 # 显示数据
cv2.putText(frame, f"EAR: {result['ear']}", (20, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 1) cv2.putText(
cv2.putText(frame, f"MAR: {result['mar']}", (20, 95), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 1) frame,
if result['ear'] < 0.15: f"EAR: {result['ear']}",
cv2.putText(frame, "EYE CLOSE", (250, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) (20, 70),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 255, 0),
1,
)
cv2.putText(
frame,
f"MAR: {result['mar']}",
(20, 95),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 255, 0),
1,
)
cv2.putText(
frame,
f"Iris Ratio: {result['iris_ratio']}",
(20, 190),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 255, 0),
1,
)
cv2.putText(
frame,
f"Eye Close Freq: {result['eye_close_freq']}",
(20, 170),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 0, 255),
1,
)
cv2.putText(
frame,
f"Heart Rate BPM: {result['heart_rate_bpm']}",
(20, 210),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 165, 255),
1,
)
if result["ear"] < 0.15:
cv2.putText(
frame, "EYE CLOSE", (250, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2
)
p, y, r = result["pose"] p, y, r = result["pose"]
cv2.putText(frame, f"Pose: P{p} Y{y} R{r}", (20, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1) cv2.putText(
frame,
f"Pose: P{p} Y{y} R{r}",
(20, 120),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 255),
1,
)
emo = result.get("emotion_label", "N/A") emo = result.get("emotion_label", "N/A")
va = result.get("emotion_va", (0,0)) va = result.get("emotion_va", (0, 0))
# 显示格式: Emo: happy (-0.5, 0.2) # 显示格式: Emo: happy (-0.5, 0.2)
emo_text = f"Emo: {emo} ({va[0]:.2f}, {va[1]:.2f})" emo_text = f"Emo: {emo} ({va[0]:.2f}, {va[1]:.2f})"
cv2.putText(frame, emo_text, (20, 145), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 165, 255), 1) cv2.putText(
frame, emo_text, (20, 145), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 165, 255), 1
)
if __name__ == "__main__": if __name__ == "__main__":
t1 = threading.Thread(target=capture_thread, daemon=True) t1 = threading.Thread(target=capture_thread, daemon=True)
@@ -275,7 +486,19 @@ if __name__ == "__main__":
try: try:
while not stop_event.is_set(): while not stop_event.is_set():
time.sleep(1) try:
frame, result = show_queue.get(timeout=1)
except queue.Empty:
continue
# frame = apply_soft_roi(frame)
display_frame = frame.copy()
draw_debug_info(display_frame, result)
cv2.imshow("Monitor Client", display_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
stop_event.set()
# time.sleep(1)
cv2.destroyAllWindows()
except KeyboardInterrupt: except KeyboardInterrupt:
print("停止程序...") print("停止程序...")
stop_event.set() stop_event.set()

49
reproject/server_demo.py Normal file
View File

@@ -0,0 +1,49 @@
import time
import numpy as np
import cv2
from webrtc_server import WebRTCServer
FPS = 60
server = WebRTCServer(fps=FPS)
server.start()
def run_ani():
width, height = 1920, 1080
frame_count = 60
while True:
frame = np.zeros((height, width, 3), dtype=np.uint8)
center_x = (frame_count * 10) % width
cv2.circle(frame, (center_x, height // 2), 50, (0, 255, 0), -1)
cv2.putText(
frame,
f"AniCam Frame: {frame_count}",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(255, 255, 255),
2,
)
color = (frame_count * 5 % 256, 100, 200)
cv2.rectangle(frame, (50, 50), (150, 150), color, -1)
server.provide_frame(frame)
frame_count += 1
time.sleep(1 / FPS)
def run_cam(device_id):
cap = cv2.VideoCapture(device_id)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 2560)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1440)
while True:
ret, frame = cap.read()
if ret:
server.provide_frame(frame)
cv2.imshow("Camera", frame)
time.sleep(1 / FPS)
if __name__ == "__main__":
run_cam(0)

97
reproject/test.py Normal file
View File

@@ -0,0 +1,97 @@
import cv2
import threading
import time
import queue
import socket
import json
import urllib.request
import struct
import mediapipe as mp
import numpy as np
from analyzer import MonitorSystem
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
def apply_soft_roi(frame):
h, w = frame.shape[:2]
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
black_bg = np.zeros((h, w, 3), dtype=np.uint8)
results = face_mesh.process(rgb_frame)
if not results.multi_face_landmarks:
return frame
landmarks = results.multi_face_landmarks[0].landmark
xs = [l.x for l in landmarks]
ys = [l.y for l in landmarks]
# 计算人脸框
face_loc = (
int(min(ys) * h - 0.1 * h), int(max(xs) * w + 0.1 * w),
int(max(ys) * h + 0.1 * h), int(min(xs) * w - 0.1 * w)
)
pad = 30
face_loc = (max(0, face_loc[0]-pad), min(w, face_loc[1]+pad),
min(h, face_loc[2]+pad), max(0, face_loc[3]-pad))
top = face_loc[0]
right = face_loc[1]
bottom = face_loc[2]
left = face_loc[3]
scale_factor = 10
small_bg = cv2.resize(frame, (w // scale_factor, h // scale_factor), interpolation=cv2.INTER_LINEAR)
# 使用 INTER_NEAREST 马赛克效果
# 使用 INTER_LINEAR 毛玻璃模糊效果
blurred_frame = cv2.resize(small_bg, (w, h), interpolation=cv2.INTER_LINEAR)
face_roi = frame[top:bottom, left:right]
blurred_frame[top:bottom, left:right] = face_roi
black_bg[top:bottom, left:right] = face_roi
return blurred_frame
def test_compression_efficiency():
# 1. 读取一张测试图 (或者用摄像头抓一帧)
cap = cv2.VideoCapture(0)
time.sleep(5) # 等待摄像头稳定
while True:
ret, frame = cap.read()
cap.release()
if not ret:
print("无法读取摄像头")
return
# 2. 生成模糊处理后的图
processed_frame = apply_soft_roi(frame) # 使用你的函数
# 3. 【关键】模拟网络传输/存储:进行 JPG 编码
# 这里的 80 代表 JPEG 质量,模拟视频编码过程
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80]
# 编码原图
_, encoded_original = cv2.imencode('.jpg', frame, encode_param)
original_size = len(encoded_original)
# 编码处理后的图
_, encoded_processed = cv2.imencode('.jpg', processed_frame, encode_param)
processed_size = len(encoded_processed)
# 4. 对比结果
print(f"原始画面编码后大小: {original_size / 1024:.2f} KB")
print(f"ROI处理编码后大小: {processed_size / 1024:.2f} KB")
savings = (original_size - processed_size) / original_size * 100
print(f"📉 带宽/存储节省了: {savings:.2f}%")
# 可视化对比
cv2.imshow("Original", frame)
cv2.imshow("Processed (Bandwidth Saver)", processed_frame)
# 运行测试
# 注意:你需要先定义 face_mesh 和 apply_soft_roi 才能运行
test_compression_efficiency()

157
reproject/webrtc_server.py Normal file
View File

@@ -0,0 +1,157 @@
import threading
import time
import asyncio
import socketio
import numpy as np
import cv2
import av
from aiortc import (
MediaStreamError,
RTCConfiguration,
RTCPeerConnection,
RTCSessionDescription,
VideoStreamTrack,
)
from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE
class WebRTCServer:
def __init__(self, fps):
self.pcs = set()
self.fps = fps
self.frameContainer = [None]
self.background_loop = asyncio.new_event_loop()
self._rtc_thread = threading.Thread(
target=self._start_background_loop,
args=(self.background_loop,),
daemon=True,
)
self._rtc_thread.start()
self.server = "ws://10.128.50.6:5000"
def _start_background_loop(self, loop):
asyncio.set_event_loop(loop)
loop.run_forever()
async def _websocket_start(self):
sio = socketio.AsyncClient()
@sio.event
async def connect():
print("已连接到中心信令服务器")
# TODO 注册自己为设备
await sio.emit("checkin", {"device_id": "cam_001"})
@sio.event
async def offer(data):
# data 包含: { sdp: "...", type: "offer", "sid": "..." }
print("收到 WebRTC Offer")
localDescription = await self._handle_offer(data)
await sio.emit(
"answer",
{
"sdp": localDescription.sdp,
"type": localDescription.type,
"sid": data["sid"],
},
)
await sio.connect(self.server)
print(self.server, "connected")
await sio.wait()
async def _handle_offer(self, offer):
pc = RTCPeerConnection(RTCConfiguration(iceServers=[]))
self.pcs.add(pc)
start = time.time()
@pc.on("connectionstatechange")
async def on_state_change():
if pc.connectionState == "failed" or pc.connectionState == "closed":
await pc.close()
print("Connection closed")
self.pcs.discard(pc)
@pc.on("datachannel")
def on_data_channel(channel):
route_channel(channel)
await pc.setRemoteDescription(
RTCSessionDescription(offer["sdp"], offer.get("type", "offer"))
)
pc.addTrack(VideoFrameTrack(self.fps, self.frameContainer))
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
print(f"Handle offer in {(time.time() - start)*1000:.2f}ms")
return pc.localDescription
def start(self):
asyncio.run_coroutine_threadsafe(self._websocket_start(), self.background_loop)
def provide_frame(self, frame):
self.frameContainer[0] = frame
def stop(self):
if self.background_loop.is_running():
self.background_loop.call_soon_threadsafe(self.background_loop.stop)
if self._rtc_thread.is_alive():
self._rtc_thread.join(timeout=2)
class VideoFrameTrack(VideoStreamTrack):
def __init__(self, fps, fc):
super().__init__()
self.fps = fps
self.frameContainer = fc
async def next_timestamp(self):
"""
重写父类方法,去除帧率限制
"""
if self.readyState != "live":
raise MediaStreamError
if hasattr(self, "_timestamp"):
self._timestamp += int(1 / self.fps * VIDEO_CLOCK_RATE)
wait = self._start + (self._timestamp / VIDEO_CLOCK_RATE) - time.time()
await asyncio.sleep(wait)
else:
self._start = time.time()
self._timestamp = 0
return self._timestamp, VIDEO_TIME_BASE
async def recv(self):
pts, time_base = await self.next_timestamp()
frame = self.frameContainer[0]
if frame is None:
frame = np.zeros((480, 640, 3), dtype=np.uint8)
else:
frame = self.frameContainer[0]
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
video_frame = av.VideoFrame.from_ndarray(frame, format="rgb24")
video_frame.pts = pts
video_frame.time_base = time_base
return video_frame
def route_channel(channel):
match channel.label:
case "latency":
@channel.on("message")
def on_message(message):
now = int(time.time() * 1000 + 0.5)
channel.send(str(now))
pre = int(message)
print(f"Latency: {now - pre}ms")
case _:
print(f"Unknown Channel {channel.label}")