实时摄像头人脸陌生人识别(基于InsightFace)

main012_optimized.py

import os
import cv2
import time
import argparse
import warnings
import datetime
import numpy as np
from threading import Thread, Lock
import sys
import logging
import locale
from flask import Flask, jsonify, request, send_from_directory
import json
import pika
import concurrent.futures
import multiprocessing
import traceback
from sklearn.decomposition import PCA
from sklearn.preprocessing import normalize
from insightface.app import FaceAnalysis
from flask_cors import CORS

warnings.filterwarnings(‘ignore’, category=DeprecationWarning)
warnings.filterwarnings(‘ignore’, category=FutureWarning, module=‘insightface’)

强制 UTF-8 编码

os.environ[“PYTHONIOENCODING”] = “utf-8”

设置OpenCV线程相关环境变量

os.environ[“OPENCV_FFMPEG_CAPTURE_OPTIONS”] = “rtsp_transport;tcp”
os.environ[“OPENCV_FFMPEG_THREADS”] = “1”

配置日志

logging.basicConfig(
level=logging.INFO,
format=‘%(asctime)s - %(levelname)s - %(message)s’,
handlers=[
logging.FileHandler(‘face_recognition.log’, encoding=‘utf-8’),
logging.StreamHandler(sys.stdout)
]
)

Flask应用

app = Flask(name)

优化后的 CORS 配置

CORS(app,
origins=[“*”],
allow_headers=[“Content-Type”, “Authorization”, “Accept”, “X-Requested-With”],
methods=[“GET”, “POST”, “PUT”, “DELETE”, “OPTIONS”],
max_age=3600,
supports_credentials=True,
expose_headers=[“Content-Type”, “Content-Length”, “Access-Control-Allow-Origin”])

---------- 配置参数 ----------

LOCAL_MODEL_ROOT = r"C:\Users\lenovo.insightface"
MODEL_NAME = “buffalo_l”

获取系统默认编码

DEFAULT_ENCODING = locale.getpreferredencoding()

MQ配置

MQ_CONFIG = {
‘host’: ‘mq-host’,
‘port’: ‘mq-port’,
‘username’: ‘mq-username’,
‘password’: ‘mq-password’,
‘exchange’: ‘’,
‘routing_key’: ‘mq-queue’,
‘virtual_host’: ‘/’,
‘heartbeat’: 600,
‘blocked_connection_timeout’: 300
}

目录配置

DEFAULT_FACE_DIR = r"D:\faceData\face"
DEFAULT_DETECTED_DIR = r"D:\faceData\detected_faces"
UNKNOWN_FACES_DIR = r"D:\faceData\unknown_faces"

全局变量

manager_instance = None
mq_connection = None
mq_channel = None
mq_last_heartbeat = time.time()

-------------------------------------------

def imread_chinese(path):
“”“支持中文路径的图像读取函数”“”
try:
path = str(path)
# 路径遍历防护
if ‘…’ in path or path.startswith(‘/’):
logging.warning(f"检测到潜在的路径遍历攻击: {path}")
return None

    img_array = np.fromfile(path, dtype=np.uint8)
    img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
    return img
except Exception as e:
    logging.error(f"读取图像失败: {path}, 错误: {e}")
    logging.debug(f"详细错误信息: {traceback.format_exc()}")
    return None

def imwrite_chinese(path, img):
“”“强制使用 imencode + tofile 保存图像”“”
try:
path = str(path)
# 路径遍历防护
if ‘…’ in path or path.startswith(‘/’):
logging.warning(f"检测到潜在的路径遍历攻击: {path}")
return False

    ext = os.path.splitext(path)[1].lower()
    if ext == '.png':
        success, buf = cv2.imencode('.png', img)
    else:
        success, buf = cv2.imencode('.jpg', img)

    if success:
        buf.tofile(path)
        return True
    else:
        logging.warning(f"图像编码失败: {path}")
        return False
except Exception as e:
    logging.error(f"保存图像失败: {path}, 错误: {e}")
    logging.debug(f"详细错误信息: {traceback.format_exc()}")
    return False

添加静态文件路由

@app.route(‘/detected_faces/path:filename’)
def serve_detected_faces(filename):
try:
# 路径遍历防护
if ‘…’ in filename or filename.startswith(‘/’):
logging.warning(f"检测到潜在的路径遍历攻击: {filename}")
return jsonify({“error”: “无效的文件名”}), 400

    full_path = os.path.join(DEFAULT_DETECTED_DIR, filename)
    if os.path.exists(full_path):
        return send_from_directory(DEFAULT_DETECTED_DIR, filename)
    else:
        logging.warning(f"请求的文件不存在: {full_path}")
        return jsonify({"error": "文件未找到"}), 404
except Exception as e:
    logging.error(f"提供图像文件时出错: {e}")
    logging.debug(f"详细错误信息: {traceback.format_exc()}")
    return jsonify({"error": "文件访问出错"}), 500

