目前可以成功动态更换模型运行的

This commit is contained in:
2025-09-12 14:05:09 +08:00
parent 435b2a0e6c
commit 4be7f7bf14
13 changed files with 1518 additions and 325 deletions

View File

@ -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
View 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
View 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