Files
vision-record/person_manager.py
T

676 lines
24 KiB
Python
Raw Normal View History

"""
Person Manager - 人员识别与管理模块
功能:
- 人脸检测(MediaPipe
- 人脸识别,判断是否为同一个人
- 人员库管理,新人自动添加
- 追踪人员进出记录
"""
import cv2
import numpy as np
import json
import datetime
from pathlib import Path
from config import DATA_DIR
try:
import mediapipe as mp
HAS_MEDIAPIPE = True
except ImportError:
HAS_MEDIAPIPE = False
print("[PersonManager] MediaPipe not installed, using basic detection")
try:
import face_recognition
HAS_FACE_REC = True
except ImportError:
HAS_FACE_REC = False
print("[PersonManager] face_recognition not installed, using basic matching")
class PersonManager:
"""人员识别与管理器"""
def __init__(self):
self.persons_db_path = DATA_DIR / "persons.json"
self.faces_dir = DATA_DIR / "faces"
# 创建目录
self.faces_dir.mkdir(parents=True, exist_ok=True)
# 加载人员库
self.persons = self._load_persons_db()
# 初始化检测器状态
self.face_detector = None
self.mp_face_detection = None
self.cv_face_detector = None
self.has_mediapipe = HAS_MEDIAPIPE
# 从配置读取参数
try:
from config import config_mgr
self.config['mediapipe_min_confidence'] = config_mgr.get('min_detection_confidence', 0.3)
self.config['confirm_frames'] = config_mgr.get('confirm_frames', 3)
except:
pass
# 初始化检测器
self._init_detectors()
# 配置
self.config = {
'face_match_threshold': 0.6,
'unknown_person_id': 'unknown',
'max_persons': 100,
# YOLO 检测参数
'yolo_min_confidence': 0.3,
'confirm_frames': 3,
'leave_frames': 2,
}
# 从配置文件读取参数
try:
from config import config_mgr
self.config['yolo_min_confidence'] = config_mgr.get('yolo_min_confidence', 0.3)
self.config['face_match_threshold'] = config_mgr.get('face_match_threshold', 0.6)
self.config['confirm_frames'] = config_mgr.get('confirm_frames', 3)
self.config['leave_frames'] = config_mgr.get('leave_frames', 2)
except:
pass
# 追踪状态(连续判断)
self.tracked_persons = {}
self.prev_persons = []
self.confirmation_buffer = {}
# 统计
self.total_detections = 0
self.known_persons_detected = 0
self.new_persons_added = 0
def _load_persons_db(self):
"""加载人员数据库"""
if self.persons_db_path.exists():
try:
with open(self.persons_db_path, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
return {}
def _save_persons_db(self):
"""保存人员数据库"""
with open(self.persons_db_path, 'w', encoding='utf-8') as f:
json.dump(self.persons, f, ensure_ascii=False, indent=2)
def _init_detectors(self):
"""初始化检测器"""
# MediaPipe 人脸检测(目前不使用,由 YOLO 负责)
self.face_detector = None
self.mp_face_detection = None
# OpenCV 人脸检测(备用)
self.cv_face_detector = None
try:
model_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
if Path(model_path).exists():
self.cv_face_detector = cv2.CascadeClassifier(model_path)
print("[PersonManager] OpenCV Haar Cascade initialized (backup)")
except Exception as e:
print(f"[PersonManager] OpenCV detector init failed: {e}")
# YOLO 检测(主要检测器)
self.yolo_detector = None
try:
from ultralytics import YOLO
self.yolo_detector = YOLO('yolov8n.pt')
print("[PersonManager] YOLOv8nano initialized (primary detector)")
except ImportError:
print("[PersonManager] YOLO not installed. Install with: pip install ultralytics")
except Exception as e:
print(f"[PersonManager] YOLO init failed: {e}")
def detect_persons_yolo(self, image):
"""YOLO 人体检测(只检测是否有人)
Returns:
list: [{'bbox': [x,y,w,h], 'confidence': float}]
"""
persons = []
if self.yolo_detector is None:
return persons
min_conf = self.config.get('yolo_min_confidence', 0.3)
try:
results = self.yolo_detector(image, classes=[0], verbose=False) # class 0 = person
for r in results:
for box in r.boxes:
x1, y1, x2, y2 = box.xyxy[0].tolist()
conf = box.conf[0].item()
# 置信度过滤
if conf < min_conf:
continue
persons.append({
'bbox': [int(x1), int(y1), int(x2-x1), int(y2-y1)],
'confidence': conf,
'source': 'yolo'
})
if persons:
print(f"[PersonManager] YOLO detected {len(persons)} persons (conf > {min_conf})")
except Exception as e:
print(f"[PersonManager] YOLO detection failed: {e}")
return persons
def identify_person(self, image, person_bbox, person_index):
"""识别具体人(使用 face_recognition/MediaPipe/颜色直方图)
Args:
image: 图片
person_bbox: 人体 bbox
person_index: 人员序号
Returns:
dict: {'person_id': str, 'name': str, 'is_new': bool, 'confidence': float}
"""
x, y, w, h = person_bbox
# 从人体 bbox 中提取人脸区域(通常在上方)
face_region_y = y
face_region_h = int(h * 0.4) # 人脸约占人体高度的 40%
face_region = image[face_region_y:face_region_y+face_region_h, x:x+w]
if face_region.size == 0:
return {
'person_id': f"person_{person_index}",
'name': f"Person #{person_index}",
'is_new': True,
'confidence': 0.5,
'method': 'yolo_only'
}
# 方法1: face_recognition(最准确)
encoding = None
method_used = 'unknown'
if HAS_FACE_REC:
try:
rgb_face = cv2.cvtColor(face_region, cv2.COLOR_BGR2RGB)
encodings = face_recognition.face_encodings(rgb_face)
if len(encodings) > 0:
encoding = encodings[0]
method_used = 'face_recognition'
print(f"[PersonManager] #{person_index} Using face_recognition")
except Exception as e:
print(f"[PersonManager] #{person_index} face_recognition failed: {e}")
# 方法2: MediaPipe 人脸关键点
if encoding is None and self.has_mediapipe:
try:
import mediapipe as mp_local
mp_face_mesh = mp_local.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1,
min_detection_confidence=self.config.get('yolo_min_confidence', 0.3)
)
rgb_face = cv2.cvtColor(face_region, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb_face)
if results.multi_face_landmarks:
landmarks = results.multi_face_landmarks[0]
features = []
for landmark in landmarks.landmark:
features.extend([landmark.x, landmark.y, landmark.z])
encoding = np.array(features)
method_used = 'mediapipe'
print(f"[PersonManager] #{person_index} Using MediaPipe landmarks")
face_mesh.close()
except Exception as e:
print(f"[PersonManager] #{person_index} MediaPipe failed: {e}")
# 方法3: 颜色直方图(备用)
if encoding is None:
try:
face_resized = cv2.resize(face_region, (64, 64))
hsv = cv2.cvtColor(face_resized, cv2.COLOR_BGR2HSV)
hist_h = cv2.calcHist([hsv], [0], None, [16], [0, 180])
hist_s = cv2.calcHist([hsv], [1], None, [16], [0, 256])
hist_v = cv2.calcHist([hsv], [2], None, [16], [0, 256])
encoding = np.concatenate([
cv2.normalize(hist_h, hist_h).flatten(),
cv2.normalize(hist_s, hist_s).flatten(),
cv2.normalize(hist_v, hist_v).flatten()
])
method_used = 'color_histogram'
print(f"[PersonManager] #{person_index} Using color histogram (backup)")
except Exception as e:
print(f"[PersonManager] #{person_index} Histogram failed: {e}")
# 匹配人员库
if encoding is not None:
match_result = self.match_face(encoding)
match_result['method'] = method_used
return match_result
# 无法识别,返回默认
return {
'person_id': f"unknown_{person_index}",
'name': f"Person #{person_index}",
'is_new': True,
'confidence': 0.3,
'method': 'no_face'
}
def extract_face_encoding(self, image, face_bbox):
"""提取人脸特征(用于识别是否为同一个人)
使用 MediaPipe 的人脸关键点作为特征,不依赖 dlib
Args:
image: 图片
face_bbox: [x, y, w, h]
Returns:
numpy array: 人脸特征向量
"""
if isinstance(image, str):
image = cv2.imread(image)
if image is None:
return None
x, y, w, h = face_bbox
# 确保坐标有效
h_img, w_img = image.shape[:2]
x = max(0, min(x, w_img - 1))
y = max(0, min(y, h_img - 1))
w = max(1, min(w, w_img - x))
h = max(1, min(h, h_img - y))
# 提取人脸区域
face_image = image[y:y+h, x:x+w]
# 方法1:使用 face_recognition(如果安装了)
if HAS_FACE_REC:
try:
rgb_face = cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
encodings = face_recognition.face_encodings(rgb_face)
if len(encodings) > 0:
return encodings[0]
except:
pass
# 方法2:使用 MediaPipe 人脸关键点(推荐)
if self.has_mediapipe:
try:
import mediapipe as mp_local
mp_face_mesh = mp_local.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(static_image_mode=True, max_num_faces=1)
rgb_face = cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb_face)
if results.multi_face_landmarks:
# 提取关键点坐标作为特征
landmarks = results.multi_face_landmarks[0]
features = []
for landmark in landmarks.landmark:
features.extend([landmark.x, landmark.y, landmark.z])
face_mesh.close()
return np.array(features)
except Exception as e:
print(f"[PersonManager] MediaPipe face_mesh failed: {e}")
pass
# 方法3:使用颜色直方图(最简单,备用)
face_resized = cv2.resize(face_image, (64, 64))
hsv = cv2.cvtColor(face_resized, cv2.COLOR_BGR2HSV)
hist_h = cv2.calcHist([hsv], [0], None, [16], [0, 180])
hist_s = cv2.calcHist([hsv], [1], None, [16], [0, 256])
hist_v = cv2.calcHist([hsv], [2], None, [16], [0, 256])
feature = np.concatenate([
cv2.normalize(hist_h, hist_h).flatten(),
cv2.normalize(hist_s, hist_s).flatten(),
cv2.normalize(hist_v, hist_v).flatten()
])
return feature
def match_face(self, face_encoding, threshold=None):
"""匹配人脸,找出对应的已知人员
Args:
face_encoding: 人脸特征向量
threshold: 匹配阈值
Returns:
dict: {'person_id': str, 'name': str, 'is_new': bool}
"""
if threshold is None:
threshold = self.config.get('face_match_threshold', 0.6)
if face_encoding is None:
return {'person_id': 'unknown', 'name': 'Unknown', 'is_new': False}
best_match = None
best_distance = float('inf')
for person_id, person_data in self.persons.items():
if 'face_encoding' in person_data:
stored_encoding = np.array(person_data['face_encoding'])
if HAS_FACE_REC:
# face_recognition 距离计算
distance = face_recognition.face_distance([stored_encoding], face_encoding)[0]
else:
# 简单特征距离
distance = np.linalg.norm(stored_encoding - face_encoding)
if distance < best_distance:
best_distance = distance
best_match = person_data
if best_match and best_distance < threshold:
self.known_persons_detected += 1
return {
'person_id': best_match.get('person_id'),
'name': best_match.get('name', 'Unknown'),
'is_new': False,
'confidence': 1 - best_distance
}
# 未匹配到,是新人员
return {
'person_id': 'unknown',
'name': 'Unknown',
'is_new': True,
'confidence': 0
}
def add_new_person(self, image, face_bbox, name=None):
"""添加新人员到库
Args:
image: 图片
face_bbox: 人脸位置
name: 人员名称(可选)
Returns:
dict: 新人员信息
"""
if isinstance(image, str):
image = cv2.imread(image)
# 提取特征
face_encoding = self.extract_face_encoding(image, face_bbox)
if face_encoding is None:
return None
# 生成人员ID
person_id = f"person_{len(self.persons) + 1}"
if name is None:
name = f"Person #{len(self.persons) + 1}"
# 保存人脸图片
x, y, w, h = face_bbox
face_image = image[y:y+h, x:x+w]
face_path = self.faces_dir / f"{person_id}.jpg"
cv2.imwrite(str(face_path), face_image)
# 记录到数据库
person_data = {
'person_id': person_id,
'name': name,
'face_encoding': face_encoding.tolist() if isinstance(face_encoding, np.ndarray) else face_encoding,
'face_path': str(face_path),
'first_seen': datetime.datetime.now().isoformat(),
'last_seen': datetime.datetime.now().isoformat(),
'visit_count': 1
}
self.persons[person_id] = person_data
self._save_persons_db()
self.new_persons_added += 1
print(f"[PersonManager] New person added: {person_id} ({name})")
return person_data
def update_person_visit(self, person_id):
"""更新人员访问记录"""
if person_id in self.persons:
self.persons[person_id]['last_seen'] = datetime.datetime.now().isoformat()
self.persons[person_id]['visit_count'] += 1
self._save_persons_db()
def analyze_image(self, image_path, save_new_person=True):
"""分析图片中的人员
流程:
1. YOLO 检测人体(是否有人)
2. face_recognition/MediaPipe/颜色直方图 识别具体人
3. 连续帧判断确认
Args:
image_path: 图片路径
save_new_person: 是否保存新人员
Returns:
dict: {
'persons': list, # 识别的人员(带序号)
'confirmed_change': bool,
'person_indices': list, # 人员序号列表
}
"""
image = cv2.imread(image_path)
if image is None:
return {'persons': [], 'error': 'Cannot load image'}
self.total_detections += 1
# Step 1: YOLO 检测人体
detected_persons = self.detect_persons_yolo(image)
current_count = len(detected_persons)
# Step 2: 识别每个检测到的人
identified_persons = []
for idx, person in enumerate(detected_persons):
person_index = idx + 1 # 序号从 1 开始
# 使用 face_recognition/MediaPipe/颜色直方图 识别
identity = self.identify_person(image, person['bbox'], person_index)
identified_persons.append({
'person_id': identity['person_id'],
'name': identity['name'],
'person_index': person_index,
'bbox': person['bbox'],
'is_new': identity['is_new'],
'confidence': identity.get('confidence', person['confidence']),
'method': identity.get('method', 'unknown'),
'yolo_confidence': person['confidence'],
'source': 'yolo'
})
# Step 3: 连续帧判断
confirmed_change = False
prev_count = len(self.prev_persons)
if current_count != prev_count:
# 人数变化
key = f"count_{current_count}"
if key not in self.confirmation_buffer:
self.confirmation_buffer[key] = {'count': 0, 'persons': []}
self.confirmation_buffer[key]['count'] += 1
self.confirmation_buffer[key]['persons'] = identified_persons
# 达到确认帧数
if self.confirmation_buffer[key]['count'] >= self.config['confirm_frames']:
confirmed_change = True
print(f"[PersonManager] Confirmed change: {prev_count} -> {current_count} (after {self.config['confirm_frames']} frames)")
# 保存新人员
if save_new_person and confirmed_change:
for person in identified_persons:
if person['is_new'] and len(self.persons) < self.config['max_persons']:
# 保存人脸特征和人脸图片
x, y, w, h = person['bbox']
face_region = image[y:y+int(h*0.4), x:x+w]
if face_region.size > 0:
encoding = self.extract_face_encoding(image, person['bbox'])
if encoding is not None:
person_id = f"person_{len(self.persons) + 1}"
person['person_id'] = person_id
person['name'] = f"Person #{len(self.persons) + 1}"
# 保存到人员库(包括人脸图片)
self.add_new_person_with_encoding(person_id, encoding, person['name'], face_region)
# 清空缓冲区,更新状态
self.confirmation_buffer = {key: self.confirmation_buffer[key]}
self.prev_persons = identified_persons
else:
# 人数不变,维持状态
self.prev_persons = identified_persons
# 清空其他变化缓冲区
keys_to_remove = [k for k in self.confirmation_buffer.keys() if k != f"count_{current_count}"]
for k in keys_to_remove:
del self.confirmation_buffer[k]
# 统计
new_count = sum(1 for p in identified_persons if p['is_new'])
known_count = current_count - new_count
return {
'persons': identified_persons,
'new_count': new_count,
'known_count': known_count,
'total_count': current_count,
'confirmed_change': confirmed_change,
'current_count': current_count,
'prev_count': prev_count,
'person_indices': [p['person_index'] for p in identified_persons],
'methods_used': [p['method'] for p in identified_persons],
'detection_source': 'yolo'
}
def add_new_person_with_encoding(self, person_id, encoding, name=None, face_image=None):
"""保存新人员到库(已有 encoding)
Args:
person_id: 人员ID
encoding: 特征向量
name: 名称
face_image: 人脸图片(可选,用于保存人脸图片)
Returns:
dict: 人员信息
"""
if name is None:
name = person_id
# 保存人脸图片
face_path = ''
if face_image is not None and face_image.size > 0:
face_path = str(self.faces_dir / f"{person_id}.jpg")
cv2.imwrite(face_path, face_image)
print(f"[PersonManager] Face image saved: {face_path}")
person_data = {
'person_id': person_id,
'name': name,
'face_encoding': encoding.tolist() if isinstance(encoding, np.ndarray) else encoding,
'face_path': face_path,
'first_seen': datetime.datetime.now().isoformat(),
'last_seen': datetime.datetime.now().isoformat(),
'visit_count': 1
}
self.persons[person_id] = person_data
self._save_persons_db()
self.new_persons_added += 1
print(f"[PersonManager] New person saved: {person_id} ({name})")
return person_data
def get_persons_list(self):
"""获取人员列表"""
return [
{
'person_id': p['person_id'],
'name': p['name'],
'visit_count': p['visit_count'],
'first_seen': p['first_seen'], # 已经精确到秒
'last_seen': p['last_seen'], # 已经精确到秒
2026-04-19 16:43:25 +08:00
'face_path': p.get('face_path', ''), # 人脸图片路径
}
for p in self.persons.values()
]
def get_stats(self):
"""获取统计信息"""
return {
'total_persons': len(self.persons),
'total_detections': self.total_detections,
'known_persons_detected': self.known_persons_detected,
'new_persons_added': self.new_persons_added,
'recognition_rate': self.known_persons_detected / max(self.total_detections, 1)
}
def reset(self):
"""重置统计"""
self.total_detections = 0
self.known_persons_detected = 0
self.new_persons_added = 0
# 全局实例
person_manager = PersonManager()
if __name__ == "__main__":
# 测试
import sys
if len(sys.argv) >= 2:
test_image = sys.argv[1]
print(f"[Test] Analyzing: {test_image}")
result = person_manager.analyze_image(test_image)
print(f"[Test] Faces detected: {len(result['faces'])}")
print(f"[Test] Persons: {result['total_count']}")
print(f"[Test] New: {result['new_count']}, Known: {result['known_count']}")
for person in result['persons']:
status = "NEW" if person['is_new'] else "KNOWN"
print(f" - [{status}] {person['name']} (confidence: {person['confidence']:.2f})")
else:
print("Usage: python person_manager.py <image_path>")