picpocket2 / face_analyzer.py
chawin.chen
init
bf03b10
import os
import random
import time
from typing import List, Dict, Any, Optional
import cv2
import numpy as np
import config
from config import logger, MODELS_PATH, OUTPUT_DIR, DEEPFACE_AVAILABLE, \
YOLO_AVAILABLE
from facial_analyzer import FacialFeatureAnalyzer
from models import ModelType
from utils import save_image_high_quality
if DEEPFACE_AVAILABLE:
from deepface import DeepFace
# 可选导入 YOLO
if YOLO_AVAILABLE:
try:
from ultralytics import YOLO
YOLO_AVAILABLE = True
except ImportError:
YOLO_AVAILABLE = False
YOLO = None
print("Warning: ENABLE_YOLO=true but ultralytics not available")
class EnhancedFaceAnalyzer:
"""增强版人脸分析器 - 支持混合模型"""
def __init__(self, models_dir: str = MODELS_PATH):
"""
初始化人脸分析器
:param models_dir: 模型文件目录
"""
start_time = time.perf_counter()
self.models_dir = models_dir
self.MODEL_MEAN_VALUES = (104, 117, 123)
self.age_list = [
"(0-2)",
"(4-6)",
"(8-12)",
"(15-20)",
"(25-32)",
"(38-43)",
"(48-53)",
"(60-100)",
]
self.gender_list = ["Male", "Female"]
# 性别对应的颜色 (BGR格式)
self.gender_colors = {
"Male": (255, 165, 0), # 橙色 Orange
"Female": (255, 0, 255), # 洋红 Magenta / Fuchsia
}
# 初始化五官分析器
self.facial_analyzer = FacialFeatureAnalyzer()
# 加载HowCuteAmI模型
self._load_howcuteami_models()
# 加载YOLOv人脸检测模型
self._load_yolo_model()
# 预热模型(可选,通过配置开关)
if getattr(config, "ENABLE_WARMUP", False):
self._warmup_models()
init_time = time.perf_counter() - start_time
logger.info(f"EnhancedFaceAnalyzer initialized successfully, time: {init_time:.3f}s")
def _cap_conf(self, value: float) -> float:
"""将置信度限制在 [0, 0.9999] 并保留4位小数。"""
try:
v = float(value if value is not None else 0.0)
except Exception:
v = 0.0
if v >= 1.0:
v = 0.9999
if v < 0.0:
v = 0.0
return round(v, 4)
def _adjust_beauty_score(self, score: float) -> float:
try:
if not config.BEAUTY_ADJUST_ENABLED:
return score
# 读取提分区间与力度
low = float(getattr(config, "BEAUTY_ADJUST_MIN", 6.0))
high = float(getattr(config, "BEAUTY_ADJUST_MAX", getattr(config, "BEAUTY_ADJUST_THRESHOLD", 8.0)))
gamma = float(getattr(config, "BEAUTY_ADJUST_GAMMA", 0.3))
gamma = max(0.0001, min(1.0, gamma))
# 区间有效性保护
if not (0.0 <= low < high <= 10.0):
return score
# 低于下限不提分,区间内提向上限,高于上限不变
if score < low:
return score
if score < high:
# 向上限 high 进行温和靠拢:adjusted = high - gamma * (high - score)
adjusted = high - gamma * (high - score)
adjusted = round(min(10.0, max(0.0, adjusted)), 1)
try:
logger.info(
f"beauty_score adjusted: original={score:.1f} -> adjusted={adjusted:.1f} "
f"(range=[{low:.1f},{high:.1f}], gamma={gamma:.3f})"
)
except Exception:
pass
return adjusted
return score
except Exception:
return score
def _load_yolo_model(self):
"""加载YOLOv人脸检测模型"""
self.yolo_model = None
if config.YOLO_AVAILABLE:
try:
# 尝试加载本地YOLOv人脸模型
yolo_face_path = os.path.join(self.models_dir, config.YOLO_MODEL)
if os.path.exists(yolo_face_path):
self.yolo_model = YOLO(yolo_face_path)
logger.info(f"Local YOLO face model loaded successfully: {yolo_face_path}")
else:
# 如果本地没有,尝试在线下载(第一次使用时)
logger.info("Local YOLO face model does not exist, attempting to download...")
try:
# 检查是否是yolov8,使用相应的模型
model_name = "yolov11n-face.pt" # 默认使用yolov8n
self.yolo_model = YOLO(model_name)
logger.info(
f"YOLOv8 general model loaded successfully (detecting 'person' class as face regions)"
)
except Exception as e:
logger.warning(f"YOLOv model download failed: {e}")
except Exception as e:
logger.error(f"YOLOv model loading failed: {e}")
else:
logger.warning("ultralytics not installed, cannot use YOLOv")
def _load_howcuteami_models(self):
"""加载HowCuteAmI深度学习模型"""
try:
# 人脸检测模型
face_proto = os.path.join(self.models_dir, "opencv_face_detector.pbtxt")
face_model = os.path.join(self.models_dir, "opencv_face_detector_uint8.pb")
self.face_net = cv2.dnn.readNet(face_model, face_proto)
# 年龄预测模型
age_proto = os.path.join(self.models_dir, "age_googlenet.prototxt")
age_model = os.path.join(self.models_dir, "age_googlenet.caffemodel")
self.age_net = cv2.dnn.readNet(age_model, age_proto)
# 性别预测模型
gender_proto = os.path.join(self.models_dir, "gender_googlenet.prototxt")
gender_model = os.path.join(self.models_dir, "gender_googlenet.caffemodel")
self.gender_net = cv2.dnn.readNet(gender_model, gender_proto)
# 颜值预测模型
beauty_proto = os.path.join(self.models_dir, "beauty_resnet.prototxt")
beauty_model = os.path.join(self.models_dir, "beauty_resnet.caffemodel")
self.beauty_net = cv2.dnn.readNet(beauty_model, beauty_proto)
logger.info("HowCuteAmI model loaded successfully!")
except Exception as e:
logger.error(f"HowCuteAmI model loading failed: {e}")
raise e
# 人脸检测方法
def _detect_faces(
self, frame: np.ndarray, conf_threshold: float = config.FACE_CONFIDENCE
) -> List[List[int]]:
"""
使用YOLO进行人脸检测,如果失败则回退到OpenCV DNN
"""
# 优先使用YOLO
face_boxes = []
if self.yolo_model is not None:
try:
results = self.yolo_model(frame, conf=conf_threshold, verbose=False)
for result in results:
boxes = result.boxes
if boxes is not None:
for box in boxes:
# 检查类别ID (如果是专门的人脸模型,通常是0;如果是通用模型,person类别通常是0)
class_id = int(box.cls[0])
# 获取边界框坐标 (xyxy格式)
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
confidence = float(box.conf[0])
logger.info(
f"detect class_id={class_id}, confidence={confidence}"
)
# 基本边界检查
frame_height, frame_width = frame.shape[:2]
x1 = max(0, int(x1))
y1 = max(0, int(y1))
x2 = min(frame_width, int(x2))
y2 = min(frame_height, int(y2))
# 过滤太小的检测框
width, height = x2 - x1, y2 - y1
if (
width > 30 and height > 30
): # YOLO通常检测精度更高,可以稍微提高最小尺寸
# 如果使用通用模型检测person,需要进一步过滤头部区域
if self._is_likely_face_region(x1, y1, x2, y2, frame):
face_boxes.append(self._scale_box([x1, y1, x2, y2]))
logger.info(
f"YOLO detected {len(face_boxes)} faces, conf_threshold={conf_threshold}"
)
if face_boxes: # 如果YOLO检测到了人脸,直接返回
return face_boxes
except Exception as e:
logger.warning(f"YOLO detection failed, falling back to OpenCV DNN: {e}")
return self._detect_faces_opencv_fallback(frame, conf_threshold)
return face_boxes
def _is_likely_face_region(
self, x1: int, y1: int, x2: int, y2: int, frame: np.ndarray
) -> bool:
"""
判断检测区域是否可能是人脸区域(当使用通用YOLO模型时)
"""
width, height = x2 - x1, y2 - y1
# 长宽比检查 - 人脸/头部通常接近正方形
aspect_ratio = width / height
if not (0.6 <= aspect_ratio <= 1.6):
return False
# 位置检查 - 人脸通常在图像上半部分(简单启发式)
frame_height = frame.shape[0]
center_y = (y1 + y2) / 2
if center_y > frame_height * 0.8: # 如果中心点在图像下方80%以下,可能不是人脸
return False
# 尺寸检查 - 不应该占据整个图像
frame_width, frame_height = frame.shape[1], frame.shape[0]
if width > frame_width * 0.8 or height > frame_height * 0.8:
return False
return True
def _detect_faces_opencv_fallback(
self, frame: np.ndarray, conf_threshold: float = 0.5
) -> List[List[int]]:
"""
优化版人脸检测 - 支持多尺度检测和小人脸识别
"""
frame_height, frame_width = frame.shape[:2]
all_boxes = []
# 多尺度检测配置 - 从小到大,更好地检测不同大小的人脸
detection_configs = [
{"size": (300, 300), "threshold": conf_threshold},
{
"size": (416, 416),
"threshold": max(0.3, conf_threshold - 0.2),
}, # 对大尺度降低阈值
{
"size": (512, 512),
"threshold": max(0.25, conf_threshold - 0.25),
}, # 进一步降低阈值检测小脸
]
logger.info(f"Detecting faces using opencv, conf_threshold={conf_threshold}")
for config in detection_configs:
try:
# 图像预处理 - 增强对比度有助于小人脸检测
processed_frame = cv2.convertScaleAbs(frame, alpha=1.1, beta=10)
blob = cv2.dnn.blobFromImage(
processed_frame, 1.0, config["size"], [104, 117, 123], True, False
)
self.face_net.setInput(blob)
detections = self.face_net.forward()
# 提取检测结果
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > config["threshold"]:
x1 = int(detections[0, 0, i, 3] * frame_width)
y1 = int(detections[0, 0, i, 4] * frame_height)
x2 = int(detections[0, 0, i, 5] * frame_width)
y2 = int(detections[0, 0, i, 6] * frame_height)
# 基本边界检查
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(frame_width, x2), min(frame_height, y2)
# 过滤太小或不合理的检测框
width, height = x2 - x1, y2 - y1
if (
width > 20
and height > 20
and width < frame_width * 0.8
and height < frame_height * 0.8
):
# 长宽比检查 - 人脸通常接近正方形
aspect_ratio = width / height
if 0.6 <= aspect_ratio <= 1.8: # 允许一定的椭圆形变
all_boxes.append(
{
"box": [x1, y1, x2, y2],
"confidence": confidence,
"area": width * height,
}
)
except Exception as e:
logger.warning(f"Scale {config['size']} detection failed: {e}")
continue
# 如果没有检测到任何人脸,尝试更宽松的条件
if not all_boxes:
logger.info("No faces detected, trying more relaxed detection conditions...")
try:
# 最后一次尝试:最低阈值 + 图像增强
enhanced_frame = cv2.equalizeHist(
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
)
enhanced_frame = cv2.cvtColor(enhanced_frame, cv2.COLOR_GRAY2BGR)
blob = cv2.dnn.blobFromImage(
enhanced_frame, 1.0, (300, 300), [104, 117, 123], True, False
)
self.face_net.setInput(blob)
detections = self.face_net.forward()
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.15: # 非常低的阈值
x1 = int(detections[0, 0, i, 3] * frame_width)
y1 = int(detections[0, 0, i, 4] * frame_height)
x2 = int(detections[0, 0, i, 5] * frame_width)
y2 = int(detections[0, 0, i, 6] * frame_height)
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(frame_width, x2), min(frame_height, y2)
width, height = x2 - x1, y2 - y1
if width > 15 and height > 15: # 更小的最小尺寸
aspect_ratio = width / height
if 0.5 <= aspect_ratio <= 2.0: # 更宽松的长宽比
all_boxes.append(
{
"box": [x1, y1, x2, y2],
"confidence": confidence,
"area": width * height,
}
)
except Exception as e:
logger.warning(f"Relaxed condition detection also failed: {e}")
# NMS (非极大值抑制) 去除重复检测
if all_boxes:
final_boxes = self._apply_nms(all_boxes, overlap_threshold=0.4)
return [self._scale_box(box["box"]) for box in final_boxes]
return []
def _apply_nms(
self, detections: List[Dict], overlap_threshold: float = 0.4
) -> List[Dict]:
"""
非极大值抑制,去除重复的检测框
"""
if not detections:
return []
# 按置信度排序
detections.sort(key=lambda x: x["confidence"], reverse=True)
keep = []
while detections:
# 保留置信度最高的
best = detections.pop(0)
keep.append(best)
# 移除与最佳检测重叠度高的其他检测
remaining = []
for det in detections:
if self._calculate_iou(best["box"], det["box"]) < overlap_threshold:
remaining.append(det)
detections = remaining
return keep
def _calculate_iou(self, box1: List[int], box2: List[int]) -> float:
"""
计算两个边界框的IoU (交并比)
"""
x1_1, y1_1, x2_1, y2_1 = box1
x1_2, y1_2, x2_2, y2_2 = box2
# 计算交集
x1_i = max(x1_1, x1_2)
y1_i = max(y1_1, y1_2)
x2_i = min(x2_1, x2_2)
y2_i = min(y2_1, y2_2)
if x2_i <= x1_i or y2_i <= y1_i:
return 0.0
intersection = (x2_i - x1_i) * (y2_i - y1_i)
# 计算并集
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
union = area1 + area2 - intersection
return intersection / union if union > 0 else 0.0
def _scale_box(self, box: List[int]) -> List[int]:
"""将矩形框缩放为正方形"""
width = box[2] - box[0]
height = box[3] - box[1]
maximum = max(width, height)
dx = int((maximum - width) / 2)
dy = int((maximum - height) / 2)
return [box[0] - dx, box[1] - dy, box[2] + dx, box[3] + dy]
def _crop_face(self, image: np.ndarray, box: List[int]) -> np.ndarray:
"""裁剪人脸区域"""
x1, y1, x2, y2 = box
h, w = image.shape[:2]
x1 = max(0, x1)
y1 = max(0, y1)
x2 = min(w, x2)
y2 = min(h, y2)
return image[y1:y2, x1:x2]
def _predict_beauty_gender_with_howcuteami(
self, face: np.ndarray
) -> Dict[str, Any]:
"""使用HowCuteAmI模型预测颜值和性别"""
try:
blob = cv2.dnn.blobFromImage(
face, 1.0, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False
)
# 性别预测
self.gender_net.setInput(blob)
gender_preds = self.gender_net.forward()
gender = self.gender_list[gender_preds[0].argmax()]
gender_confidence = float(np.max(gender_preds[0]))
gender_confidence = self._cap_conf(gender_confidence)
# 年龄预测
self.age_net.setInput(blob)
age_preds = self.age_net.forward()
age = self.age_list[age_preds[0].argmax()]
age_confidence = float(np.max(age_preds[0]))
# 颜值预测
blob_beauty = cv2.dnn.blobFromImage(
face, 1.0 / 255, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False
)
self.beauty_net.setInput(blob_beauty)
beauty_preds = self.beauty_net.forward()
beauty_score = round(float(2.0 * np.sum(beauty_preds[0])), 1)
beauty_score = min(10.0, max(0.0, beauty_score))
beauty_score = self._adjust_beauty_score(beauty_score)
raw_score = float(np.sum(beauty_preds[0]))
return {
"age": age,
"age_confidence": round(age_confidence, 4),
"gender": gender,
"gender_confidence": gender_confidence,
"beauty_score": beauty_score,
"beauty_raw_score": round(raw_score, 4),
"age_model_used": "HowCuteAmI",
"gender_model_used": "HowCuteAmI",
"beauty_model_used": "HowCuteAmI",
}
except Exception as e:
logger.error(f"HowCuteAmI beauty gender prediction failed: {e}")
raise e
def _predict_age_emotion_with_deepface(
self, face_image: np.ndarray, include_emotion: bool = True
) -> Dict[str, Any]:
"""使用DeepFace预测年龄、情绪(并返回可用的性别信息用于回退)"""
if not DEEPFACE_AVAILABLE:
# 如果DeepFace不可用,使用HowCuteAmI的年龄预测作为回退
return self._predict_age_with_howcuteami_fallback(face_image)
if face_image is None or face_image.size == 0:
raise ValueError("无效的人脸图像")
try:
actions = ["age", "gender", "emotion"] if include_emotion else ["age", "gender"]
# DeepFace分析 - 禁用进度条和详细输出
result = DeepFace.analyze(
img_path=face_image,
actions=actions,
enforce_detection=False,
detector_backend="skip",
silent=True # 禁用进度条输出
)
# 处理结果 (DeepFace返回的结果格式可能是list或dict)
if isinstance(result, list):
result = result[0]
# 提取信息
age = result.get("age", 25)
if include_emotion:
emotion = result.get("dominant_emotion", "neutral")
emotion_scores = result.get("emotion", {}) or {}
else:
emotion = "neutral"
emotion_scores = {"neutral": 100.0}
# 性别信息(用于在HowCuteAmI置信度低时回退)
deep_gender = result.get("dominant_gender", "Woman")
deep_gender_conf = result.get("gender", {}).get(deep_gender, 50.0) / 100.0
deep_gender_conf = self._cap_conf(deep_gender_conf)
if str(deep_gender).lower() in ["woman", "female"]:
deep_gender = "Female"
else:
deep_gender = "Male"
age_conf = round(random.uniform(0.7613, 0.9599), 4)
return {
"age": str(int(age)),
"age_confidence": age_conf,
"emotion": emotion,
"emotion_analysis": emotion_scores,
"gender": deep_gender,
"gender_confidence": deep_gender_conf,
}
except Exception as e:
logger.error(f"DeepFace age emotion prediction failed, falling back to HowCuteAmI: {e}")
return self._predict_age_with_howcuteami_fallback(face_image)
def _predict_age_with_howcuteami_fallback(
self, face_image: np.ndarray
) -> Dict[str, Any]:
"""HowCuteAmI年龄预测回退方案"""
try:
if face_image is None or face_image.size == 0:
raise ValueError("无法读取人脸图像")
face_resized = cv2.resize(face_image, (224, 224))
blob = cv2.dnn.blobFromImage(
face_resized, 1.0, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False
)
# 年龄预测
self.age_net.setInput(blob)
age_preds = self.age_net.forward()
age = self.age_list[age_preds[0].argmax()]
age_confidence = float(np.max(age_preds[0]))
return {
"age": age[1:-1], # 去掉括号
"age_confidence": round(age_confidence, 4),
"emotion": "neutral", # 默认情绪
"emotion_analysis": {"neutral": 100.0}, # 默认情绪分析
}
except Exception as e:
logger.error(f"HowCuteAmI age prediction fallback failed: {e}")
return {
"age": "25-32",
"age_confidence": 0.5,
"emotion": "neutral",
"emotion_analysis": {"neutral": 100.0},
}
def _predict_with_hybrid_model(
self, face: np.ndarray, face_image: np.ndarray
) -> Dict[str, Any]:
"""混合模型预测:HowCuteAmI(颜值+性别)+ DeepFace(年龄+情绪,年龄置信度低时优先使用)"""
# 使用HowCuteAmI预测颜值和性别
beauty_gender_result = self._predict_beauty_gender_with_howcuteami(face)
# Hybrid 模式下可配置是否启用 DeepFace 情绪识别(默认启用)。
deepface_emotion_enabled = bool(getattr(config, "DEEPFACE_EMOTION_ENABLED", True))
age_emotion_result: Optional[Dict[str, Any]] = None
# 首先获取HowCuteAmI的年龄/性别预测置信度
howcuteami_age_confidence = beauty_gender_result.get("age_confidence", 0)
gender_confidence = beauty_gender_result.get("gender_confidence", 0)
if gender_confidence >= 1:
gender_confidence = 0.9999
age = beauty_gender_result["age"]
# 如果HowCuteAmI的年龄置信度低于阈值,则使用DeepFace的年龄
agec = config.AGE_CONFIDENCE
if howcuteami_age_confidence < agec:
# 需要 DeepFace 年龄时,仍调用 DeepFace(但可按开关选择是否同时跑 emotion)
age_emotion_result = self._predict_age_emotion_with_deepface(
face_image, include_emotion=deepface_emotion_enabled
)
deep_age = age_emotion_result["age"]
logger.info(
f"HowCuteAmI age confidence ({howcuteami_age_confidence}) below {agec}, value=({age}); using DeepFace for age prediction, value={deep_age}"
)
# 合并结果,使用DeepFace的年龄预测
result = {
"gender": beauty_gender_result["gender"], # 先用HowCuteAmI,后面可能回退
"gender_confidence": self._cap_conf(gender_confidence),
"beauty_score": beauty_gender_result["beauty_score"],
"beauty_raw_score": beauty_gender_result["beauty_raw_score"],
"age": deep_age,
"age_confidence": age_emotion_result["age_confidence"],
"emotion": age_emotion_result.get("emotion") or "neutral",
"emotion_analysis": age_emotion_result.get("emotion_analysis") or {"neutral": 100.0},
"model_used": "hybrid_deepface_age",
"age_model_used": "DeepFace",
"gender_model_used": "HowCuteAmI",
}
else:
# HowCuteAmI年龄置信度足够高,使用原有逻辑
logger.info(
f"HowCuteAmI age confidence ({howcuteami_age_confidence}) is high enough, value={age}; using HowCuteAmI for age prediction"
)
# 情绪识别完全可选:关闭时直接返回默认值,避免多一次 DeepFace 推理。
if deepface_emotion_enabled:
age_emotion_result = self._predict_age_emotion_with_deepface(
face_image, include_emotion=True
)
emotion = age_emotion_result.get("emotion") or "neutral"
emotion_analysis = age_emotion_result.get("emotion_analysis") or {"neutral": 100.0}
else:
emotion = "neutral"
emotion_analysis = {"neutral": 100.0}
# 合并结果,保留HowCuteAmI的年龄预测
result = {
"gender": beauty_gender_result["gender"], # 先用HowCuteAmI,后面可能回退
"gender_confidence": self._cap_conf(gender_confidence),
"beauty_score": beauty_gender_result["beauty_score"],
"beauty_raw_score": beauty_gender_result["beauty_raw_score"],
"age": beauty_gender_result["age"],
"age_confidence": beauty_gender_result["age_confidence"],
"emotion": emotion,
"emotion_analysis": emotion_analysis,
"model_used": "hybrid",
"age_model_used": "HowCuteAmI",
"gender_model_used": "HowCuteAmI",
}
# 统一性别判定规则:任一模型判为Female则Female;两者都为Male才Male
try:
how_gender = beauty_gender_result.get("gender")
how_conf = float(beauty_gender_result.get("gender_confidence", 0) or 0)
deep_gender = age_emotion_result.get("gender") if age_emotion_result else None
deep_conf = float(age_emotion_result.get("gender_confidence", 0) or 0) if age_emotion_result else 0.0
final_gender = result.get("gender")
final_conf = float(result.get("gender_confidence", 0) or 0)
# 规则判断
if (str(how_gender) == "Female") or (str(deep_gender) == "Female"):
final_gender = "Female"
final_conf = max(how_conf if how_gender == "Female" else 0,
deep_conf if deep_gender == "Female" else 0)
result["gender_model_used"] = "Combined(H+DF)"
elif (str(how_gender) == "Male") and (str(deep_gender) == "Male"):
final_gender = "Male"
final_conf = max(how_conf if how_gender == "Male" else 0,
deep_conf if deep_gender == "Male" else 0)
result["gender_model_used"] = "Combined(H+DF)"
# 否则保持原判定
result["gender"] = final_gender
result["gender_confidence"] = self._cap_conf(final_conf)
except Exception:
pass
return result
def _predict_with_howcuteami(self, face: np.ndarray) -> Dict[str, Any]:
"""使用HowCuteAmI模型进行完整预测"""
try:
# 性别预测
blob = cv2.dnn.blobFromImage(
face, 1.0, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False
)
self.gender_net.setInput(blob)
gender_preds = self.gender_net.forward()
gender = self.gender_list[gender_preds[0].argmax()]
gender_confidence = float(np.max(gender_preds[0]))
gender_confidence = self._cap_conf(gender_confidence)
# 年龄预测
self.age_net.setInput(blob)
age_preds = self.age_net.forward()
age = self.age_list[age_preds[0].argmax()]
age_confidence = float(np.max(age_preds[0]))
# 颜值预测
blob_beauty = cv2.dnn.blobFromImage(
face, 1.0 / 255, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False
)
self.beauty_net.setInput(blob_beauty)
beauty_preds = self.beauty_net.forward()
beauty_score = round(float(2.0 * np.sum(beauty_preds[0])), 1)
beauty_score = min(10.0, max(0.0, beauty_score))
beauty_score = self._adjust_beauty_score(beauty_score)
raw_score = float(np.sum(beauty_preds[0]))
return {
"gender": gender,
"gender_confidence": gender_confidence,
"age": age[1:-1], # 去掉括号
"age_confidence": round(age_confidence, 4),
"beauty_score": beauty_score,
"beauty_raw_score": round(raw_score, 4),
"model_used": "HowCuteAmI",
"emotion": "neutral", # HowCuteAmI不支持情绪分析
"emotion_analysis": {"neutral": 100.0},
"age_model_used": "HowCuteAmI",
"gender_model_used": "HowCuteAmI",
"beauty_model_used": "HowCuteAmI",
}
except Exception as e:
logger.error(f"HowCuteAmI prediction failed: {e}")
raise e
def _predict_with_deepface(self, face_image: np.ndarray) -> Dict[str, Any]:
"""使用DeepFace进行预测"""
if not DEEPFACE_AVAILABLE:
raise ValueError("DeepFace未安装")
if face_image is None or face_image.size == 0:
raise ValueError("无效的人脸图像")
try:
deepface_emotion_enabled = bool(getattr(config, "DEEPFACE_EMOTION_ENABLED", True))
actions = ["age", "gender", "emotion"] if deepface_emotion_enabled else ["age", "gender"]
# DeepFace分析 - 禁用进度条和详细输出
result = DeepFace.analyze(
img_path=face_image,
actions=actions,
enforce_detection=False,
detector_backend="skip",
silent=True # 禁用进度条输出
)
# 处理结果 (DeepFace返回的结果格式可能是list或dict)
if isinstance(result, list):
result = result[0]
# 提取信息
age = result.get("age", 25)
gender = result.get("dominant_gender", "Woman")
gender_confidence = result.get("gender", {}).get(gender, 0.5) / 100
gender_confidence = self._cap_conf(gender_confidence)
# 统一性别标签
if gender.lower() in ["woman", "female"]:
gender = "Female"
else:
gender = "Male"
# DeepFace没有内置颜值评分,这里使用简单的启发式方法
if deepface_emotion_enabled:
emotion = result.get("dominant_emotion", "neutral")
emotion_scores = result.get("emotion", {}) or {}
else:
emotion = "neutral"
emotion_scores = {"neutral": 100.0}
# 基于情绪和年龄的简单颜值估算
happiness_score = emotion_scores.get("happy", 0) / 100
neutral_score = emotion_scores.get("neutral", 0) / 100
# 简单的颜值算法 (可以改进)
base_beauty = 6.0 # 基础分
emotion_bonus = happiness_score * 2 + neutral_score * 1
age_factor = max(0.5, 1 - abs(age - 25) / 50) # 25岁为最佳年龄
beauty_score = round(min(10.0, base_beauty + emotion_bonus + age_factor), 2)
age_conf = round(random.uniform(0.7613, 0.9599), 4)
return {
"gender": gender,
"gender_confidence": gender_confidence,
"age": str(int(age)),
"age_confidence": age_conf, # DeepFace年龄置信度(随机范围)
"beauty_score": beauty_score,
"beauty_raw_score": round(beauty_score / 10, 4),
"model_used": "DeepFace",
"emotion": emotion,
"emotion_analysis": emotion_scores,
"age_model_used": "DeepFace",
"gender_model_used": "DeepFace",
"beauty_model_used": "Heuristic",
}
except Exception as e:
logger.error(f"DeepFace prediction failed: {e}")
raise e
def analyze_faces(
self,
image: np.ndarray,
original_image_hash: str,
model_type: ModelType = ModelType.HYBRID,
) -> Dict[str, Any]:
"""
分析图片中的人脸
:param image: 输入图像
:param original_image_hash: 原始图片的MD5哈希值
:param model_type: 使用的模型类型
:return: 分析结果
"""
if image is None:
raise ValueError("无效的图像输入")
# 检测人脸
face_boxes = self._detect_faces(image)
if not face_boxes:
return {
"success": False,
"message": "请尝试上传清晰、无遮挡的正面照片",
"face_count": 0,
"faces": [],
"annotated_image": None,
"model_used": model_type.value,
}
results = {
"success": True,
"message": f"成功检测到 {len(face_boxes)} 张人脸",
"face_count": len(face_boxes),
"faces": [],
"model_used": model_type.value,
}
# 复制原图用于绘制
annotated_image = image.copy()
logger.info(
f"Input annotated_image shape: {annotated_image.shape}, dtype: {annotated_image.dtype}, ndim: {annotated_image.ndim}"
)
# 分析每张人脸
for i, face_box in enumerate(face_boxes):
# 裁剪人脸
face_cropped = self._crop_face(image, face_box)
if face_cropped.size == 0:
logger.warning(f"Cropped face {i + 1} is empty, skipping.")
continue
face_resized = cv2.resize(face_cropped, (224, 224))
face_for_deepface = face_cropped.copy()
# 根据模型类型进行预测
try:
if model_type == ModelType.HYBRID:
# 混合模式:颜值性别用HowCuteAmI,年龄情绪用DeepFace
prediction_result = self._predict_with_hybrid_model(
face_resized, face_for_deepface
)
elif model_type == ModelType.HOWCUTEAMI:
prediction_result = self._predict_with_howcuteami(face_resized)
# 非混合模式也进行性别合并:引入DeepFace性别(不需要 emotion,减少耗时)
try:
age_emotion_result = self._predict_age_emotion_with_deepface(
face_for_deepface, include_emotion=False
)
how_gender = prediction_result.get("gender")
how_conf = float(prediction_result.get("gender_confidence", 0) or 0)
deep_gender = age_emotion_result.get("gender")
deep_conf = float(age_emotion_result.get("gender_confidence", 0) or 0)
final_gender = prediction_result.get("gender")
final_conf = float(prediction_result.get("gender_confidence", 0) or 0)
if (str(how_gender) == "Female") or (str(deep_gender) == "Female"):
final_gender = "Female"
final_conf = max(
how_conf if how_gender == "Female" else 0,
deep_conf if deep_gender == "Female" else 0,
)
prediction_result["gender_model_used"] = "Combined(H+DF)"
elif (str(how_gender) == "Male") and (str(deep_gender) == "Male"):
final_gender = "Male"
final_conf = max(
how_conf if how_gender == "Male" else 0,
deep_conf if deep_gender == "Male" else 0,
)
prediction_result["gender_model_used"] = "Combined(H+DF)"
prediction_result["gender"] = final_gender
prediction_result["gender_confidence"] = round(float(final_conf), 4)
except Exception:
pass
elif model_type == ModelType.DEEPFACE and DEEPFACE_AVAILABLE:
prediction_result = self._predict_with_deepface(face_for_deepface)
# 非混合模式也进行性别合并:引入HowCuteAmI性别
try:
beauty_gender_result = self._predict_beauty_gender_with_howcuteami(
face_resized
)
deep_gender = prediction_result.get("gender")
deep_conf = float(prediction_result.get("gender_confidence", 0) or 0)
how_gender = beauty_gender_result.get("gender")
how_conf = float(beauty_gender_result.get("gender_confidence", 0) or 0)
final_gender = prediction_result.get("gender")
final_conf = float(prediction_result.get("gender_confidence", 0) or 0)
if (str(how_gender) == "Female") or (str(deep_gender) == "Female"):
final_gender = "Female"
final_conf = max(
how_conf if how_gender == "Female" else 0,
deep_conf if deep_gender == "Female" else 0,
)
prediction_result["gender_model_used"] = "Combined(H+DF)"
elif (str(how_gender) == "Male") and (str(deep_gender) == "Male"):
final_gender = "Male"
final_conf = max(
how_conf if how_gender == "Male" else 0,
deep_conf if deep_gender == "Male" else 0,
)
prediction_result["gender_model_used"] = "Combined(H+DF)"
prediction_result["gender"] = final_gender
prediction_result["gender_confidence"] = round(float(final_conf), 4)
except Exception:
pass
else:
# 回退到混合模式
prediction_result = self._predict_with_hybrid_model(
face_resized, face_for_deepface
)
logger.warning(
f"Model {model_type.value} is not available, using hybrid mode"
)
except Exception as e:
logger.error(f"Prediction failed, using default values: {e}")
prediction_result = {
"gender": "Unknown",
"gender_confidence": 0.5,
"age": "25-32",
"age_confidence": 0.5,
"beauty_score": 5.0,
"beauty_raw_score": 0.5,
"emotion": "neutral",
"emotion_analysis": {"neutral": 100.0},
"model_used": "fallback",
}
# 五官分析
# facial_features = self.facial_analyzer.analyze_facial_features(
# face_cropped, face_box
# )
# 颜色设置与年龄显示统一(应用女性年龄调整)
gender = prediction_result.get("gender", "Unknown")
color_bgr = self.gender_colors.get(gender, (128, 128, 128))
color_hex = f"#{color_bgr[2]:02x}{color_bgr[1]:02x}{color_bgr[0]:02x}"
# 年龄文本与调整
raw_age_str = prediction_result.get("age", "Unknown")
display_age_str = str(raw_age_str)
age_adjusted_flag = False
age_adjustment_value = int(getattr(config, "FEMALE_AGE_ADJUSTMENT", 0) or 0)
age_adjustment_threshold = int(getattr(config, "FEMALE_AGE_ADJUSTMENT_THRESHOLD", 999) or 999)
# 仅对女性且年龄达到阈值时进行调整
try:
# 支持 "25-32" 或 "25" 格式
if "-" in str(raw_age_str):
age_num = int(str(raw_age_str).split("-")[0].strip("() "))
else:
age_num = int(str(raw_age_str).strip())
if str(gender) == "Female" and age_num >= age_adjustment_threshold and age_adjustment_value > 0:
adjusted_age = max(0, age_num - age_adjustment_value)
display_age_str = str(adjusted_age)
age_adjusted_flag = True
try:
logger.info(f"Adjusted age for female (draw+data): {age_num} -> {adjusted_age}")
except Exception:
pass
except Exception:
# 无法解析年龄时,保持原样
pass
# 保存裁剪的人脸
cropped_face_filename = f"{original_image_hash}_face_{i + 1}.webp"
cropped_face_path = os.path.join(OUTPUT_DIR, cropped_face_filename)
try:
save_image_high_quality(face_cropped, cropped_face_path)
logger.info(f"cropped face: {cropped_face_path}")
except Exception as e:
logger.error(f"Failed to save cropped face {cropped_face_path}: {e}")
cropped_face_filename = None
# 在图片上绘制标注
if config.DRAW_SCORE:
cv2.rectangle(
annotated_image,
(face_box[0], face_box[1]),
(face_box[2], face_box[3]),
color_bgr,
int(round(image.shape[0] / 400)),
8,
)
# 标签文本
beauty_score = prediction_result.get("beauty_score", 0)
label = f"{gender}, {display_age_str}, {beauty_score}"
font_scale = max(
0.3, min(0.7, image.shape[0] / 800)
) # 从500改为800,范围从0.5-1.0改为0.3-0.7
font_thickness = 2
font = cv2.FONT_HERSHEY_SIMPLEX
# 绘制文本
text_x = face_box[0]
text_y = face_box[1] - 10 if face_box[1] - 10 > 20 else face_box[1] + 30
# 计算文字大小(宽高)
(text_width, text_height), baseline = cv2.getTextSize(label, font, font_scale, font_thickness)
# 画黑色矩形背景,稍微比文字框大一点,增加边距
background_tl = (text_x, text_y - text_height - baseline) # 矩形左上角
background_br = (text_x + text_width, text_y + baseline) # 矩形右下角
if config.DRAW_SCORE:
cv2.rectangle(
annotated_image,
background_tl,
background_br,
color_bgr, # 黑色背景
thickness=-1 # 填充
)
cv2.putText(
annotated_image,
label,
(text_x, text_y),
font,
font_scale,
(255, 255, 255),
font_thickness,
cv2.LINE_AA,
)
# 构建人脸结果
face_result = {
"face_id": i + 1,
"gender": gender,
"gender_confidence": prediction_result.get("gender_confidence", 0),
"gender_model_used": prediction_result.get("gender_model_used", prediction_result.get("model_used", model_type.value)),
"age": display_age_str,
"age_confidence": prediction_result.get("age_confidence", 0),
"age_model_used": prediction_result.get("age_model_used", prediction_result.get("model_used", model_type.value)),
"beauty_score": prediction_result.get("beauty_score", 0),
"beauty_raw_score": prediction_result.get("beauty_raw_score", 0),
"emotion": prediction_result.get("emotion") or "neutral",
"emotion_analysis": prediction_result.get("emotion_analysis") or {"neutral": 100.0},
# "facial_features": facial_features, # 五官分析
"bounding_box": {
"x1": int(face_box[0]),
"y1": int(face_box[1]),
"x2": int(face_box[2]),
"y2": int(face_box[3]),
},
"color": {
"bgr": [int(color_bgr[0]), int(color_bgr[1]), int(color_bgr[2])],
"hex": color_hex,
},
"cropped_face_filename": cropped_face_filename,
"model_used": prediction_result.get("model_used", model_type.value),
}
if age_adjusted_flag:
face_result["age_adjusted"] = True
face_result["age_adjustment_value"] = int(age_adjustment_value)
results["faces"].append(face_result)
results["annotated_image"] = annotated_image
return results
def _warmup_models(self):
"""预热模型,减少首次调用延迟"""
try:
logger.info("Starting to warm up models...")
# 创建一个小的测试图像 (64x64)
test_image = np.ones((64, 64, 3), dtype=np.uint8) * 128
# 预热DeepFace模型(如果可用)
if DEEPFACE_AVAILABLE:
try:
import tempfile
deepface_emotion_enabled = bool(getattr(config, "DEEPFACE_EMOTION_ENABLED", True))
with tempfile.NamedTemporaryFile(suffix='.webp', delete=False) as tmp_file:
cv2.imwrite(tmp_file.name, test_image, [cv2.IMWRITE_WEBP_QUALITY, 95])
# 预热DeepFace - 使用最小的actions集合
DeepFace.analyze(
img_path=tmp_file.name,
actions=["age", "gender", "emotion"] if deepface_emotion_enabled else ["age", "gender"],
detector_backend="yolov8",
enforce_detection=False,
silent=True
)
os.unlink(tmp_file.name)
logger.info("DeepFace model warm-up completed")
except Exception as e:
logger.warning(f"DeepFace model warm-up failed: {e}")
# 预热OpenCV DNN模型
try:
# 预热人脸检测模型
blob = cv2.dnn.blobFromImage(test_image, 1.0, (300, 300), (104, 117, 123))
self.face_net.setInput(blob)
self.face_net.forward()
# 预热年龄预测模型
test_face = cv2.resize(test_image, (224, 224))
blob = cv2.dnn.blobFromImage(test_face, 1.0, (224, 224), self.MODEL_MEAN_VALUES, swapRB=False)
self.age_net.setInput(blob)
self.age_net.forward()
# 预热性别预测模型
self.gender_net.setInput(blob)
self.gender_net.forward()
# 预热颜值评分模型
self.beauty_net.setInput(blob)
self.beauty_net.forward()
logger.info("OpenCV DNN model warm-up completed")
except Exception as e:
logger.warning(f"OpenCV DNN model warm-up failed: {e}")
logger.info("Model warm-up completed")
except Exception as e:
logger.warning(f"Error occurred during model warm-up: {e}")