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}")