import sys
import cv2
import os
import numpy as np
import uuid
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from face_11 import Ui_mainWindow # 假设UI文件生成的模块名为face_11
class FaceSignInSystem(QtWidgets.QMainWindow, Ui_mainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.current_face_id = ""
self.photo_count = 0
self.a = 0
self.b = 0
self.signed_names = set() # 用于存储已签到的姓名
self.sign_records = [] # 存储完整签到记录(含时间)
self.last_signed_time = {} # 记录每个人最后签到时间,避免重复签到
self.sign_cooldown = 5 # 签到冷却时间(秒)
# 新增:多角度识别配置
self.angle_thresholds = {
"frontal": 5000, # 正面阈值
"left": 9500, # 左侧面阈值
"right": 9500, # 右侧面阈值
"up": 8500, # 抬头阈值
"down": 8500 # 低头阈值
}
# 初始化标签显示
self.label_3.setWordWrap(True)
self.label_3.setStyleSheet("color: green; font-size: 12pt;")
self.label_4.setStyleSheet("color: red; font-size: 12pt;")
self.label_3.setText("签到信息")
self.label_4.setText("缺勤信息")
# 隐藏初始不可用的按钮
self.camera_btn.hide()
self.ovecam_btn.hide()
self.oversign_btn.hide()
self.user_edit.setObjectName("user_edit")
self.user_edit.setPlaceholderText("请输入姓名")
# 创建班级表
def name(self, face_id):
try:
with open('info.csv', 'r+') as f:
myInfo = f.readlines()
namelist = [line.split(',')[0].strip() for line in myInfo]
if face_id not in namelist:
f.writelines(f'\n{face_id},已录入')
except Exception as e:
print(f"创建班级表时出错: {e}")
# 生成图片文件(修改:添加角度信息)
def gen_face_name(self, str_face_id, angle="frontal"):
return f"{str_face_id}_{angle}_{str(uuid.uuid4())}.jpg"
# 绘制中文字符
def ChineseText(self, img, text, position, textColor=(255, 0, 0), textsize=30):
try:
font_paths = ["simsun.ttc", "simhei.ttf", "Arial.ttf"]
font = None
for font_path in font_paths:
try:
font = ImageFont.truetype(font_path, textsize, encoding='utf-8')
break
except:
continue
if font is None:
font = ImageFont.load_default()
if isinstance(img, np.ndarray):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img)
draw.text(position, text, textColor, font=font)
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
except Exception as e:
print(f"绘制文字失败: {e}")
return img
# 保存签到信息
def saveInfo(self, face_id):
try:
now = datetime.now()
if face_id in self.last_signed_time:
time_diff = (now - self.last_signed_time[face_id]).total_seconds()
if time_diff < self.sign_cooldown:
return False # 冷却中,不执行签到
if face_id not in self.signed_names:
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
self.signed_names.add(face_id)
self.last_signed_time[face_id] = now
new_record = f"{face_id} 于 {now_str} 签到"
self.sign_records.append(new_record)
display_records = self.sign_records[-20:]
self.label_3.setText("\n".join(display_records))
return True
return False
except Exception as e:
print(f"保存签到信息时出错: {e}")
self.label_3.setText("签到失败,请重试")
return False
# 生成缺勤名单
def absenteeism(self):
try:
total = set()
with open('info.csv', 'r') as f:
total = {line.split(',')[0].strip() for line in f if line.strip() and not line.startswith('name')}
absentee_list = total - self.signed_names
if absentee_list:
self.label_4.setText(f"缺勤人员:{', '.join(absentee_list)}")
with open('absenteeism.csv', 'w') as f:
for name in absentee_list:
f.write(f'{name},未签到\n')
else:
self.label_4.setText("无缺勤人员")
except Exception as e:
print(f"建立缺勤名单时出错: {e}")
self.label_4.setText("生成缺勤名单失败")
# 加载模型
def load_model(self, file_scp):
face_ids = []
face_models = []
angle_models = {} # 新增:按角度存储模型
try:
with open(file_scp, 'r', encoding='utf-8') as f:
lines = f.read().splitlines()
for line in lines:
parts = line.strip().split()
if len(parts) >= 2:
face_id = parts[0]
model_path = parts[1]
if not os.path.exists(model_path):
print(f"模型文件不存在: {model_path},尝试重新生成模型")
self.generate_face_model(face_id)
if not os.path.exists(model_path):
print(f"重新生成模型失败: {model_path}")
continue
try:
model_data = np.load(model_path)
face_ids.append(face_id)
face_models.append(model_data)
# 提取角度信息
angle = "frontal" # 默认正面
if "_left_" in model_path:
angle = "left"
elif "_right_" in model_path:
angle = "right"
elif "_up_" in model_path:
angle = "up"
elif "_down_" in model_path:
angle = "down"
if face_id not in angle_models:
angle_models[face_id] = {}
angle_models[face_id][angle] = model_data
print(f"成功加载模型: {model_path} ({angle})")
except Exception as e:
print(f"加载模型 {model_path} 失败: {e}")
return face_ids, face_models, angle_models
except Exception as e:
print(f"加载模型索引文件失败: {e}")
return [], [], {}
# 新增:检测人脸角度
def detect_face_angle(self, face_roi):
# 简化版角度检测,实际应用中可以使用更复杂的姿态估计算法
eyes = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml').detectMultiScale(face_roi)
if len(eyes) >= 2:
# 计算双眼之间的角度
eye1, eye2 = sorted(eyes, key=lambda e: e[0])[:2]
eye1_center = (eye1[0] + eye1[2] // 2, eye1[1] + eye1[3] // 2)
eye2_center = (eye2[0] + eye2[2] // 2, eye2[1] + eye2[3] // 2)
dx = eye2_center[0] - eye1_center[0]
dy = eye2_center[1] - eye1_center[1]
angle = np.degrees(np.arctan2(dy, dx))
return angle
return 0
# 人脸识别(修改:添加角度感知)
def face_recognize(self, face_roi, face_models, face_ids, angle_models=None, face_angle=0):
target_size = (100, 100)
face_roi = cv2.resize(face_roi, target_size)
face_vector = face_roi.flatten()
min_distance = float('inf')
best_match_index = -1
# 确定当前人脸角度分类
angle_category = "frontal"
if face_angle < -30:
angle_category = "left"
elif face_angle > 30:
angle_category = "right"
# 优先使用特定角度的模型
if angle_models:
for i, face_id in enumerate(face_ids):
if face_id in angle_models and angle_category in angle_models[face_id]:
model = angle_models[face_id][angle_category]
model_vectors = model.reshape(model.shape[0], -1)
distances = np.sqrt(np.sum((model_vectors - face_vector) ** 2, axis=1))
avg_distance = np.mean(distances)
# 根据角度调整阈值
threshold = self.angle_thresholds.get(angle_category, 7000)
if avg_distance < min_distance and avg_distance < threshold:
min_distance = avg_distance
best_match_index = i
# 回退到通用模型
if best_match_index == -1:
for i, model in enumerate(face_models):
model_vectors = model.reshape(model.shape[0], -1)
distances = np.sqrt(np.sum((model_vectors - face_vector) ** 2, axis=1))
avg_distance = np.mean(distances)
if avg_distance < min_distance:
min_distance = avg_distance
best_match_index = i
if best_match_index != -1 and min_distance < self.angle_thresholds.get(angle_category, 7000):
return face_ids[best_match_index], min_distance
else:
return "unknown", min_distance
# 获取最大人脸区域
def get_max_face_opencv(self, face_rects):
if len(face_rects) == 0:
return 0, None
max_area = 0
max_rect = None
for (x, y, w, h) in face_rects:
area = w * h
if area > max_area:
max_area = area
max_rect = (x, y, w, h)
return max_area, max_rect
# 生成人脸模型(修改:支持多角度模型)
def generate_face_model(self, face_id):
face_dir = os.path.join('faces', face_id)
os.makedirs(face_dir, exist_ok=True)
# 按角度分类图片
angle_images = {
"frontal": [],
"left": [],
"right": [],
"up": [],
"down": []
}
for img_name in os.listdir(face_dir):
if img_name.endswith('.jpg'):
img_path = os.path.join(face_dir, img_name)
with open(img_path, 'rb') as f:
img_data = np.frombuffer(f.read(), np.uint8)
img = cv2.imdecode(img_data, cv2.IMREAD_GRAYSCALE)
if img is not None:
img = cv2.resize(img, (100, 100))
# 确定图片角度分类
angle = "frontal"
if "_left_" in img_name:
angle = "left"
elif "_right_" in img_name:
angle = "right"
elif "_up_" in img_name:
angle = "up"
elif "_down_" in img_name:
angle = "down"
angle_images[angle].append(img)
# 为每个角度生成模型
model_paths = []
for angle, images in angle_images.items():
if images:
model_path = os.path.join(face_dir, f"{face_id}_{angle}.npy")
try:
model_data = np.array(images)
np.save(model_path, model_data)
print(f"模型已保存至:{model_path} ({angle})")
model_paths.append(model_path)
except Exception as e:
print(f"保存{angle}模型失败: {e}")
# 更新模型索引文件
try:
with open('model.scp', 'a', encoding='utf-8') as f:
for path in model_paths:
angle = "frontal"
if "_left_" in path:
angle = "left"
elif "_right_" in path:
angle = "right"
elif "_up_" in path:
angle = "up"
elif "_down_" in path:
angle = "down"
f.write(f"{face_id} {path} {angle}\n")
except Exception as e:
print(f"更新模型索引文件失败: {e}")
return model_paths
# 信息录入功能(修改:强制多角度采集)
def do_info(self):
self.info_btn.hide()
self.sign_btn.hide()
self.oversign_btn.hide()
self.camera_btn.show()
self.ovecam_btn.show()
face_id = self.user_edit.text().strip()
if not face_id:
print("请输入有效的姓名")
return
self.current_face_id = face_id
self.a = 0
# 多角度采集
required_angles = ["frontal", "left", "right", "up", "down"]
angle_descriptions = {
"frontal": "正面",
"left": "左侧面",
"right": "右侧面",
"up": "抬头",
"down": "低头"
}
try:
os.makedirs('faces', exist_ok=True)
face_dir = os.path.join('faces', face_id)
os.makedirs(face_dir, exist_ok=True)
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
if face_cascade.empty():
print("无法加载人脸检测器")
return
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("无法打开摄像头")
return
angle_index = 0
current_angle = required_angles[angle_index]
angle_count = {angle: 0 for angle in required_angles}
total_count = 0
print(f"开始采集 {face_id} 的人脸数据")
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
face_rects = face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100)
)
max_area, max_rect = self.get_max_face_opencv(face_rects)
# 显示当前采集状态
status_text = f"请面对摄像头 {angle_descriptions[current_angle]},已采集: {angle_count[current_angle]}/3"
frame = self.ChineseText(frame, status_text, (10, 30), (0, 255, 0), 24)
if max_area > 10:
x, y, w, h = max_rect
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imshow("FACE", frame)
if self.a == 1:
self.a = 0
if max_area > 10 and angle_count[current_angle] < 3:
x, y, w, h = max_rect
roi = frame[y:y + h, x:x + w]
save_path = os.path.join(face_dir, self.gen_face_name(face_id, current_angle))
cv2.imencode('.jpg', roi)[1].tofile(save_path)
angle_count[current_angle] += 1
total_count += 1
print(f"已保存{angle_descriptions[current_angle]}人脸图片: {save_path}")
# 切换到下一个角度
if angle_count[current_angle] >= 3 and angle_index < len(required_angles) - 1:
angle_index += 1
current_angle = required_angles[angle_index]
elif self.a == 3:
self.a = 0
break
if cv2.waitKey(1) == 27:
break
cap.release()
cv2.destroyAllWindows()
if total_count > 0:
print(f"成功采集 {total_count} 张 {face_id} 的人脸图片")
self.generate_face_model(face_id)
self.name(face_id)
else:
print("未采集到任何人脸图片")
except Exception as e:
print(f"发生错误: {e}")
if 'cap' in locals() and cap.isOpened():
cap.release()
cv2.destroyAllWindows()
# 开始拍照
def do_camera(self):
if not self.current_face_id:
print("请先输入姓名")
return
self.a = 1
# 结束录入
def do_ovecam(self):
self.info_btn.show()
self.sign_btn.show()
self.oversign_btn.hide()
self.camera_btn.hide()
self.ovecam_btn.hide()
self.a = 3
# 开始签到(修改:添加角度感知)
def do_sign(self):
self.info_btn.hide()
self.sign_btn.hide()
self.oversign_btn.show()
# 加载模型(获取角度模型)
face_ids, face_models, angle_models = self.load_model("model.scp")
if not face_models:
print("没有可用的人脸模型,请先录入人脸数据")
self.do_oversign()
return
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
if face_cascade.empty():
print("无法加载人脸检测器")
self.do_oversign()
return
cap = cv2.VideoCapture(0)
# 重置签到记录
self.last_signed_time = {}
while True:
success, img = cap.read()
if not success:
break
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face_rects = face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100)
)
# 只处理最大的人脸
max_area, max_rect = self.get_max_face_opencv(face_rects)
if max_area > 10000: # 设置最小人脸面积阈值
x, y, w, h = max_rect
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
face_roi = gray[y:y + h, x:x + w]
# 检测人脸角度
face_angle = self.detect_face_angle(face_roi)
# 进行人脸识别(带角度信息)
face_id, confidence = self.face_recognize(face_roi, face_models, face_ids, angle_models, face_angle)
# 根据识别结果显示不同信息
if face_id != "unknown" and confidence < self.angle_thresholds.get("frontal", 7000):
# 显示角度信息
#angle_text = f"角度: {int(face_angle)}°"
#img = self.ChineseText(img, angle_text, (x, y - 60), (0, 165, 255))
signed = self.saveInfo(face_id)
st = "签到成功" if signed else "已签到"
text = f"{face_id}{st}"
color = (0, 0, 255) if signed else (0, 165, 255)
else:
text = "未注册"
color = (0, 0, 255)
# 显示识别信息
img = self.ChineseText(img, text, (x, y - 30), color)
cv2.imshow("FACE", img)
cv2.resizeWindow("FACE",640, 480)
key = cv2.waitKey(1)
if key == 27 or key == ord('q') or self.b == 1:
self.b = 0
break
self.absenteeism()
cap.release()
cv2.destroyAllWindows()
# 结束签到
def do_oversign(self):
self.info_btn.show()
self.sign_btn.show()
self.oversign_btn.hide()
self.b = 1
self.absenteeism()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = FaceSignInSystem()
window.show()
sys.exit(app.exec_())==完整的主代码 ArcFace 进行特征提取与匹配
最新发布