@app.route(‘/known_faces/path:filename’)
def serve_known_faces(filename):
try:
# 路径遍历防护
if ‘…’ in filename or filename.startswith(‘/’):
logging.warning(f"检测到潜在的路径遍历攻击: {filename}")
return jsonify({“error”: “无效的文件名”}), 400

    full_path = os.path.join(DEFAULT_FACE_DIR, filename)
    if os.path.exists(full_path):
        return send_from_directory(DEFAULT_FACE_DIR, filename)
    else:
        logging.warning(f"请求的原始图像文件不存在: {full_path}")
        return jsonify({"error": "文件未找到"}), 404
except Exception as e:
    logging.error(f"提供原始图像文件时出错: {e}")
    logging.debug(f"详细错误信息: {traceback.format_exc()}")
    return jsonify({"error": "文件访问出错"}), 500

@app.route(‘/unknown_faces/path:filename’)
def serve_unknown_faces(filename):
try:
# 路径遍历防护
if ‘…’ in filename or filename.startswith(‘/’):
logging.warning(f"检测到潜在的路径遍历攻击: {filename}")
return jsonify({“error”: “无效的文件名”}), 400

    full_path = os.path.join(UNKNOWN_FACES_DIR, filename)
    if os.path.exists(full_path):
        return send_from_directory(UNKNOWN_FACES_DIR, filename)
    else:
        logging.warning(f"请求的陌生人文件不存在: {full_path}")
        return jsonify({"error": "文件未找到"}), 404
except Exception as e:
    logging.error(f"提供陌生人图像文件时出错: {e}")
    logging.debug(f"详细错误信息: {traceback.format_exc()}")
    return jsonify({"error": "文件访问出错"}), 500

class DataAugmentation:
“”“数据增强类”“”

