目前可以成功动态更换模型运行的
This commit is contained in:
		| @ -4,6 +4,11 @@ import insightface | ||||
| from insightface.app import FaceAnalysis | ||||
| from io import BytesIO | ||||
| from PIL import Image | ||||
| import logging | ||||
|  | ||||
| # 配置日志(便于排查) | ||||
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - [FaceUtil] - %(levelname)s - %(message)s') | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| # 全局变量存储InsightFace引擎和特征列表 | ||||
| _insightface_app = None | ||||
| @ -11,135 +16,141 @@ _feature_list = [] | ||||
|  | ||||
|  | ||||
| def init_insightface(): | ||||
|     """初始化InsightFace引擎""" | ||||
|     """初始化InsightFace引擎(确保成功后再使用)""" | ||||
|     global _insightface_app | ||||
|     try: | ||||
|         print("正在初始化InsightFace引擎...") | ||||
|         app = FaceAnalysis(name='buffalo_l', root='~/.insightface') | ||||
|         app.prepare(ctx_id=0, det_size=(640, 640)) | ||||
|         print("InsightFace引擎初始化完成") | ||||
|         if _insightface_app is not None: | ||||
|             logger.info("InsightFace引擎已初始化,无需重复执行") | ||||
|             return _insightface_app | ||||
|  | ||||
|         logger.info("正在初始化InsightFace引擎(模型:buffalo_l)...") | ||||
|         # 手动指定模型下载路径(避免权限问题,可选) | ||||
|         app = FaceAnalysis( | ||||
|             name='buffalo_l', | ||||
|             root='~/.insightface',  # 模型默认下载路径 | ||||
|             providers=['CPUExecutionProvider']  # 强制用CPU(若有GPU可加'CUDAExecutionProvider') | ||||
|         ) | ||||
|         app.prepare(ctx_id=0, det_size=(640, 640))  # det_size越大,小人脸检测越准 | ||||
|         logger.info("InsightFace引擎初始化完成") | ||||
|         _insightface_app = app | ||||
|         return app | ||||
|     except Exception as e: | ||||
|         print(f"InsightFace初始化失败: {e}") | ||||
|         logger.error(f"InsightFace初始化失败:{str(e)}", exc_info=True)  # 打印详细堆栈 | ||||
|         _insightface_app = None | ||||
|         return None | ||||
|  | ||||
|  | ||||
| def add_binary_data(binary_data): | ||||
|     """ | ||||
|     接收单张图片的二进制数据、提取特征并保存 | ||||
|  | ||||
|     参数: | ||||
|         binary_data: 图片的二进制数据(bytes类型) | ||||
|  | ||||
|     返回: | ||||
|         成功提取特征时返回 (True, 特征值numpy数组) | ||||
|         失败时返回 (False, None) | ||||
|     返回:(True, 特征值numpy数组) 或 (False, 错误信息字符串) | ||||
|     """ | ||||
|     global _insightface_app, _feature_list | ||||
|  | ||||
|     # 1. 先检查引擎是否初始化成功 | ||||
|     if not _insightface_app: | ||||
|         print("引擎未初始化、无法处理") | ||||
|         return False, None | ||||
|         init_result = init_insightface()  # 尝试重新初始化 | ||||
|         if not init_result: | ||||
|             error_msg = "InsightFace引擎未初始化,无法检测人脸" | ||||
|             logger.error(error_msg) | ||||
|             return False, error_msg | ||||
|  | ||||
|     try: | ||||
|         # 直接处理二进制数据: 转换为图像格式 | ||||
|         img = Image.open(BytesIO(binary_data)) | ||||
|         frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) | ||||
|         # 2. 验证二进制数据有效性 | ||||
|         if len(binary_data) < 1024:  # 过滤过小的无效图片(小于1KB) | ||||
|             error_msg = f"图片过小({len(binary_data)}字节),可能不是有效图片" | ||||
|             logger.warning(error_msg) | ||||
|             return False, error_msg | ||||
|  | ||||
|         # 提取特征 | ||||
|         # 3. 二进制数据转CV2格式(关键步骤,避免通道错误) | ||||
|         try: | ||||
|             img = Image.open(BytesIO(binary_data)).convert("RGB")  # 强制转RGB | ||||
|             frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)  # InsightFace需要BGR格式 | ||||
|         except Exception as e: | ||||
|             error_msg = f"图片格式转换失败:{str(e)}" | ||||
|             logger.error(error_msg, exc_info=True) | ||||
|             return False, error_msg | ||||
|  | ||||
|         # 4. 检查图片尺寸(避免极端尺寸导致检测失败) | ||||
|         height, width = frame.shape[:2] | ||||
|         if height < 64 or width < 64:  # 人脸检测最小建议尺寸 | ||||
|             error_msg = f"图片尺寸过小({width}x{height}),需至少64x64像素" | ||||
|             logger.warning(error_msg) | ||||
|             return False, error_msg | ||||
|  | ||||
|         # 5. 调用InsightFace检测人脸 | ||||
|         logger.info(f"开始检测人脸(图片尺寸:{width}x{height},格式:BGR)") | ||||
|         faces = _insightface_app.get(frame) | ||||
|         if faces: | ||||
|             # 获取当前提取的特征值 | ||||
|             current_feature = faces[0].embedding | ||||
|             # 添加到特征列表 | ||||
|             _feature_list.append(current_feature) | ||||
|             print(f"已累计 {len(_feature_list)} 个特征") | ||||
|             # 返回成功标志和当前特征值 | ||||
|             return True, current_feature | ||||
|         else: | ||||
|             print("二进制数据中未检测到人脸") | ||||
|             return False, None | ||||
|  | ||||
|         if not faces: | ||||
|             error_msg = "未检测到人脸(请确保图片包含清晰正面人脸,无遮挡、不模糊)" | ||||
|             logger.warning(error_msg) | ||||
|             return False, error_msg | ||||
|  | ||||
|         # 6. 提取特征并保存 | ||||
|         current_feature = faces[0].embedding | ||||
|         _feature_list.append(current_feature) | ||||
|         logger.info(f"人脸检测成功,提取特征值(维度:{current_feature.shape[0]}),累计特征数:{len(_feature_list)}") | ||||
|         return True, current_feature | ||||
|  | ||||
|     except Exception as e: | ||||
|         print(f"处理二进制数据出错: {e}") | ||||
|         return False, None | ||||
|         error_msg = f"处理图片时发生异常:{str(e)}" | ||||
|         logger.error(error_msg, exc_info=True) | ||||
|         return False, error_msg | ||||
|  | ||||
|  | ||||
| # 以下函数保持不变(get_average_feature/clear_features/get_feature_list) | ||||
| def get_average_feature(features=None): | ||||
|     """ | ||||
|     计算多个特征向量的平均值 | ||||
|  | ||||
|     参数: | ||||
|         features: 可选、特征值列表。如果未提供、则使用全局存储的_feature_list | ||||
|                   每个元素可以是字符串格式或numpy数组 | ||||
|  | ||||
|     返回: | ||||
|         单一平均特征向量的numpy数组、若无可计算数据则返回None | ||||
|     """ | ||||
|     global _feature_list | ||||
|  | ||||
|     # 如果未提供features参数、则使用全局特征列表 | ||||
|     if features is None: | ||||
|         features = _feature_list | ||||
|  | ||||
|     try: | ||||
|         # 验证输入是否为列表且不为空 | ||||
|         if features is None: | ||||
|             features = _feature_list | ||||
|         if not isinstance(features, list) or len(features) == 0: | ||||
|             print("输入必须是包含至少一个特征值的列表") | ||||
|             logger.warning("输入必须是包含至少一个特征值的列表") | ||||
|             return None | ||||
|  | ||||
|         # 处理每个特征值 | ||||
|         processed_features = [] | ||||
|         for i, embedding in enumerate(features): | ||||
|             try: | ||||
|                 if isinstance(embedding, str): | ||||
|                     # 处理包含括号和逗号的字符串格式 | ||||
|                     embedding_str = embedding.replace('[', '').replace(']', '').replace(',', ' ').strip() | ||||
|                     embedding_list = [float(num) for num in embedding_str.split() if num.strip()] | ||||
|                     embedding_np = np.array(embedding_list, dtype=np.float32) | ||||
|                 else: | ||||
|                     embedding_np = np.array(embedding, dtype=np.float32) | ||||
|  | ||||
|                 # 验证特征值格式 | ||||
|                 if len(embedding_np.shape) == 1: | ||||
|                     processed_features.append(embedding_np) | ||||
|                     print(f"已添加第 {i + 1} 个特征值用于计算平均值") | ||||
|                     logger.info(f"已添加第 {i + 1} 个特征值用于计算平均值") | ||||
|                 else: | ||||
|                     print(f"跳过第 {i + 1} 个特征值、不是一维数组") | ||||
|  | ||||
|                     logger.warning(f"跳过第 {i + 1} 个特征值:不是一维数组") | ||||
|             except Exception as e: | ||||
|                 print(f"处理第 {i + 1} 个特征值时出错: {e}") | ||||
|                 logger.error(f"处理第 {i + 1} 个特征值时出错:{str(e)}") | ||||
|  | ||||
|         # 确保有有效的特征值 | ||||
|         if not processed_features: | ||||
|             print("没有有效的特征值用于计算平均值") | ||||
|             logger.warning("没有有效的特征值用于计算平均值") | ||||
|             return None | ||||
|  | ||||
|         # 检查所有特征向量维度是否相同 | ||||
|         dims = {feat.shape[0] for feat in processed_features} | ||||
|         if len(dims) > 1: | ||||
|             print(f"特征值维度不一致、无法计算平均值。检测到的维度: {dims}") | ||||
|             logger.error(f"特征值维度不一致:{dims},无法计算平均值") | ||||
|             return None | ||||
|  | ||||
|         # 计算平均值 | ||||
|         avg_feature = np.mean(processed_features, axis=0) | ||||
|         print(f"成功计算 {len(processed_features)} 个特征值的平均特征向量、维度: {avg_feature.shape[0]}") | ||||
|  | ||||
|         logger.info(f"计算成功:{len(processed_features)} 个特征值的平均向量(维度:{avg_feature.shape[0]})") | ||||
|         return avg_feature | ||||
|  | ||||
|     except Exception as e: | ||||
|         print(f"计算平均特征值时出错: {e}") | ||||
|         logger.error(f"计算平均特征值出错:{str(e)}", exc_info=True) | ||||
|         return None | ||||
|  | ||||
|  | ||||
| def clear_features(): | ||||
|     """清空已存储的特征数据""" | ||||
|     global _feature_list | ||||
|     _feature_list = [] | ||||
|     print("已清空所有特征数据") | ||||
|     logger.info("已清空所有特征数据") | ||||
|  | ||||
|  | ||||
| def get_feature_list(): | ||||
|     """获取当前存储的特征列表""" | ||||
|     global _feature_list | ||||
|     return _feature_list.copy()  # 返回副本防止外部直接修改 | ||||
|     logger.info(f"当前特征列表长度:{len(_feature_list)}") | ||||
|     return _feature_list.copy() | ||||
							
								
								
									
										83
									
								
								util/file_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								util/file_util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import os | ||||
