401 lines
14 KiB
Python
401 lines
14 KiB
Python
import cv2
|
||
import mediapipe as mp
|
||
import time
|
||
import numpy as np
|
||
import threading
|
||
import queue
|
||
import multiprocessing as mp_proc
|
||
from multiprocessing import shared_memory
|
||
from collections import deque
|
||
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
|
||
|
||
# try:
|
||
# from new_emotion_test import analyze_emotion_with_hsemotion
|
||
|
||
# HAS_EMOTION_MODULE = True
|
||
# except ImportError:
|
||
# print("⚠️ 未找到 new_emotion_test.py,情绪功能将不可用")
|
||
# HAS_EMOTION_MODULE = False
|
||
|
||
|
||
class MonitorSystem:
|
||
def __init__(self, face_db):
|
||
# 初始化 MediaPipe
|
||
self.mp_face_mesh = mp.solutions.face_mesh
|
||
self.face_mesh = self.mp_face_mesh.FaceMesh(
|
||
max_num_faces=1,
|
||
refine_landmarks=True,
|
||
min_detection_confidence=0.5,
|
||
min_tracking_confidence=0.5,
|
||
)
|
||
|
||
# 初始化人脸底库
|
||
# self.face_lib = FaceLibrary(face_db)
|
||
|
||
# --- 时间控制 ---
|
||
self.last_identity_check_time = 0
|
||
self.IDENTITY_CHECK_INTERVAL = 2.0
|
||
|
||
self.last_emotion_check_time = 0
|
||
self.EMOTION_CHECK_INTERVAL = 3.0
|
||
|
||
# --- 历史数据 ---
|
||
self.HISTORY_LEN = 5
|
||
self.ear_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 = {"label": "detecting...", "va": (0.0, 0.0)}
|
||
|
||
self.current_user = None
|
||
self.current_emotion = "Neutral"
|
||
|
||
self.frame_shape = (720, 1280, 3)
|
||
frame_size = int(np.prod(self.frame_shape))
|
||
|
||
# 必须先解除可能存在的残留 (Windows上有时不需要,但保持好习惯)
|
||
# 最好是随机生成一个名字,确保每次运行都是新的
|
||
import secrets
|
||
auth_key = secrets.token_hex(4)
|
||
shm_unique_name = f"monitor_shm_{auth_key}"
|
||
|
||
try:
|
||
self.shm = shared_memory.SharedMemory(create=True, size=frame_size, name=shm_unique_name)
|
||
except FileExistsError:
|
||
# 如果真的点背碰上了,就 connect 这一块
|
||
self.shm = shared_memory.SharedMemory(name=shm_unique_name)
|
||
|
||
print(f"[Main] 共享内存已创建: {self.shm.name} (Size: {frame_size} bytes)")
|
||
|
||
# 本地 numpy 包装器
|
||
self.shared_frame_array = np.ndarray(
|
||
self.frame_shape, dtype=np.uint8, buffer=self.shm.buf
|
||
)
|
||
# 初始化为全黑,避免噪音
|
||
self.shared_frame_array.fill(0)
|
||
|
||
# 跨进程队列
|
||
self.task_queue = mp_proc.Queue(maxsize=2)
|
||
self.result_queue = mp_proc.Queue(maxsize=2) # 1就够了,最新的覆盖
|
||
|
||
# 3. 启动进程
|
||
# Windows下传参,只传名字字符串是安全的
|
||
self.worker_proc = mp_proc.Process(
|
||
target=background_worker_process,
|
||
args=(
|
||
self.shm.name,
|
||
self.frame_shape,
|
||
self.task_queue,
|
||
self.result_queue,
|
||
face_db,
|
||
),
|
||
)
|
||
self.worker_proc.daemon = True
|
||
self.worker_proc.start()
|
||
|
||
def _get_smoothed_value(self, history, current_val):
|
||
"""内部函数:计算滑动平均值"""
|
||
history.append(current_val)
|
||
if len(history) == 0:
|
||
return current_val
|
||
return sum(history) / len(history)
|
||
|
||
def process_frame(self, frame):
|
||
"""
|
||
输入 BGR 图像,返回分析结果字典
|
||
"""
|
||
# 强制检查分辨率,如果不匹配则 Resize (对应 __init__ 中硬编码的 1280x720)
|
||
# 这一步至关重要,否则后台进程读到的全是黑屏
|
||
target_h, target_w = self.frame_shape[:2]
|
||
if frame.shape[:2] != (target_h, target_w):
|
||
frame = cv2.resize(frame, (target_w, target_h))
|
||
|
||
h, w = frame.shape[:2]
|
||
# 现在肯定匹配了,放心写入
|
||
try:
|
||
self.shared_frame_array[:] = frame[:]
|
||
except Exception:
|
||
# 极端情况:数组形状不匹配 (比如通道数变了)
|
||
pass
|
||
|
||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||
|
||
results = self.face_mesh.process(rgb_frame)
|
||
|
||
analysis_data = {
|
||
"has_face": False,
|
||
"ear": 0.0,
|
||
"mar": 0.0,
|
||
"iris_ratio": (0.5, 0.5), # 0最左/上,1最右/下
|
||
"pose": (0, 0, 0),
|
||
"identity": self.current_user,
|
||
"emotion_label": self.cached_emotion["label"],
|
||
"emotion_va": self.cached_emotion["va"],
|
||
"landmark": (0, w, h, 0),
|
||
"frame": frame,
|
||
}
|
||
|
||
if not results.multi_face_landmarks:
|
||
self.ear_history.clear()
|
||
self.mar_history.clear()
|
||
self.iris_ratio_history[0].clear()
|
||
self.iris_ratio_history[1].clear()
|
||
return analysis_data
|
||
|
||
analysis_data["has_face"] = True
|
||
landmarks = results.multi_face_landmarks[0].landmark
|
||
|
||
# 计算 EAR
|
||
left_ear = calculate_ear([landmarks[i] for i in LEFT_EYE], w, h)
|
||
right_ear = calculate_ear([landmarks[i] for i in RIGHT_EYE], w, h)
|
||
raw_ear = (left_ear + right_ear) / 2.0
|
||
|
||
# 计算 MAR
|
||
top = np.array([landmarks[13].x * w, landmarks[13].y * h])
|
||
bottom = np.array([landmarks[14].x * w, landmarks[14].y * h])
|
||
left = np.array([landmarks[78].x * w, landmarks[78].y * h])
|
||
right = np.array([landmarks[308].x * w, landmarks[308].y * h])
|
||
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 进行数据平滑 ---
|
||
smoothed_ear = self._get_smoothed_value(self.ear_history, raw_ear)
|
||
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)
|
||
|
||
analysis_data.update(
|
||
{
|
||
"ear": round(smoothed_ear, 4),
|
||
"mar": round(smoothed_mar, 4),
|
||
"iris_ratio": (
|
||
round(smoothed_iris_ratio[0], 4),
|
||
round(smoothed_iris_ratio[1], 4),
|
||
),
|
||
"pose": (int(pitch), int(yaw), int(roll)),
|
||
}
|
||
)
|
||
|
||
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),
|
||
)
|
||
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:
|
||
sface_loc = (
|
||
int(min(ys) * h),
|
||
int(max(xs) * w),
|
||
int(max(ys) * h),
|
||
int(min(xs) * w),
|
||
)
|
||
spad = 20
|
||
sface_loc = (
|
||
max(0, sface_loc[0] - spad),
|
||
min(w, sface_loc[1] + spad),
|
||
min(h, sface_loc[2] + spad),
|
||
max(0, sface_loc[3] - spad),
|
||
)
|
||
|
||
if self.task_queue.full():
|
||
self.task_queue.get()
|
||
self.task_queue.put((sface_loc, 0))
|
||
|
||
self.last_identity_check_time = now
|
||
|
||
# --- 情绪识别 ---
|
||
if (
|
||
now - self.last_emotion_check_time > self.EMOTION_CHECK_INTERVAL
|
||
):
|
||
|
||
# 计算裁剪坐标
|
||
x_min = int(min(xs) * w)
|
||
x_max = int(max(xs) * w)
|
||
y_min = int(min(ys) * h)
|
||
y_max = int(max(ys) * h)
|
||
|
||
pad_x = int((x_max - x_min) * 0.1)
|
||
pad_y = int((y_max - y_min) * 0.1)
|
||
|
||
x_min = max(0, x_min - pad_x)
|
||
x_max = min(w, x_max + pad_x)
|
||
y_min = max(0, y_min - pad_y)
|
||
y_max = min(h, y_max + pad_y)
|
||
|
||
face_loc = (y_min, x_max, y_max, x_min)
|
||
|
||
if self.task_queue.full():
|
||
self.task_queue.get()
|
||
self.task_queue.put((face_loc, 1))
|
||
|
||
self.last_emotion_check_time = now
|
||
|
||
while not self.result_queue.empty():
|
||
type_, data = self.result_queue.get()
|
||
if type_ == "identity":
|
||
self.current_user = data
|
||
elif type_ == "emotion":
|
||
self.cached_emotion["label"] = data.get("emotion", "unknown")
|
||
self.cached_emotion["va"] = data.get("vaVal", (0.0, 0.0))
|
||
|
||
analysis_data["identity"] = self.current_user
|
||
analysis_data["emotion_label"] = self.cached_emotion["label"]
|
||
analysis_data["emotion_va"] = self.cached_emotion["va"]
|
||
|
||
return analysis_data
|
||
|
||
# def _id_emo_loop(self):
|
||
# while True:
|
||
# try:
|
||
# frame, face_loc, task_type = self.task_queue.get()
|
||
# if task_type == 0:
|
||
# match_result = self.face_lib.identify(frame, face_location=face_loc)
|
||
# if match_result:
|
||
# self.current_user = match_result["info"]
|
||
# elif task_type == 1 and HAS_EMOTION_MODULE:
|
||
# face_crop = frame[
|
||
# face_loc[0] : face_loc[2], face_loc[3] : face_loc[1]
|
||
# ]
|
||
|
||
# if face_crop.size > 0:
|
||
# try:
|
||
# emo_results = analyze_emotion_with_hsemotion(face_crop)
|
||
|
||
# if emo_results:
|
||
# top_res = emo_results[0]
|
||
# self.cached_emotion["label"] = top_res.get(
|
||
# "emotion", "unknown"
|
||
# )
|
||
# self.cached_emotion["va"] = top_res.get(
|
||
# "vaVal", (0.0, 0.0)
|
||
# )
|
||
|
||
# except Exception as e:
|
||
# print(f"情绪分析出错: {e}")
|
||
# except Exception as e:
|
||
# print(f"线程处理出错: {e}")
|
||
|
||
|
||
def background_worker_process(
|
||
shm_name, # 共享内存的名字
|
||
frame_shape, # 图像大小 (h, w, 3)
|
||
task_queue, # 任务队列 (主 -> 从)
|
||
result_queue, # 结果队列 (从 -> 主)
|
||
face_db_data, # 把人脸库数据传过去初始化
|
||
):
|
||
existing_shm = shared_memory.SharedMemory(name=shm_name)
|
||
# 创建 numpy 数组视图,无需复制数据
|
||
shared_frame = np.ndarray(frame_shape, dtype=np.uint8, buffer=existing_shm.buf)
|
||
|
||
print("[Worker] 正在加载模型...")
|
||
from face_library import FaceLibrary
|
||
|
||
face_lib = FaceLibrary(face_db_data)
|
||
|
||
try:
|
||
from new_emotion_test import analyze_emotion_with_hsemotion
|
||
|
||
has_emo = True
|
||
except:
|
||
has_emo = False
|
||
print("[Worker] 模型加载完毕")
|
||
|
||
while True:
|
||
try:
|
||
# 阻塞等待任务
|
||
# task_info = (task_type, face_loc)
|
||
face_loc, task_type = task_queue.get()
|
||
|
||
# 注意:这里读取的是共享内存里的图,不需要传图!
|
||
# 切片操作也是零拷贝
|
||
# 为了安全,这里 copy 一份出来处理,避免主进程修改
|
||
# 但实际上如果主进程只写新帧,这里读旧帧也问题不大
|
||
# 为了绝对安全和解耦,我们假定主进程已经写入了对应的帧
|
||
|
||
# (实战技巧:通常我们会用一个信号量或多块共享内存来实现乒乓缓存)
|
||
# 简化版:我们直接从 shared_frame 读。
|
||
# 由于主进程跑得快,可能SharedMemory里已经是下一帧了。
|
||
# 但对于识别身份来说,差一两帧根本没区别!这才是优化的精髓。
|
||
|
||
current_frame_view = shared_frame.copy() # .copy() 如果你怕读写冲突
|
||
|
||
if task_type == 0: # Identity
|
||
# RGB转换
|
||
rgb = cv2.cvtColor(current_frame_view, cv2.COLOR_BGR2RGB)
|
||
res = face_lib.identify(rgb, face_location=face_loc)
|
||
if res:
|
||
result_queue.put(("identity", res["info"]))
|
||
|
||
elif task_type == 1 and has_emo: # Emotion
|
||
# BGR 直接切
|
||
roi = current_frame_view[
|
||
face_loc[0] : face_loc[2], face_loc[3] : face_loc[1]
|
||
]
|
||
if roi.size > 0:
|
||
emo_res = analyze_emotion_with_hsemotion(roi)
|
||
if emo_res:
|
||
result_queue.put(("emotion", emo_res[0]))
|
||
|
||
except Exception as e:
|
||
print(f"[Worker Error] {e}")
|