511 lines
22 KiB
Python
511 lines
22 KiB
Python
import cv2
|
|
import os
|
|
import numpy as np
|
|
from typing import Dict, Any, Tuple, Optional
|
|
from .logging_utils import logger
|
|
|
|
class FaceDetector:
|
|
# 类变量用于缓存模型
|
|
_face_cascade = None
|
|
_mediapipe_face_mesh = None
|
|
_dlib_detector = None
|
|
_dlib_predictor = None
|
|
_face_analyzer = None # InsightFace模型
|
|
|
|
# 缓存变量
|
|
_last_image_hash = None
|
|
_last_face_info = None
|
|
|
|
@staticmethod
|
|
def analyze_face(image, mask, detection_method="insightface"):
|
|
"""分析人脸,返回人脸信息"""
|
|
# 计算图像哈希值,用于缓存
|
|
image_hash = hash(image.tobytes())
|
|
|
|
# 检查缓存
|
|
if FaceDetector._last_image_hash == image_hash and FaceDetector._last_face_info is not None:
|
|
return FaceDetector._last_face_info.copy()
|
|
|
|
# 初始化人脸信息
|
|
face_info = {
|
|
"face_width": None, # 人脸宽度
|
|
"face_height": None, # 人脸高度
|
|
"face_rect": None, # 人脸矩形 (x, y, w, h)
|
|
"eyes_center": None, # 眼睛中心点 (x, y)
|
|
"gender": "unknown", # 性别
|
|
"gender_confidence": 0.0, # 性别置信度
|
|
"confidence": 0.0 # 检测置信度
|
|
}
|
|
|
|
# 根据选择的检测方法进行人脸分析
|
|
detection_methods = []
|
|
|
|
if detection_method == "auto":
|
|
detection_methods = ["insightface", "dlib", "opencv", "mediapipe", "mask"]
|
|
elif detection_method == "none":
|
|
# 仅使用遮罩分析
|
|
detection_methods = ["mask"]
|
|
else:
|
|
detection_methods = [detection_method, "mask"] # 总是添加遮罩作为备选
|
|
|
|
# 尝试使用选定的方法进行人脸检测
|
|
for method in detection_methods:
|
|
if method == "insightface" and face_info["confidence"] < 0.9:
|
|
FaceDetector._analyze_face_insightface(image, face_info)
|
|
|
|
# 如果InsightFace检测成功且置信度足够高,就不再尝试其他方法
|
|
if face_info["confidence"] >= 0.9:
|
|
break
|
|
|
|
elif method == "dlib" and face_info["confidence"] < 0.8:
|
|
FaceDetector._analyze_face_dlib(image, face_info)
|
|
|
|
# 如果dlib检测成功且置信度足够高,就不再尝试其他方法
|
|
if face_info["confidence"] >= 0.8:
|
|
break
|
|
|
|
elif method == "opencv" and face_info["confidence"] < 0.7:
|
|
FaceDetector._analyze_face_opencv(image, face_info)
|
|
|
|
# 如果OpenCV检测成功且置信度足够高,就不再尝试其他方法
|
|
if face_info["confidence"] >= 0.7:
|
|
break
|
|
|
|
elif method == "mediapipe" and face_info["confidence"] < 0.9:
|
|
FaceDetector._analyze_face_mediapipe(image, face_info)
|
|
|
|
# 如果MediaPipe检测成功且置信度足够高,就不再尝试其他方法
|
|
if face_info["confidence"] >= 0.9:
|
|
break
|
|
|
|
elif method == "mask" and face_info["confidence"] < 0.5:
|
|
FaceDetector._analyze_face_mask(image, mask, face_info)
|
|
|
|
# 如果仍然没有检测到眼睛中心,使用图像中心点上移1/4处
|
|
if face_info["eyes_center"] is None:
|
|
h, w = image.shape[:2]
|
|
center_x = w // 2
|
|
eyes_y = h // 4
|
|
face_info["eyes_center"] = (center_x, eyes_y)
|
|
|
|
# 更新缓存
|
|
FaceDetector._last_image_hash = image_hash
|
|
FaceDetector._last_face_info = face_info.copy()
|
|
|
|
return face_info
|
|
|
|
@staticmethod
|
|
def _analyze_face_insightface(image, face_info):
|
|
"""使用InsightFace进行人脸分析"""
|
|
try:
|
|
# 延迟导入InsightFace
|
|
from insightface.app import FaceAnalysis
|
|
|
|
# 延迟加载InsightFace模型
|
|
if FaceDetector._face_analyzer is None:
|
|
FaceDetector._face_analyzer = FaceDetector._load_insightface()
|
|
if FaceDetector._face_analyzer is None:
|
|
logger.warning("InsightFace模型加载失败,跳过InsightFace分析")
|
|
return False
|
|
|
|
# 性能优化:缩小图像进行检测
|
|
h, w = image.shape[:2]
|
|
scale = 1.0
|
|
|
|
# 如果图像较大,先缩小进行检测(与dlib类似的优化)
|
|
if max(h, w) > 800:
|
|
scale = 800 / max(h, w)
|
|
small_img = cv2.resize(image, (0, 0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
|
|
else:
|
|
small_img = image
|
|
|
|
# 转换为RGB格式
|
|
rgb_image = cv2.cvtColor(small_img, cv2.COLOR_BGR2RGB)
|
|
|
|
# 分析人脸
|
|
faces = FaceDetector._face_analyzer.get(rgb_image)
|
|
|
|
if len(faces) > 0:
|
|
# 获取最大的人脸
|
|
max_area = 0
|
|
max_face = None
|
|
for face in faces:
|
|
bbox = face.bbox.astype(int)
|
|
area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
|
|
if area > max_area:
|
|
max_area = area
|
|
max_face = face
|
|
|
|
if max_face is not None:
|
|
# 获取人脸边界框
|
|
bbox = max_face.bbox.astype(int)
|
|
x, y, x2, y2 = bbox
|
|
|
|
# 如果进行了缩放,恢复原始坐标
|
|
if scale != 1.0:
|
|
x = int(x / scale)
|
|
y = int(y / scale)
|
|
x2 = int(x2 / scale)
|
|
y2 = int(y2 / scale)
|
|
|
|
w, h = x2 - x, y2 - y
|
|
|
|
# 更新人脸信息
|
|
face_info["face_width"] = w
|
|
face_info["face_height"] = h
|
|
face_info["face_rect"] = (x, y, w, h)
|
|
face_info["confidence"] = float(max_face.det_score)
|
|
|
|
# 获取关键点
|
|
landmarks = max_face.landmark_2d_106
|
|
if landmarks is not None and len(landmarks) > 0:
|
|
# 计算眼睛中心点
|
|
left_eye = landmarks[96] # 左眼中心
|
|
right_eye = landmarks[97] # 右眼中心
|
|
|
|
# 如果进行了缩放,恢复原始坐标
|
|
if scale != 1.0:
|
|
eyes_center_x = int((left_eye[0] / scale + right_eye[0] / scale) / 2)
|
|
eyes_center_y = int((left_eye[1] / scale + right_eye[1] / scale) / 2)
|
|
else:
|
|
eyes_center_x = int((left_eye[0] + right_eye[0]) / 2)
|
|
eyes_center_y = int((left_eye[1] + right_eye[1]) / 2)
|
|
|
|
face_info["eyes_center"] = (eyes_center_x, eyes_center_y)
|
|
|
|
# 获取性别
|
|
if hasattr(max_face, 'gender') and max_face.gender is not None:
|
|
gender_idx = int(max_face.gender)
|
|
gender = "male" if gender_idx == 1 else "female"
|
|
face_info["gender"] = gender
|
|
face_info["gender_confidence"] = 0.9 # InsightFace没有提供性别置信度
|
|
logger.info(f"InsightFace检测性别: {gender}")
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"InsightFace人脸分析失败: {str(e)}")
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _analyze_face_opencv(image, face_info):
|
|
"""使用OpenCV进行人脸分析"""
|
|
try:
|
|
# 延迟加载人脸检测器
|
|
if FaceDetector._face_cascade is None:
|
|
FaceDetector._face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
faces = FaceDetector._face_cascade.detectMultiScale(gray, 1.1, 4)
|
|
|
|
if len(faces) > 0:
|
|
# 获取最大的人脸
|
|
max_area = 0
|
|
max_face = None
|
|
for (x, y, w, h) in faces:
|
|
area = w * h
|
|
if area > max_area:
|
|
max_area = area
|
|
max_face = (x, y, w, h)
|
|
|
|
x, y, w, h = max_face
|
|
face_info["face_width"] = w
|
|
face_info["face_height"] = h
|
|
face_info["face_rect"] = (x, y, w, h)
|
|
face_info["confidence"] = 0.7
|
|
|
|
# 估计眼睛位置 - OpenCV只能粗略估计
|
|
eyes_x = x + w // 2
|
|
eyes_y = y + int(h * 0.4) # 眼睛通常在人脸上部40%处
|
|
face_info["eyes_center"] = (eyes_x, eyes_y)
|
|
|
|
# 简单的性别估计 - 基于脸部宽高比
|
|
face_ratio = w / h if h > 0 else 0
|
|
if face_ratio > 0.78: # 男性脸部通常更宽
|
|
face_info["gender"] = "male"
|
|
else:
|
|
face_info["gender"] = "female"
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"OpenCV人脸分析失败: {str(e)}")
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _analyze_face_mediapipe(image, face_info):
|
|
"""使用MediaPipe进行人脸分析"""
|
|
try:
|
|
# 延迟导入和加载MediaPipe
|
|
import mediapipe as mp
|
|
|
|
if FaceDetector._mediapipe_face_mesh is None:
|
|
mp_face_mesh = mp.solutions.face_mesh
|
|
FaceDetector._mediapipe_face_mesh = mp_face_mesh.FaceMesh(
|
|
static_image_mode=True,
|
|
max_num_faces=1,
|
|
refine_landmarks=True,
|
|
min_detection_confidence=0.5)
|
|
|
|
# 转换为RGB格式
|
|
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
h, w = rgb_image.shape[:2]
|
|
|
|
# 处理图像
|
|
mp_results = FaceDetector._mediapipe_face_mesh.process(rgb_image)
|
|
|
|
if mp_results.multi_face_landmarks:
|
|
landmarks = mp_results.multi_face_landmarks[0].landmark
|
|
|
|
# 计算人脸边界框
|
|
x_min = min(landmark.x for landmark in landmarks) * w
|
|
y_min = min(landmark.y for landmark in landmarks) * h
|
|
x_max = max(landmark.x for landmark in landmarks) * w
|
|
y_max = max(landmark.y for landmark in landmarks) * h
|
|
|
|
face_width = x_max - x_min
|
|
face_height = y_max - y_min
|
|
|
|
# 更新人脸信息,因为MediaPipe通常比OpenCV更准确
|
|
face_info["face_width"] = face_width
|
|
face_info["face_height"] = face_height
|
|
face_info["face_rect"] = (int(x_min), int(y_min), int(face_width), int(face_height))
|
|
face_info["confidence"] = 0.9
|
|
|
|
# MediaPipe中眼睛关键点的索引
|
|
LEFT_EYE_INDICES = [362, 385, 387, 263, 373, 380] # 左眼轮廓点
|
|
RIGHT_EYE_INDICES = [33, 160, 158, 133, 153, 144] # 右眼轮廓点
|
|
|
|
# 计算左眼中心
|
|
left_eye_x = sum(landmarks[idx].x for idx in LEFT_EYE_INDICES) / len(LEFT_EYE_INDICES) * w
|
|
left_eye_y = sum(landmarks[idx].y for idx in LEFT_EYE_INDICES) / len(LEFT_EYE_INDICES) * h
|
|
|
|
# 计算右眼中心
|
|
right_eye_x = sum(landmarks[idx].x for idx in RIGHT_EYE_INDICES) / len(RIGHT_EYE_INDICES) * w
|
|
right_eye_y = sum(landmarks[idx].y for idx in RIGHT_EYE_INDICES) / len(RIGHT_EYE_INDICES) * h
|
|
|
|
# 计算两眼中心
|
|
eyes_center_x = int((left_eye_x + right_eye_x) / 2)
|
|
eyes_center_y = int((left_eye_y + right_eye_y) / 2)
|
|
|
|
face_info["eyes_center"] = (eyes_center_x, eyes_center_y)
|
|
|
|
# 性别估计 - 基于多个特征点
|
|
# 计算下巴宽度与脸部高度的比例
|
|
jaw_width = abs(landmarks[152].x - landmarks[377].x) * w
|
|
face_height = abs(landmarks[10].y - landmarks[152].y) * h
|
|
jaw_face_ratio = jaw_width / face_height if face_height > 0 else 0
|
|
|
|
# 计算眉间距离与脸部宽度的比例
|
|
brow_distance = abs(landmarks[107].x - landmarks[336].x) * w
|
|
brow_face_ratio = brow_distance / face_width if face_width > 0 else 0
|
|
|
|
# 基于多个特征估计性别
|
|
if jaw_face_ratio > 0.85 and brow_face_ratio < 0.4:
|
|
face_info["gender"] = "male"
|
|
elif jaw_face_ratio < 0.8 and brow_face_ratio > 0.35:
|
|
face_info["gender"] = "female"
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"MediaPipe人脸分析失败: {str(e)}")
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _analyze_face_dlib(image, face_info):
|
|
"""使用dlib进行人脸分析"""
|
|
try:
|
|
# 延迟导入dlib
|
|
import dlib
|
|
|
|
# 获取模型文件路径
|
|
predictor_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models", "shape_predictor_68_face_landmarks.dat")
|
|
|
|
if not os.path.exists(predictor_path):
|
|
logger.warning(f"dlib模型文件不存在: {predictor_path}")
|
|
return False
|
|
|
|
# 延迟加载dlib模型
|
|
if FaceDetector._dlib_detector is None:
|
|
FaceDetector._dlib_detector = dlib.get_frontal_face_detector()
|
|
|
|
if FaceDetector._dlib_predictor is None:
|
|
FaceDetector._dlib_predictor = dlib.shape_predictor(predictor_path)
|
|
|
|
# 性能优化:缩小图像进行检测
|
|
h, w = image.shape[:2]
|
|
scale = 1.0
|
|
|
|
# 如果图像较大,先缩小进行检测
|
|
if max(h, w) > 800:
|
|
scale = 800 / max(h, w)
|
|
small_img = cv2.resize(image, (0, 0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
|
|
else:
|
|
small_img = image
|
|
|
|
# 转换为灰度图
|
|
gray = cv2.cvtColor(small_img, cv2.COLOR_BGR2GRAY)
|
|
|
|
# 检测人脸 - 使用更快的参数
|
|
faces = FaceDetector._dlib_detector(gray, 0)
|
|
|
|
if len(faces) > 0:
|
|
# 获取人脸关键点
|
|
shape = FaceDetector._dlib_predictor(gray, faces[0])
|
|
|
|
# 计算人脸边界框
|
|
x_min = min(shape.part(i).x for i in range(shape.num_parts))
|
|
y_min = min(shape.part(i).y for i in range(shape.num_parts))
|
|
x_max = max(shape.part(i).x for i in range(shape.num_parts))
|
|
y_max = max(shape.part(i).y for i in range(shape.num_parts))
|
|
|
|
# 如果进行了缩放,恢复原始坐标
|
|
if scale != 1.0:
|
|
x_min = int(x_min / scale)
|
|
y_min = int(y_min / scale)
|
|
x_max = int(x_max / scale)
|
|
y_max = int(y_max / scale)
|
|
|
|
face_width = x_max - x_min
|
|
face_height = y_max - y_min
|
|
|
|
# 更新人脸信息
|
|
face_info["face_width"] = face_width
|
|
face_info["face_height"] = face_height
|
|
face_info["face_rect"] = (x_min, y_min, face_width, face_height)
|
|
face_info["confidence"] = 0.8
|
|
|
|
# 计算眼睛中心点
|
|
left_eye_center_idx = 37 # 左眼中心点索引
|
|
right_eye_center_idx = 44 # 右眼中心点索引
|
|
|
|
# 如果进行了缩放,恢复原始坐标
|
|
if scale != 1.0:
|
|
left_eye_x = int(shape.part(left_eye_center_idx).x / scale)
|
|
left_eye_y = int(shape.part(left_eye_center_idx).y / scale)
|
|
right_eye_x = int(shape.part(right_eye_center_idx).x / scale)
|
|
right_eye_y = int(shape.part(right_eye_center_idx).y / scale)
|
|
else:
|
|
left_eye_x = shape.part(left_eye_center_idx).x
|
|
left_eye_y = shape.part(left_eye_center_idx).y
|
|
right_eye_x = shape.part(right_eye_center_idx).x
|
|
right_eye_y = shape.part(right_eye_center_idx).y
|
|
|
|
# 计算两眼中心
|
|
eyes_center_x = int((left_eye_x + right_eye_x) / 2)
|
|
eyes_center_y = int((left_eye_y + right_eye_y) / 2)
|
|
|
|
face_info["eyes_center"] = (eyes_center_x, eyes_center_y)
|
|
|
|
# 性别估计 - 简化计算
|
|
# 使用眉间距离与脸部宽度的比例作为性别特征
|
|
if scale != 1.0:
|
|
brow_distance = abs(int(shape.part(21).x / scale) - int(shape.part(22).x / scale))
|
|
else:
|
|
brow_distance = abs(shape.part(21).x - shape.part(22).x)
|
|
|
|
brow_face_ratio = brow_distance / face_width if face_width > 0 else 0
|
|
|
|
# 简化性别判断
|
|
if brow_face_ratio < 0.15: # 眉间距离较小,可能是男性
|
|
face_info["gender"] = "male"
|
|
else:
|
|
face_info["gender"] = "female"
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"dlib人脸分析失败: {str(e)}")
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def _analyze_face_mask(image, mask, face_info):
|
|
"""使用遮罩分析人脸"""
|
|
try:
|
|
if np.max(mask) > 0:
|
|
# 计算遮罩的非零区域
|
|
non_zero_points = np.argwhere(mask > 0)
|
|
if len(non_zero_points) > 0:
|
|
y_min, x_min = non_zero_points.min(axis=0)
|
|
y_max, x_max = non_zero_points.max(axis=0)
|
|
|
|
mask_height = y_max - y_min
|
|
mask_width = x_max - x_min
|
|
|
|
# 估计人脸高度为遮罩高度的一部分
|
|
face_info["face_height"] = mask_height * 0.4 # 假设人脸占遮罩高度的40%
|
|
face_info["face_width"] = mask_width * 0.4 # 假设人脸占遮罩宽度的40%
|
|
face_info["face_rect"] = (x_min, y_min, mask_width, mask_height)
|
|
face_info["confidence"] = 0.5
|
|
|
|
# 估计眼睛位置在遮罩上部的1/4处
|
|
mask_center_x = (x_min + x_max) // 2
|
|
eyes_y = int(y_min + mask_height * 0.25)
|
|
face_info["eyes_center"] = (mask_center_x, eyes_y)
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"遮罩分析失败: {str(e)}")
|
|
|
|
return False
|
|
|
|
|
|
@staticmethod
|
|
def _load_insightface():
|
|
"""加载InsightFace模型"""
|
|
try:
|
|
import insightface
|
|
from insightface.app import FaceAnalysis
|
|
import os
|
|
|
|
# 获取当前脚本所在目录
|
|
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
# 获取ComfyUI根目录
|
|
comfyui_root = os.path.dirname(os.path.dirname(current_dir))
|
|
# 设置模型路径为相对路径
|
|
models_dir = os.path.join(comfyui_root, "models", "insightface")
|
|
buffalo_path = os.path.join(models_dir, "models", "buffalo_l")
|
|
|
|
# 检查模型文件是否存在
|
|
required_files = ["1k3d68.onnx", "2d106det.onnx", "det_10g.onnx", "genderage.onnx", "w600k_r50.onnx"]
|
|
models_exist = True
|
|
|
|
# 确保目录存在
|
|
if not os.path.exists(buffalo_path):
|
|
os.makedirs(buffalo_path, exist_ok=True)
|
|
models_exist = False
|
|
else:
|
|
# 检查所有必需的模型文件
|
|
for file in required_files:
|
|
if not os.path.exists(os.path.join(buffalo_path, file)):
|
|
models_exist = False
|
|
break
|
|
|
|
if models_exist:
|
|
logger.info(f"InsightFace模型已存在,直接使用本地模型: {buffalo_path}")
|
|
else:
|
|
logger.info(f"未找到完整的InsightFace模型,将下载到: {buffalo_path}")
|
|
|
|
# 设置InsightFace模型路径
|
|
os.environ['INSIGHTFACE_HOME'] = models_dir
|
|
|
|
# 创建分析器 - 使用更小的检测尺寸以提高速度
|
|
face_analyzer = FaceAnalysis(
|
|
name="buffalo_l",
|
|
root=models_dir,
|
|
providers=['CPUExecutionProvider'],
|
|
allowed_modules=['detection', 'landmark_2d_106', 'genderage'] # 只加载必要的模块
|
|
)
|
|
|
|
# 准备模型 - 使用更小的检测尺寸
|
|
face_analyzer.prepare(ctx_id=0, det_size=(320, 320)) # 降低检测尺寸以提高速度
|
|
|
|
if models_exist:
|
|
logger.info("InsightFace模型加载成功")
|
|
else:
|
|
logger.info(f"InsightFace模型下载并加载成功,保存在: {buffalo_path}")
|
|
|
|
return face_analyzer
|
|
except Exception as e:
|
|
logger.error(f"InsightFace模型加载失败: {str(e)}") |