| import datetime | ||||
| from pathlib import Path | ||||
| from typing import Dict | ||||
|  | ||||
|  | ||||
| def save_face_to_up_images( | ||||
|         client_ip: str, | ||||
|         face_name: str, | ||||
|         image_bytes: bytes, | ||||
|         image_format: str = "jpg" | ||||
| ) -> Dict[str, str]: | ||||
|     """ | ||||
|     保存人脸图片到 `/up_images/用户IP/人脸名字/` 路径 | ||||
|     修复路径计算错误,确保所有路径在up_images根目录下 | ||||
|  | ||||
|     参数: | ||||
|         client_ip: 客户端IP(原始格式,如192.168.1.101) | ||||
|         face_name: 人脸名称(用户输入,可为空) | ||||
|         image_bytes: 人脸图片二进制数据 | ||||
|         image_format: 图片格式(默认jpg) | ||||
|  | ||||
|     返回: | ||||
|         字典:success(是否成功)、db_path(存数据库的相对路径)、local_abs_path(本地绝对路径)、msg(提示) | ||||
|     """ | ||||
|     try: | ||||
|         # 1. 基础参数校验 | ||||
|         if not client_ip.strip(): | ||||
|             return {"success": False, "db_path": "", "local_abs_path": "", "msg": "客户端IP不能为空"} | ||||
|         if not image_bytes: | ||||
|             return {"success": False, "db_path": "", "local_abs_path": "", "msg": "图片二进制数据为空"} | ||||
|         if image_format.lower() not in ["jpg", "jpeg", "png"]: | ||||
|             return {"success": False, "db_path": "", "local_abs_path": "", "msg": "仅支持jpg/jpeg/png格式"} | ||||
|  | ||||
|         # 2. 处理特殊字符(避免路径错误) | ||||
|         safe_ip = client_ip.strip().replace(".", "_")  # IP中的.替换为_ | ||||
|         safe_face_name = face_name.strip() if (face_name and face_name.strip()) else "未命名" | ||||
|         safe_face_name = "".join([c for c in safe_face_name if c not in r'\/:*?"<>|'])  # 过滤非法字符 | ||||
|  | ||||
|         # 3. 构建根目录(强制转为绝对路径,避免相对路径混淆) | ||||
|         root_dir = Path("up_images").resolve()  # 转为绝对路径(关键修复!) | ||||
|         if not root_dir.exists(): | ||||
|             root_dir.mkdir(parents=True, exist_ok=True) | ||||
|             print(f"[FileUtil] 已创建up_images根目录:{root_dir}") | ||||
|  | ||||
|         # 4. 构建文件层级路径(确保在root_dir子目录下) | ||||
|         ip_dir = root_dir / safe_ip | ||||
|         face_name_dir = ip_dir / safe_face_name | ||||
|         face_name_dir.mkdir(parents=True, exist_ok=True)  # 自动创建目录 | ||||
|         print(f"[FileUtil] 图片存储目录:{face_name_dir}") | ||||
|  | ||||
|         # 5. 生成唯一文件名(毫秒级时间戳) | ||||
|         timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3] | ||||
|         image_filename = f"face_{safe_ip}_{timestamp}.{image_format.lower()}" | ||||
|  | ||||
|         # 6. 计算路径(关键修复:确保所有路径都是绝对路径且在root_dir下) | ||||
|         local_abs_path = face_name_dir / image_filename  # 绝对路径 | ||||
|  | ||||
|         # 验证路径是否在root_dir下(防止路径穿越攻击) | ||||
|         if not local_abs_path.resolve().is_relative_to(root_dir.resolve()): | ||||
|             raise Exception(f"图片路径不在up_images根目录下(安全校验失败):{local_abs_path}") | ||||
|  | ||||
|         # 数据库存储路径:从root_dir开始的相对路径(如 up_images/192_168_110_31/小王/xxx.jpg) | ||||
|         db_path = str(root_dir.name / local_abs_path.relative_to(root_dir)) | ||||
|  | ||||
|         # 7. 写入图片文件 | ||||
|         with open(local_abs_path, "wb") as f: | ||||
|             f.write(image_bytes) | ||||
|         print(f"[FileUtil] 图片保存成功:") | ||||
|         print(f"  数据库路径:{db_path}") | ||||
|         print(f"  本地绝对路径:{local_abs_path}") | ||||
|  | ||||
|         return { | ||||
|             "success": True, | ||||
|             "db_path": db_path,  # 存数据库的相对路径(up_images开头) | ||||
|             "local_abs_path": str(local_abs_path),  # 本地绝对路径 | ||||
|             "msg": "图片保存成功" | ||||
|         } | ||||
|  | ||||
|     except Exception as e: | ||||
|         error_msg = f"图片保存失败:{str(e)}" | ||||
|         print(f"[FileUtil] 错误:{error_msg}") | ||||
|         return {"success": False, "db_path": "", "local_abs_path": "", "msg": error_msg} | ||||
							
								
								
									
										61
									
								
								util/model_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								util/model_util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| import os | ||||