@staticmethod
def augment_image(image):
    """对图像进行多种数据增强"""
    augmented_images = [image]  # 原始图像

    # 亮度调整
    brightness_factor = np.random.uniform(0.7, 1.3)
    bright_img = np.clip(image * brightness_factor, 0, 255).astype(np.uint8)
    augmented_images.append(bright_img)

    # 对比度调整
    contrast_factor = np.random.uniform(0.7, 1.3)
    contrast_img = np.clip(128 + contrast_factor * (image - 128), 0, 255).astype(np.uint8)
    augmented_images.append(contrast_img)

    # 水平翻转
    flipped_img = cv2.flip(image, 1)
    augmented_images.append(flipped_img)

    # 轻微旋转
    angle = np.random.uniform(-15, 15)
    center = (image.shape[1] // 2, image.shape[0] // 2)
    rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
    rotated_img = cv2.warpAffine(image, rotation_matrix, (image.shape[1], image.shape[0]))
    augmented_images.append(rotated_img)

    return augmented_images

class AngularMargin:
“”“角度间隔损失函数实现,增强数值稳定性”“”

@staticmethod
def arcface(cos_theta, label_one_hot, margin=0.5, eps=1e-7):
    """ArcFace: cos(theta + m)"""
    # 增强数值稳定性
    cos_theta = np.clip(cos_theta, -1.0 + eps, 1.0 - eps)
    theta = np.arccos(cos_theta)
    # 限制角度范围以增强稳定性
    theta = np.clip(theta, 0, np.pi)
    marginal_cos = np.cos(theta + margin)
    # 确保结果在有效范围内
    marginal_cos = np.clip(marginal_cos, -1.0, 1.0)
    return label_one_hot * marginal_cos + (1 - label_one_hot) * cos_theta

@staticmethod
def cosface(cos_theta, label_one_hot, margin=0.35, eps=1e-7):
    """CosFace: cos(theta) - m"""
    # 增强数值稳定性
    cos_theta = np.clip(cos_theta, -1.0 + eps, 1.0 - eps)
    marginal_cos = cos_theta - margin * label_one_hot
    return np.clip(marginal_cos, -1.0, 1.0)

@staticmethod
def sphereface(cos_theta, label_one_hot, margin=4.0, eps=1e-7):
    """SphereFace: cos(m * theta)"""
    # 增强数值稳定性
    cos_theta = np.clip(cos_theta, -1.0 + eps, 1.0 - eps)
    theta = np.arccos(cos_theta)
    # 限制角度范围
    theta = np.clip(theta, 0, np.pi)
    marginal_cos = np.cos(margin * theta)
    # 确保结果在有效范围内
    marginal_cos = np.clip(marginal_cos, -1.0, 1.0)
    return label_one_hot * marginal_cos + (1 - label_one_hot) * cos_theta

class EnhancedFaceRecognizer:
# 类变量,用于共享模型实例
_shared_app = None
_shared_app_lock = Lock()

def __init__(self, known_faces_dir, tolerance=None, unknown_threshold=None, base_url="",
             loss_type=None, margin=None, scale=64, use_pca=False, pca_components=128,
             min_detection_size=640, gpu_id=0):
    # 使用传入的参数,如果没有则使用硬编码的默认值
    self.tolerance = tolerance if tolerance is not None else 0.3
    self.unknown_threshold = unknown_threshold if unknown_threshold is not None else 0.4
    self.loss_type = loss_type if loss_type is not None else "cosface"
    self.margin = margin if margin is not None else 0.35

    self.prev_gray = {}
    self.motion_threshold = 0.01
    self.lock = Lock()
    self.unknown_faces = []
    self.unknown_counter = 0
    self.known_faces_dir = known_faces_dir
    self.last_load_time = 0
    self.unknown_last_seen = {}
    self.last_saved_faces = {}
    self.base_url = base_url.rstrip('/') if base_url else ""
    self.similarity_cache = {}  # 添加相似度缓存
    self.cache_ttl = 5  # 缓存5秒

    # 新增参数
    self.scale = scale
    self.use_pca = use_pca
    self.pca_components = pca_components
    self.pca = None
    self.min_detection_size = min_detection_size
    self.gpu_id = gpu_id  # GPU ID

    # 获取或创建共享的模型实例
    with EnhancedFaceRecognizer._shared_app_lock:
        if EnhancedFaceRecognizer._shared_app is None:
            model_dir = os.path.join(LOCAL_MODEL_ROOT, "models", MODEL_NAME)
            if not os.path.isdir(model_dir):
                logging.error(f"本地模型目录不存在: {model_dir}")
                sys.exit(1)

            EnhancedFaceRecognizer._shared_app = FaceAnalysis(name=MODEL_NAME, root=LOCAL_MODEL_ROOT)
            EnhancedFaceRecognizer._shared_app.prepare(ctx_id=self.gpu_id, det_size=(640, 640))
            logging.info(f"InsightFace-GPU 已加载,使用GPU ID: {self.gpu_id}")
        self.app = EnhancedFaceRecognizer._shared_app

    # 加载人脸库
    self.known_encodings, self.known_names, self.known_ids = [], [], []
    self._load_known_faces(known_faces_dir)
    logging.info(f"已加载 {len(self.known_names)} 个人脸数据")

def _normalize_features(self, features):
    """特征归一化"""
    return normalize(features.reshape(1, -1), norm='l2').flatten()

def _apply_pca(self, features):
    """应用PCA降维"""
    if self.pca is None or not self.use_pca:
        return features
    return self.pca.transform(features.reshape(1, -1))[0]

def _train_pca(self):
    """训练PCA模型,智能选择组件数量"""
    if not self.use_pca or len(self.known_encodings) < 2:
        return

    try:
        # 归一化特征
        normalized_features = np.array([
            self._normalize_features(f) for f in self.known_encodings
        ])

        # 智能选择PCA组件数量
        n_samples, n_features = normalized_features.shape

        # 根据样本数量和特征维度智能确定组件数
        if self.pca_components and self.pca_components > 0:
            # 使用指定的组件数
            n_components = min(self.pca_components, n_samples - 1, n_features)
        else:
            # 自动确定组件数,保留95%的方差
            # 先用全部组件拟合,计算方差解释比例
            temp_pca = PCA()
            temp_pca.fit(normalized_features)

            # 计算累积方差解释比例
            cumsum_ratio = np.cumsum(temp_pca.explained_variance_ratio_)

            # 找到解释95%方差所需的组件数
            n_components = np.argmax(cumsum_ratio >= 0.95) + 1
            n_components = min(n_components, self.pca_components or 128, n_samples - 1, n_features)

        # 训练PCA
        self.pca = PCA(n_components=n_components)
        self.pca.fit(normalized_features)

        # 记录PCA信息
        if hasattr(self.pca, 'explained_variance_ratio_'):
            total_variance = np.sum(self.pca.explained_variance_ratio_)
            logging.info(
                f"PCA训练完成,从{len(self.known_encodings[0])}维降至{n_components}维,保留{total_variance:.2%}方差")
        else:
            logging.info(f"PCA训练完成,从{len(self.known_encodings[0])}维降至{n_components}维")

    except Exception as e:
        logging.error(f"PCA训练失败: {e}")
        logging.debug(f"详细错误信息: {traceback.format_exc()}")
        self.pca = None

def _load_known_faces(self, directory):
    if not os.path.exists(directory):
        logging.warning(f"人脸库目录不存在,将创建目录: {directory}")
        os.makedirs(directory, exist_ok=True)
        return

    logging.info(f"正在从目录加载人脸库: {os.path.abspath(directory)}")

    self.known_encodings, self.known_names, self.known_ids = [], [], []

    try:
        files = [f for f in os.listdir(directory) if f.lower().endswith(('.jpg', '.png'))]
    except Exception as e:
        logging.error(f"无法读取人脸库目录: {e}")
        logging.debug(f"详细错误信息: {traceback.format_exc()}")
        return

    loaded_count = 0
    for fn in files:
        path = os.path.join(directory, fn)
        if not os.path.exists(path):
            continue

        try:
            # 路径遍历防护
            if
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值