Files
video/core/face.py

326 lines
12 KiB
Python
Raw Permalink Normal View History

2025-09-04 22:59:27 +08:00
import os
import numpy as np
import cv2
2025-09-05 17:23:50 +08:00
import gc
import time
import threading
from PIL import Image
2025-09-04 22:59:27 +08:00
from insightface.app import FaceAnalysis
2025-09-09 16:30:12 +08:00
# 假设service.face_service中get_all_face_name_with_eigenvalue可获取人脸数据
2025-09-04 22:59:27 +08:00
from service.face_service import get_all_face_name_with_eigenvalue
2025-09-09 16:30:12 +08:00
# GPU状态检查支持
2025-09-05 17:23:50 +08:00
try:
import pynvml
pynvml.nvmlInit()
_nvml_available = True
except ImportError:
2025-09-09 16:30:12 +08:00
print("警告: pynvml库未安装无法检测GPU状态默认尝试使用GPU")
2025-09-05 17:23:50 +08:00
_nvml_available = False
2025-09-09 16:30:12 +08:00
# 全局人脸引擎与特征库
2025-09-04 22:59:27 +08:00
_face_app = None
2025-09-09 16:30:12 +08:00
_known_faces_embeddings = {} # 姓名 -> 归一化特征值的映射
_known_faces_names = [] # 已知人脸姓名列表
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# GPU使用状态标记
_using_gpu = False # 是否使用GPU
_used_gpu_id = -1 # 使用的GPU ID-1表示CPU
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 资源管理变量
_ref_count = 0 # 引擎引用计数(记录当前使用次数)
_last_used_time = 0 # 最后一次使用引擎的时间
_lock = threading.Lock() # 线程安全锁
_release_timeout = 8 # 闲置超时时间(秒)
_is_releasing = False # 资源释放中标记
_monitor_thread_running = False # 监控线程运行标记
# 调试计数器
2025-09-05 17:23:50 +08:00
_debug_counter = {
2025-09-09 16:30:12 +08:00
"engine_created": 0, # 引擎创建次数
"engine_released": 0, # 引擎释放次数
"detection_calls": 0 # 检测函数调用次数
2025-09-05 17:23:50 +08:00
}
2025-09-09 16:30:12 +08:00
def check_gpu_availability(gpu_id, memory_threshold=0.7):
"""检查指定GPU的内存使用率是否低于阈值判定为“可用”"""
2025-09-05 17:23:50 +08:00
if not _nvml_available:
2025-09-09 16:30:12 +08:00
return True
2025-09-05 17:23:50 +08:00
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
2025-09-09 16:30:12 +08:00
memory_usage = mem_info.used / mem_info.total
return memory_usage < memory_threshold
2025-09-05 17:23:50 +08:00
except Exception as e:
2025-09-09 16:30:12 +08:00
print(f"检查GPU {gpu_id} 状态失败: {e}")
2025-09-05 17:23:50 +08:00
return False
def select_best_gpu(preferred_gpus=[0, 1]):
2025-09-09 16:30:12 +08:00
"""按优先级选择可用GPU优先0号均不可用则返回-1CPU"""
2025-09-05 17:23:50 +08:00
for gpu_id in preferred_gpus:
try:
2025-09-09 16:30:12 +08:00
# 验证GPU是否存在
2025-09-05 17:23:50 +08:00
if _nvml_available:
pynvml.nvmlDeviceGetHandleByIndex(gpu_id)
2025-09-09 16:30:12 +08:00
# 验证GPU内存是否充足
2025-09-05 17:23:50 +08:00
if check_gpu_availability(gpu_id):
2025-09-09 16:30:12 +08:00
print(f"GPU {gpu_id} 可用将使用该GPU")
2025-09-05 17:23:50 +08:00
return gpu_id
else:
if gpu_id == 0:
2025-09-09 16:30:12 +08:00
print("GPU 0 内存使用率过高尝试其他GPU")
2025-09-05 17:23:50 +08:00
except Exception as e:
2025-09-09 16:30:12 +08:00
print(f"GPU {gpu_id} 不可用或访问失败: {e}")
print("所有指定GPU均不可用将使用CPU计算")
2025-09-05 17:23:50 +08:00
return -1
2025-09-09 16:30:12 +08:00
def _release_engine_resources():
"""释放人脸引擎的所有资源模型、特征库、GPU缓存等"""
2025-09-05 17:23:50 +08:00
global _face_app, _is_releasing, _known_faces_embeddings, _known_faces_names
if not _face_app or _is_releasing:
return
try:
_is_releasing = True
2025-09-09 16:30:12 +08:00
print("开始释放人脸引擎资源...")
# 释放InsightFace模型资源
if hasattr(_face_app, "model"):
_face_app.model = None # 显式置空模型引用
_face_app = None # 释放引擎实例
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
# 清空人脸特征库
2025-09-05 17:23:50 +08:00
_known_faces_embeddings.clear()
_known_faces_names.clear()
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
_debug_counter["engine_released"] += 1
print(f"人脸引擎已释放,调试统计: {_debug_counter}")
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 强制垃圾回收
2025-09-05 17:23:50 +08:00
gc.collect()
2025-09-09 16:30:12 +08:00
# 清理各深度学习框架的GPU缓存
# Torch 缓存清理
2025-09-05 17:23:50 +08:00
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
2025-09-09 16:30:12 +08:00
print("Torch GPU缓存已清理")
2025-09-05 17:23:50 +08:00
except ImportError:
pass
2025-09-09 16:30:12 +08:00
# TensorFlow 缓存清理
2025-09-05 17:23:50 +08:00
try:
import tensorflow as tf
tf.keras.backend.clear_session()
2025-09-09 16:30:12 +08:00
print("TensorFlow会话已清理")
2025-09-05 17:23:50 +08:00
except ImportError:
pass
2025-09-09 16:30:12 +08:00
# MXNet 缓存清理InsightFace底层常用MXNet
try:
import mxnet as mx
mx.nd.waitall() # 等待所有计算完成并释放资源
print("MXNet资源已等待释放")
except ImportError:
pass
except Exception as e:
print(f"释放资源过程中出错: {e}")
2025-09-05 17:23:50 +08:00
finally:
_is_releasing = False
2025-09-09 16:30:12 +08:00
def _resource_monitor_thread():
"""后台监控线程:检测引擎闲置超时,触发资源释放"""
global _ref_count, _last_used_time, _face_app, _monitor_thread_running
_monitor_thread_running = True
while _monitor_thread_running:
time.sleep(2) # 缩短检查间隔,加快闲置检测响应
2025-09-05 17:23:50 +08:00
with _lock:
2025-09-09 16:30:12 +08:00
# 当“引擎存在 + 无引用 + 未在释放中”时,检查闲置时间
2025-09-05 17:23:50 +08:00
if _face_app and _ref_count == 0 and not _is_releasing:
2025-09-09 16:30:12 +08:00
idle_time = time.time() - _last_used_time
if idle_time > _release_timeout:
print(f"引擎闲置超时({idle_time:.1f}s > {_release_timeout}s释放资源")
_release_engine_resources()
2025-09-05 17:23:50 +08:00
def load_model(prefer_gpu=True, preferred_gpus=[0, 1]):
2025-09-09 16:30:12 +08:00
"""加载人脸识别引擎及已知人脸特征库默认优先用0号GPU"""
2025-09-05 17:23:50 +08:00
global _face_app, _known_faces_embeddings, _known_faces_names, _using_gpu, _used_gpu_id
2025-09-09 16:30:12 +08:00
# 启动后台监控线程(确保仅启动一次)
if not _monitor_thread_running:
threading.Thread(
target=_resource_monitor_thread,
daemon=True,
name="FaceEngineMonitor"
).start()
print("人脸引擎监控线程已启动")
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 若正在释放资源,等待释放完成
2025-09-05 17:23:50 +08:00
while _is_releasing:
time.sleep(0.1)
2025-09-09 16:30:12 +08:00
# 若引擎已初始化,直接返回
2025-09-05 17:23:50 +08:00
if _face_app:
return True
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
# 初始化InsightFace引擎
2025-09-04 22:59:27 +08:00
try:
2025-09-05 17:23:50 +08:00
print("正在初始化InsightFace人脸识别引擎...")
2025-09-09 16:30:12 +08:00
_face_app = FaceAnalysis(name="buffalo_l", root=os.path.expanduser("~/.insightface"))
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 选择GPU优先用0号
2025-09-05 17:23:50 +08:00
ctx_id = 0
if prefer_gpu:
ctx_id = select_best_gpu(preferred_gpus)
_using_gpu = ctx_id != -1
_used_gpu_id = ctx_id if _using_gpu else -1
2025-09-09 16:30:12 +08:00
if _using_gpu:
print(f"引擎初始化成功将使用GPU {ctx_id} 计算")
else:
print("引擎初始化成功将使用CPU计算")
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 准备模型(加载到指定设备)
2025-09-05 17:23:50 +08:00
_face_app.prepare(ctx_id=ctx_id, det_size=(640, 640))
2025-09-09 16:30:12 +08:00
print("InsightFace引擎初始化完成")
_debug_counter["engine_created"] += 1
print(f"引擎调试统计: {_debug_counter}")
2025-09-05 17:23:50 +08:00
2025-09-04 22:59:27 +08:00
except Exception as e:
2025-09-09 16:30:12 +08:00
print(f"引擎初始化失败: {e}")
2025-09-04 22:59:27 +08:00
return False
2025-09-09 16:30:12 +08:00
# 从服务加载已知人脸的姓名和特征值
2025-09-04 22:59:27 +08:00
try:
face_data = get_all_face_name_with_eigenvalue()
for person_name, eigenvalue_data in face_data.items():
2025-09-09 16:30:12 +08:00
# 兼容“numpy数组”和“字符串”格式的特征值
2025-09-04 22:59:27 +08:00
if isinstance(eigenvalue_data, np.ndarray):
eigenvalue = eigenvalue_data.astype(np.float32)
elif isinstance(eigenvalue_data, str):
2025-09-09 16:30:12 +08:00
# 清理字符串中的括号、换行等干扰符
cleaned = eigenvalue_data.replace("[", "").replace("]", "").replace("\n", "").strip()
# 分割并转换为浮点数数组
values = [v for v in cleaned.split() if v] # 兼容空格/逗号分隔
2025-09-04 22:59:27 +08:00
eigenvalue = np.array(list(map(float, values)), dtype=np.float32)
else:
2025-09-09 16:30:12 +08:00
print(f"不支持的特征值类型({type(eigenvalue_data)}),跳过 {person_name}")
2025-09-04 22:59:27 +08:00
continue
2025-09-09 16:30:12 +08:00
# 特征值归一化(保证后续相似度计算的一致性)
2025-09-04 22:59:27 +08:00
norm = np.linalg.norm(eigenvalue)
if norm != 0:
eigenvalue = eigenvalue / norm
_known_faces_embeddings[person_name] = eigenvalue
_known_faces_names.append(person_name)
2025-09-09 16:30:12 +08:00
print(f"成功加载 {len(_known_faces_names)} 个人脸的特征库")
2025-09-04 22:59:27 +08:00
except Exception as e:
2025-09-09 16:30:12 +08:00
print(f"加载人脸特征库失败: {e}")
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
return _face_app is not None
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
def detect(frame, similarity_threshold=0.4):
"""
检测并识别人脸
返回(是否匹配到已知人脸, 结果描述字符串)
"""
global _face_app, _known_faces_embeddings, _known_faces_names, _ref_count, _last_used_time
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 校验输入帧有效性
2025-09-05 17:23:50 +08:00
if frame is None or frame.size == 0:
2025-09-09 16:30:12 +08:00
return (False, "无效的输入帧数据")
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 加锁并更新引用计数、最后使用时间
2025-09-05 17:23:50 +08:00
engine = None
with _lock:
_ref_count += 1
_last_used_time = time.time()
2025-09-09 16:30:12 +08:00
_debug_counter["detection_calls"] += 1
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
# 若引擎未初始化且未在释放中,尝试初始化
2025-09-05 17:23:50 +08:00
if not _face_app and not _is_releasing:
if not load_model(prefer_gpu=True):
2025-09-09 16:30:12 +08:00
# 初始化失败,恢复引用计数
with _lock:
_ref_count = max(0, _ref_count - 1)
return (False, "人脸引擎初始化失败")
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
engine = _face_app # 获取引擎引用
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
# 校验引擎可用性
if not engine or len(_known_faces_names) == 0:
2025-09-05 17:23:50 +08:00
with _lock:
_ref_count = max(0, _ref_count - 1)
2025-09-09 16:30:12 +08:00
return (False, "人脸引擎不可用或特征库为空")
2025-09-04 22:59:27 +08:00
try:
2025-09-09 16:30:12 +08:00
# GPU计算时确保帧数据是连续内存避免CUDA错误
if _using_gpu and engine is not None and not frame.flags.contiguous:
2025-09-05 17:23:50 +08:00
frame = np.ascontiguousarray(frame)
2025-09-09 16:30:12 +08:00
# 执行人脸检测与特征提取
faces = engine.get(frame)
2025-09-04 22:59:27 +08:00
except Exception as e:
2025-09-09 16:30:12 +08:00
print(f"人脸检测过程出错: {e}")
# 出错时尝试重新初始化引擎可能是GPU状态变化导致
print("尝试重新初始化人脸引擎...")
2025-09-05 17:23:50 +08:00
with _lock:
_ref_count = max(0, _ref_count - 1)
2025-09-09 16:30:12 +08:00
load_model(prefer_gpu=True)
2025-09-04 22:59:27 +08:00
return (False, f"检测错误: {str(e)}")
result_parts = []
2025-09-09 16:30:12 +08:00
has_matched_known_face = False # 是否有任意人脸匹配到已知库
2025-09-04 22:59:27 +08:00
for face in faces:
2025-09-09 16:30:12 +08:00
# 归一化当前检测到的人脸特征
face_embedding = face.embedding.astype(np.float32)
norm = np.linalg.norm(face_embedding)
2025-09-04 22:59:27 +08:00
if norm == 0:
continue
2025-09-09 16:30:12 +08:00
face_embedding = face_embedding / norm
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
# 与已知人脸特征逐一比对
max_similarity, best_match_name = -1.0, "Unknown"
2025-09-04 22:59:27 +08:00
for name in _known_faces_names:
known_emb = _known_faces_embeddings[name]
2025-09-09 16:30:12 +08:00
similarity = np.dot(face_embedding, known_emb) # 余弦相似度
if similarity > max_similarity:
max_similarity = similarity
best_match_name = name
# 判断是否匹配成功
is_matched = max_similarity >= similarity_threshold
if is_matched:
has_matched_known_face = True
# 记录该人脸的检测结果
bbox = face.bbox # 人脸边界框
2025-09-04 22:59:27 +08:00
result_parts.append(
2025-09-09 16:30:12 +08:00
f"{'匹配' if is_matched else '未匹配'}: {best_match_name} "
f"(相似度: {max_similarity:.2f}, 边界框: {bbox.astype(int).tolist()})"
2025-09-04 22:59:27 +08:00
)
2025-09-09 16:30:12 +08:00
# 构建最终结果字符串
result_str = "未检测到人脸" if not result_parts else "; ".join(result_parts)
2025-09-04 22:59:27 +08:00
2025-09-09 16:30:12 +08:00
# 释放引用计数(线程安全)
2025-09-05 17:23:50 +08:00
with _lock:
_ref_count = max(0, _ref_count - 1)
2025-09-09 16:30:12 +08:00
# 若仍有引用更新最后使用时间若引用为0也立即标记加快闲置检测
_last_used_time = time.time()
2025-09-05 17:23:50 +08:00
2025-09-09 16:30:12 +08:00
return (has_matched_known_face, result_str)