| import numpy as np | ||||
| import traceback | ||||
| from ultralytics import YOLO | ||||
| from typing import Optional | ||||
|  | ||||
|  | ||||
| def load_yolo_model(model_path: str) -> Optional[YOLO]: | ||||
|     """ | ||||
|     加载YOLO模型(支持v5/v8),并校验模型有效性 | ||||
|     :param model_path: 模型文件的绝对路径 | ||||
|     :return: 加载成功返回YOLO模型实例,失败返回None | ||||
|     """ | ||||
|     try: | ||||
|         # 加载前的基础信息检查 | ||||
|         print(f"\n[模型工具] 开始加载模型:{model_path}") | ||||
|         print(f"[模型工具] 文件是否存在:{os.path.exists(model_path)}") | ||||
|         if os.path.exists(model_path): | ||||
|             print(f"[模型工具] 文件大小:{os.path.getsize(model_path) / 1024 / 1024:.2f} MB") | ||||
|  | ||||
|         # 强制重新加载模型,避免缓存问题 | ||||
|         model = YOLO(model_path) | ||||
|  | ||||
|         # 兼容性校验:使用numpy空数组测试模型 | ||||
|         dummy_image = np.zeros((640, 640, 3), dtype=np.uint8) | ||||
|  | ||||
|         try: | ||||
|             # 优先使用新版本参数 | ||||
|             model.predict( | ||||
|                 source=dummy_image, | ||||
|                 imgsz=640, | ||||
|                 conf=0.25, | ||||
|                 verbose=False, | ||||
|                 stream=False | ||||
|             ) | ||||
|         except Exception as pred_e: | ||||
|             print(f"[模型工具] 预测校验兼容处理:{str(pred_e)}") | ||||
|             # 兼容旧版本YOLO参数 | ||||
|             model.predict( | ||||
|                 img=dummy_image, | ||||
|                 imgsz=640, | ||||
|                 conf=0.25, | ||||
|                 verbose=False | ||||
|             ) | ||||
|  | ||||
|         # 验证模型基本属性 | ||||
|         if not hasattr(model, 'names'): | ||||
|             print("[模型工具] 警告:模型缺少类别名称属性") | ||||
|         else: | ||||
|             print(f"[模型工具] 模型包含类别:{list(model.names.values())[:5]}...")  # 显示前5个类别 | ||||
|  | ||||
|         print(f"[模型工具] 模型加载成功!") | ||||
|         return model | ||||
|  | ||||
|     except Exception as e: | ||||
|         # 详细错误信息输出 | ||||
|         print(f"\n[模型工具] 加载模型失败!路径:{model_path}") | ||||
|         print(f"[模型工具] 异常类型:{type(e).__name__}") | ||||
|         print(f"[模型工具] 异常详情:{str(e)}") | ||||
|         print(f"[模型工具] 堆栈跟踪:\n{traceback.format_exc()}") | ||||
|         return None | ||||
		Reference in New Issue
	
	Block a user