Python项目-基于深度学习的校园人脸识别考勤系统
引言
随着人工智能技术的快速发展,深度学习在计算机视觉领域的应用日益广泛。人脸识别作为其中的一个重要分支,已经在安防、金融、教育等多个领域展现出巨大的应用价值。本文将详细介绍如何使用Python和深度学习技术构建一个校园人脸识别考勤系统,该系统能够自动识别学生身份并记录考勤信息,大大提高了考勤效率,减轻了教师的工作负担。
系统概述
功能特点
- 实时人脸检测与识别:能够从摄像头视频流中实时检测并识别人脸
- 自动考勤记录:识别学生身份后自动记录考勤信息
- 数据可视化:提供直观的考勤统计和数据分析功能
- 管理员后台:方便教师和管理员查看和管理考勤记录
- 用户友好界面:简洁直观的用户界面,易于操作
技术栈
- 编程语言:Python 3.8+
- 深度学习框架:TensorFlow/Keras、PyTorch
- 人脸检测与识别:dlib、face_recognition、OpenCV
- Web框架:Flask/Django
- 数据库:SQLite/MySQL
- 前端技术:HTML、CSS、JavaScript、Bootstrap
系统设计
系统架构
系统采用经典的三层架构设计:
- 表示层:用户界面,包括学生签到界面和管理员后台
- 业务逻辑层:核心算法实现,包括人脸检测、特征提取和身份识别
- 数据访问层:负责数据的存储和检索,包括学生信息和考勤记录
数据流程
- 摄像头捕获实时视频流
- 人脸检测模块从视频帧中检测人脸
- 特征提取模块提取人脸特征
- 身份识别模块将提取的特征与数据库中的特征进行比对
- 考勤记录模块记录识别结果和时间信息
- 数据分析模块生成考勤统计报表
核心技术实现
1. 人脸检测
人脸检测是整个系统的第一步,我们使用HOG(Histogram of Oriented Gradients)算法或基于深度学习的方法(如MTCNN、RetinaFace)来检测图像中的人脸。
import cv2
import dlib
# 使用dlib的人脸检测器
detector = dlib.get_frontal_face_detector()
def detect_faces(image):
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 检测人脸
faces = detector(gray, 1)
# 返回人脸位置列表
face_locations = []
for face in faces:
x, y, w, h = face.left(), face.top(), face.width(), face.height()
face_locations.append((y, x + w, y + h, x))
return face_locations
2. 人脸特征提取
检测到人脸后,我们需要提取人脸的特征向量,这里使用深度学习模型(如FaceNet、ArcFace)来提取高维特征。
import face_recognition
def extract_face_features(image, face_locations):
# 提取人脸特征
face_encodings = face_recognition.face_encodings(image, face_locations)
return face_encodings
3. 人脸识别
将提取的特征与数据库中已存储的特征进行比对,找出最匹配的身份。
def recognize_faces(face_encodings, known_face_encodings, known_face_names):
recognized_names = []
for face_encoding in face_encodings:
# 比较人脸特征与已知特征的距离
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"
# 找出距离最小的匹配
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
recognized_names.append(name)
return recognized_names
4. 考勤记录
识别到学生身份后,系统会自动记录考勤信息,包括学生ID、姓名、时间等。
import datetime
import sqlite3
def record_attendance(student_id, student_name):
conn = sqlite3.connect('attendance.db')
cursor = conn.cursor()
# 获取当前时间
now = datetime.datetime.now()
date = now.strftime("%Y-%m-%d")
time = now.strftime("%H:%M:%S")
# 插入考勤记录
cursor.execute("""
INSERT INTO attendance (student_id, student_name, date, time)
VALUES (?, ?, ?, ?)
""", (student_id, student_name, date, time))
conn.commit()
conn.close()
系统集成
将上述模块集成到一个完整的系统中,下面是主程序的示例代码:
import cv2
import numpy as np
import face_recognition
import os
from datetime import datetime
import sqlite3
# 初始化数据库
def init_database():
conn = sqlite3.connect('attendance.db')
cursor = conn.cursor()
# 创建学生表
cursor.execute('''
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY,
student_id TEXT,
name TEXT,
face_encoding BLOB
)
''')
# 创建考勤记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS attendance (
id INTEGER PRIMARY KEY,
student_id TEXT,
student_name TEXT,
date TEXT,
time TEXT
)
''')
conn.commit()
conn.close()
# 加载已知学生人脸特征
def load_known_faces():
conn = sqlite3.connect('attendance.db')
cursor = conn.cursor()
cursor.execute("SELECT student_id, name, face_encoding FROM students")
rows = cursor.fetchall()
known_face_encodings = []
known_face_ids = []
known_face_names = []
for row in rows:
student_id, name, face_encoding_blob = row
face_encoding = np.frombuffer(face_encoding_blob, dtype=np.float64)
known_face_encodings.append(face_encoding)
known_face_ids.append(student_id)
known_face_names.append(name)
conn.close()
return known_face_encodings, known_face_ids, known_face_names
# 主程序
def main():
# 初始化数据库
init_database()
# 加载已知人脸
known_face_encodings, known_face_ids, known_face_names = load_known_faces()
# 打开摄像头
video_capture = cv2.VideoCapture(0)
# 记录已识别的学生,避免重复记录
recognized_students = set()
while True:
# 读取一帧视频
ret, frame = video_capture.read()
# 缩小图像以加快处理速度
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
# 将BGR转换为RGB(face_recognition使用RGB)
rgb_small_frame = small_frame[:, :, ::-1]
# 检测人脸
face_locations = face_recognition.face_locations(rgb_small_frame)
face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)
face_names = []
for face_encoding in face_encodings:
# 比较人脸
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"
student_id = "Unknown"
# 找出最匹配的人脸
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
student_id = known_face_ids[best_match_index]
# 记录考勤
if student_id not in recognized_students:
record_attendance(student_id, name)
recognized_students.add(student_id)
face_names.append(name)
# 显示结果
for (top, right, bottom, left), name in zip(face_locations, face_names):
# 放大回原始大小
top *= 4
right *= 4
bottom *= 4
left *= 4
# 绘制人脸框
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
# 绘制名字标签
cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (left + 6, bottom - 6), font, 1.0, (255, 255, 255), 1)
# 显示结果图像
cv2.imshow('Video', frame)
# 按q退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放资源
video_capture.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
Web界面实现
使用Flask框架构建Web界面,方便用户操作和查看考勤记录。
from flask import Flask, render_template, request, redirect, url_for
import sqlite3
import pandas as pd
import matplotlib.pyplot as plt
import io
import base64
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/attendance')
def attendance():
conn = sqlite3.connect('attendance.db')
# 获取考勤记录
query = """
SELECT student_id, student_name, date, time
FROM attendance
ORDER BY date DESC, time DESC
"""
df = pd.read_sql_query(query, conn)
conn.close()
return render_template('attendance.html', records=df.to_dict('records'))
@app.route('/statistics')
def statistics():
conn = sqlite3.connect('attendance.db')
# 获取考勤统计
query = """
SELECT date, COUNT(DISTINCT student_id) as count
FROM attendance
GROUP BY date
ORDER BY date
"""
df = pd.read_sql_query(query, conn)
conn.close()
# 生成统计图表
plt.figure(figsize=(10, 6))
plt.bar(df['date'], df['count'])
plt.xlabel('日期')
plt.ylabel('出勤人数')
plt.title('每日出勤统计')
plt.xticks(rotation=45)
# 将图表转换为base64编码
img = io.BytesIO()
plt.savefig(img, format='png')
img.seek(0)
plot_url = base64.b64encode(img.getvalue()).decode()
return render_template('statistics.html', plot_url=plot_url)
if __name__ == '__main__':
app.run(debug=True)
系统部署
环境配置
- 安装必要的Python库:
pip install opencv-python dlib face_recognition numpy flask pandas matplotlib
- 准备学生人脸数据库:
def register_new_student(student_id, name, image_path):
# 加载图像
image = face_recognition.load_image_file(image_path)
# 检测人脸
face_locations = face_recognition.face_locations(image)
if len(face_locations) != 1:
return False, "图像中没有检测到人脸或检测到多个人脸"
# 提取人脸特征
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# 将特征存入数据库
conn = sqlite3.connect('attendance.db')
cursor = conn.cursor()
cursor.execute("""
INSERT INTO students (student_id, name, face_encoding)
VALUES (?, ?, ?)
""", (student_id, name, face_encoding.tobytes()))
conn.commit()
conn.close()
return True, "学生注册成功"
- 启动系统:
python app.py
硬件要求
- 摄像头:支持720p或更高分辨率
- 处理器:建议Intel Core i5或更高性能
- 内存:至少8GB RAM
- 存储:至少100GB可用空间(用于存储学生数据和考勤记录)
系统优化与扩展
性能优化
- 模型压缩:使用模型量化和剪枝技术减小模型体积,提高推理速度
- GPU加速:利用GPU进行并行计算,加快人脸检测和识别过程
- 批处理:同时处理多个人脸,减少模型加载和初始化时间
功能扩展
- 活体检测:防止照片欺骗,提高系统安全性
- 表情识别:分析学生表情,评估课堂专注度
- 移动端应用:开发移动应用,支持远程考勤
- 多模态融合:结合声纹识别等多种生物特征,提高识别准确率
安全与隐私保护
在实施人脸识别系统时,必须高度重视用户隐私和数据安全:
- 数据加密:对存储的人脸特征和个人信息进行加密
- 权限控制:严格控制系统访问权限,防止未授权访问
- 数据最小化:只收集和存储必要的个人信息
- 透明度:向用户明确说明数据收集和使用方式
- 合规性:确保系统符合相关法律法规要求
结论
基于深度学习的校园人脸识别考勤系统是人工智能技术在教育领域的一个典型应用。通过整合计算机视觉、深度学习和Web开发技术,我们构建了一个高效、准确的自动考勤系统,不仅大大提高了考勤效率,还为教育管理提供了数据支持。
随着深度学习技术的不断发展,人脸识别系统的准确率和性能将进一步提升,应用场景也将更加广泛。同时,我们也需要关注系统在实际应用中可能面临的挑战,如隐私保护、环境适应性等问题,不断优化和完善系统功能。
源代码
Directory Content Summary
Source Directory: ./face_attendance_system
Directory Structure
face_attendance_system/
app.py
face_detection.py
README.md
requirements.txt
database/
db_setup.py
init_db.py
migrate.py
models.py
static/
css/
style.css
js/
main.js
uploads/
templates/
attendance.html
base.html
dashboard.html
edit_user.html
face_recognition_attendance.html
face_registration.html
face_registration_admin.html
index.html
login.html
register.html
user_management.html
webcam_registration.html
File Contents
app.py
import os
import numpy as np
import face_recognition
import cv2
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
from werkzeug.utils import secure_filename
import base64
from datetime import datetime
import json
import uuid
import shutil
# Import database models
from database.models import User, FaceEncoding, Attendance
from database.db_setup import init_database
# Initialize the Flask application
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # Change this to a random secret key in production
# Initialize database
init_database()
# Configure upload folder
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'uploads')
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size
# Allowed file extensions
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
def allowed_file(filename):
"""Check if file has allowed extension"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
"""Home page route"""
if 'user_id' in session:
return redirect(url_for('dashboard'))
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login route"""
if request.method == 'POST':
student_id = request.form.get('student_id')
password = request.form.get('password')
if not student_id or not password:
flash('Please provide both student ID and password', 'danger')
return render_template('login.html')
user = User.authenticate(student_id, password)
if user:
session['user_id'] = user['id']
session['student_id'] = user['student_id']
session['name'] = user['name']
flash(f'Welcome back, {user["name"]}!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid student ID or password', 'danger')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""User registration route"""
if request.method == 'POST':
student_id = request.form.get('student_id')
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# Validate input
if not all([student_id, name, email, password, confirm_password]):
flash('Please fill in all fields', 'danger')
return render_template('register.html')
if password != confirm_password:
flash('Passwords do not match', 'danger')
return render_template('register.html')
# Check if student ID already exists
existing_user = User.get_user_by_student_id(student_id)
if existing_user:
flash('Student ID already registered', 'danger')
return render_template('register.html')
# Create user
user_id = User.create_user(student_id, name, email, password)
if user_id:
flash('Registration successful! Please login.', 'success')
return redirect(url_for('login'))
else:
flash('Registration failed. Please try again.', 'danger')
return render_template('register.html')
@app.route('/logout')
def logout():
"""Logout route"""
session.clear()
flash('You have been logged out', 'info')
return redirect(url_for('index'))
@app.route('/dashboard')
def dashboard():
"""User dashboard route"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
user_id = session['user_id']
user = User.get_user_by_id(user_id)
# Get user's face encodings
face_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)
has_face_data = len(face_encodings) > 0
# Get user's attendance records
attendance_records = Attendance.get_attendance_by_user(user_id)
return render_template('dashboard.html',
user=user,
has_face_data=has_face_data,
attendance_records=attendance_records)
@app.route('/face-registration', methods=['GET', 'POST'])
def face_registration():
"""Face registration route"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
if request.method == 'POST':
# Check if the post request has the file part
if 'face_image' not in request.files:
flash('No file part', 'danger')
return redirect(request.url)
file = request.files['face_image']
# If user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file', 'danger')
return redirect(request.url)
if file and allowed_file(file.filename):
# Generate a unique filename
filename = secure_filename(f"{session['student_id']}_{uuid.uuid4().hex}.jpg")
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Process the image for face detection
image = face_recognition.load_image_file(filepath)
face_locations = face_recognition.face_locations(image)
if not face_locations:
os.remove(filepath) # Remove the file if no face is detected
flash('No face detected in the image. Please try again.', 'danger')
return redirect(request.url)
if len(face_locations) > 1:
os.remove(filepath) # Remove the file if multiple faces are detected
flash('Multiple faces detected in the image. Please upload an image with only your face.', 'danger')
return redirect(request.url)
# Extract face encoding
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# Save face encoding to database
encoding_id = FaceEncoding.save_face_encoding(session['user_id'], face_encoding)
if encoding_id:
flash('Face registered successfully!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Failed to register face. Please try again.', 'danger')
else:
flash('Invalid file type. Please upload a JPG, JPEG or PNG image.', 'danger')
return render_template('face_registration.html')
@app.route('/webcam-registration', methods=['GET', 'POST'])
def webcam_registration():
"""Face registration using webcam"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
if request.method == 'POST':
# Get the base64 encoded image from the request
image_data = request.form.get('image_data')
if not image_data:
return jsonify({'success': False, 'message': 'No image data received'})
# Remove the data URL prefix
image_data = image_data.split(',')[1]
# Decode the base64 image
image_bytes = base64.b64decode(image_data)
# Generate a unique filename
filename = f"{session['student_id']}_{uuid.uuid4().hex}.jpg"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# Save the image
with open(filepath, 'wb') as f:
f.write(image_bytes)
# Process the image for face detection
image = face_recognition.load_image_file(filepath)
face_locations = face_recognition.face_locations(image)
if not face_locations:
os.remove(filepath) # Remove the file if no face is detected
return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
if len(face_locations) > 1:
os.remove(filepath) # Remove the file if multiple faces are detected
return jsonify({'success': False, 'message': 'Multiple faces detected in the image. Please ensure only your face is visible.'})
# Extract face encoding
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# Save face encoding to database
encoding_id = FaceEncoding.save_face_encoding(session['user_id'], face_encoding)
if encoding_id:
return jsonify({'success': True, 'message': 'Face registered successfully!'})
else:
os.remove(filepath)
return jsonify({'success': False, 'message': 'Failed to register face. Please try again.'})
return render_template('webcam_registration.html')
@app.route('/webcam-registration-admin', methods=['POST'])
def webcam_registration_admin():
"""Process webcam registration for face data"""
if 'user_id' not in session:
return jsonify({'success': False, 'message': 'Please login first'})
# Get image data from form
image_data = request.form.get('image_data')
user_id = request.form.get('user_id')
if not image_data:
return jsonify({'success': False, 'message': 'No image data provided'})
# Check if user_id is provided (for admin registration)
if not user_id:
user_id = session['user_id']
# Get user data
user = User.get_user_by_id(user_id)
if not user:
return jsonify({'success': False, 'message': 'User not found'})
try:
# Remove header from the base64 string
image_data = image_data.split(',')[1]
# Decode base64 string to image
image_bytes = base64.b64decode(image_data)
# Create a temporary file to save the image
temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"temp_{uuid.uuid4().hex}.jpg")
with open(temp_filepath, 'wb') as f:
f.write(image_bytes)
# Process the image for face detection
image = face_recognition.load_image_file(temp_filepath)
face_locations = face_recognition.face_locations(image)
if not face_locations:
os.remove(temp_filepath)
return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
if len(face_locations) > 1:
os.remove(temp_filepath)
return jsonify({'success': False, 'message': 'Multiple faces detected in the image. Please ensure only one face is visible.'})
# Extract face encoding
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# Save face encoding to database
encoding_id = FaceEncoding.save_face_encoding(user_id, face_encoding)
if encoding_id:
# Save the processed image with a proper filename
final_filename = secure_filename(f"{user['student_id']}_{uuid.uuid4().hex}.jpg")
final_filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
shutil.copy(temp_filepath, final_filepath)
# Remove temporary file
os.remove(temp_filepath)
return jsonify({'success': True, 'message': 'Face registered successfully!'})
else:
os.remove(temp_filepath)
return jsonify({'success': False, 'message': 'Failed to register face. Please try again.'})
except Exception as e:
# Clean up if there was an error
if os.path.exists(temp_filepath):
os.remove(temp_filepath)
return jsonify({'success': False, 'message': f'An error occurred: {str(e)}'})
@app.route('/attendance', methods=['GET'])
def attendance():
"""View attendance records"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
attendance_records = Attendance.get_attendance_by_date(date)
return render_template('attendance.html',
attendance_records=attendance_records,
selected_date=date)
@app.route('/check-in', methods=['GET'])
def check_in():
"""Manual check-in page"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
return render_template('check_in.html')
@app.route('/process-check-in', methods=['POST'])
def process_check_in():
"""Process manual check-in"""
if 'user_id' not in session:
return jsonify({'success': False, 'message': 'Please login first'})
user_id = session['user_id']
# Record check-in
attendance_id = Attendance.record_check_in(user_id)
if attendance_id:
return jsonify({'success': True, 'message': 'Check-in successful!'})
else:
return jsonify({'success': False, 'message': 'You have already checked in today'})
@app.route('/check-out', methods=['POST'])
def check_out():
"""Process check-out"""
if 'user_id' not in session:
return jsonify({'success': False, 'message': 'Please login first'})
user_id = session['user_id']
# Record check-out
success = Attendance.record_check_out(user_id)
if success:
return jsonify({'success': True, 'message': 'Check-out successful!'})
else:
return jsonify({'success': False, 'message': 'No active check-in found for today'})
@app.route('/face-recognition-attendance', methods=['GET'])
def face_recognition_attendance():
"""Face recognition attendance page"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
return render_template('face_recognition_attendance.html')
@app.route('/process-face-attendance', methods=['POST'])
def process_face_attendance():
"""Process face recognition attendance"""
# Get the base64 encoded image from the request
image_data = request.form.get('image_data')
if not image_data:
return jsonify({'success': False, 'message': 'No image data received'})
# Remove the data URL prefix
image_data = image_data.split(',')[1]
# Decode the base64 image
image_bytes = base64.b64decode(image_data)
# Generate a temporary filename
temp_filename = f"temp_{uuid.uuid4().hex}.jpg"
temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
# Save the image
with open(temp_filepath, 'wb') as f:
f.write(image_bytes)
try:
# Process the image for face detection
image = face_recognition.load_image_file(temp_filepath)
face_locations = face_recognition.face_locations(image)
if not face_locations:
return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
if len(face_locations) > 1:
return jsonify({'success': False, 'message': 'Multiple faces detected. Please ensure only one person is in the frame.'})
# Extract face encoding
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# Get all face encodings from database
all_encodings = FaceEncoding.get_all_face_encodings()
if not all_encodings:
return jsonify({'success': False, 'message': 'No registered faces found in the database.'})
# Compare with known face encodings
known_encodings = [enc['encoding'] for enc in all_encodings]
matches = face_recognition.compare_faces(known_encodings, face_encoding)
if True in matches:
# Find the matching index
match_index = matches.index(True)
matched_user = all_encodings[match_index]
# Record attendance
attendance_id = Attendance.record_check_in(matched_user['user_id'])
if attendance_id:
return jsonify({
'success': True,
'message': f'Welcome, {matched_user["name"]}! Your attendance has been recorded.',
'user': {
'name': matched_user['name'],
'student_id': matched_user['student_id']
}
})
else:
return jsonify({
'success': True,
'message': f'Welcome back, {matched_user["name"]}! You have already checked in today.',
'user': {
'name': matched_user['name'],
'student_id': matched_user['student_id']
}
})
else:
return jsonify({'success': False, 'message': 'Face not recognized. Please register your face or try again.'})
finally:
# Clean up the temporary file
if os.path.exists(temp_filepath):
os.remove(temp_filepath)
@app.route('/user-management', methods=['GET'])
def user_management():
"""User management route for admins"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
# Check if user is admin (in a real app, you would check user role)
# For demo purposes, we'll allow all logged-in users to access this page
# Get search query and pagination parameters
search_query = request.args.get('search', '')
page = int(request.args.get('page', 1))
per_page = 10
# Get users based on search query
if search_query:
users = User.search_users(search_query, page, per_page)
total_users = User.count_search_results(search_query)
else:
users = User.get_all_users(page, per_page)
total_users = User.count_all_users()
# Calculate total pages
total_pages = (total_users + per_page - 1) // per_page
# Check if each user has face data
for user in users:
face_encodings = FaceEncoding.get_face_encodings_by_user_id(user['id'])
user['has_face_data'] = len(face_encodings) > 0
return render_template('user_management.html',
users=users,
search_query=search_query,
current_page=page,
total_pages=total_pages)
@app.route('/edit-user/<int:user_id>', methods=['GET', 'POST'])
def edit_user(user_id):
"""Edit user route"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
# Check if user is admin (in a real app, you would check user role)
# For demo purposes, we'll allow all logged-in users to access this page
# Get user data
user = User.get_user_by_id(user_id)
if not user:
flash('User not found', 'danger')
return redirect(url_for('user_management'))
# Check if user has face data
face_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)
user['has_face_data'] = len(face_encodings) > 0
if request.method == 'POST':
student_id = request.form.get('student_id')
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
role = request.form.get('role')
is_active = 'is_active' in request.form
# Update user
success = User.update_user(user_id, student_id, name, email, password, role, is_active)
if success:
flash('User updated successfully', 'success')
return redirect(url_for('user_management'))
else:
flash('Failed to update user', 'danger')
return render_template('edit_user.html', user=user)
@app.route('/delete-user/<int:user_id>', methods=['POST'])
def delete_user(user_id):
"""Delete user route"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
# Check if user is admin (in a real app, you would check user role)
# For demo purposes, we'll allow all logged-in users to access this page
# Delete user
success = User.delete_user(user_id)
if success:
flash('User deleted successfully', 'success')
else:
flash('Failed to delete user', 'danger')
return redirect(url_for('user_management'))
@app.route('/reset-face-data/<int:user_id>', methods=['POST'])
def reset_face_data(user_id):
"""Reset user's face data"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
# Check if user is admin (in a real app, you would check user role)
# For demo purposes, we'll allow all logged-in users to access this page
# Delete face encodings
success = FaceEncoding.delete_face_encodings_by_user_id(user_id)
if success:
flash('Face data reset successfully', 'success')
else:
flash('Failed to reset face data', 'danger')
return redirect(url_for('edit_user', user_id=user_id))
@app.route('/face-registration-admin/<int:user_id>', methods=['GET', 'POST'])
def face_registration_admin(user_id):
"""Face registration for admin to register user's face"""
if 'user_id' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
# Check if user is admin (in a real app, you would check user role)
# For demo purposes, we'll allow all logged-in users to access this page
# Get user data
user = User.get_user_by_id(user_id)
if not user:
flash('User not found', 'danger')
return redirect(url_for('user_management'))
if request.method == 'POST':
# Check if the post request has the file part
if 'face_image' not in request.files:
flash('No file part', 'danger')
return redirect(request.url)
file = request.files['face_image']
# If user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file', 'danger')
return redirect(request.url)
if file and allowed_file(file.filename):
# Generate a unique filename
filename = secure_filename(f"{user['student_id']}_{uuid.uuid4().hex}.jpg")
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Process the image for face detection
image = face_recognition.load_image_file(filepath)
face_locations = face_recognition.face_locations(image)
if not face_locations:
os.remove(filepath) # Remove the file if no face is detected
flash('No face detected in the image. Please try again.', 'danger')
return redirect(request.url)
if len(face_locations) > 1:
os.remove(filepath) # Remove the file if multiple faces are detected
flash('Multiple faces detected in the image. Please upload an image with only one face.', 'danger')
return redirect(request.url)
# Extract face encoding
face_encoding = face_recognition.face_encodings(image, face_locations)[0]
# Save face encoding to database
encoding_id = FaceEncoding.save_face_encoding(user_id, face_encoding)
if encoding_id:
flash('Face registered successfully!', 'success')
return redirect(url_for('edit_user', user_id=user_id))
else:
flash('Failed to register face. Please try again.', 'danger')
else:
flash('Invalid file type. Please upload a JPG, JPEG or PNG image.', 'danger')
return render_template('face_registration_admin.html', user=user)
@app.route('/detect-face', methods=['POST'])
def detect_face():
"""检测人脸API"""
if 'image_data' not in request.form:
return jsonify({'success': False, 'message': '未提供图像数据'})
# 获取图像数据
image_data = request.form.get('image_data')
try:
# 移除base64头部
if ',' in image_data:
image_data = image_data.split(',')[1]
# 解码base64图像
image_bytes = base64.b64decode(image_data)
nparr = np.frombuffer(image_bytes, np.uint8)
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
# 转换为RGB(OpenCV使用BGR)
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 检测人脸
face_locations = face_recognition.face_locations(rgb_image)
return jsonify({
'success': True,
'message': '人脸检测完成',
'face_count': len(face_locations)
})
except Exception as e:
app.logger.error(f"人脸检测错误: {str(e)}")
return jsonify({'success': False, 'message': f'处理图像时出错: {str(e)}'})
@app.route('/recognize-face', methods=['POST'])
def recognize_face():
"""识别人脸API"""
if 'image_data' not in request.form:
return jsonify({'success': False, 'message': '未提供图像数据'})
# 获取图像数据
image_data = request.form.get('image_data')
try:
# 移除base64头部
if ',' in image_data:
image_data = image_data.split(',')[1]
# 解码base64图像
image_bytes = base64.b64decode(image_data)
nparr = np.frombuffer(image_bytes, np.uint8)
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
# 转换为RGB(OpenCV使用BGR)
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 检测人脸
face_locations = face_recognition.face_locations(rgb_image)
if not face_locations:
return jsonify({'success': False, 'message': '未检测到人脸,请确保脸部清晰可见'})
if len(face_locations) > 1:
return jsonify({'success': False, 'message': '检测到多个人脸,请确保画面中只有一个人脸'})
# 提取人脸特征
face_encoding = face_recognition.face_encodings(rgb_image, face_locations)[0]
# 加载所有已知人脸编码
known_faces = FaceEncoding.get_all_face_encodings()
if not known_faces:
return jsonify({'success': False, 'message': '数据库中没有注册的人脸'})
# 比较人脸
known_encodings = [face['encoding'] for face in known_faces]
matches = face_recognition.compare_faces(known_encodings, face_encoding)
face_distances = face_recognition.face_distance(known_encodings, face_encoding)
if True in matches:
# 找到最佳匹配
best_match_index = np.argmin(face_distances)
confidence = 1 - face_distances[best_match_index]
if confidence >= 0.6: # 置信度阈值
matched_user = known_faces[best_match_index]
# 返回识别结果
return jsonify({
'success': True,
'message': f'成功识别为 {matched_user["name"]}',
'user': {
'user_id': matched_user['user_id'],
'student_id': matched_user['student_id'],
'name': matched_user['name']
},
'confidence': float(confidence)
})
else:
return jsonify({'success': False, 'message': '识别置信度过低,请重新尝试'})
else:
return jsonify({'success': False, 'message': '无法识别您的身份,请确保您已注册人脸数据'})
except Exception as e:
app.logger.error(f"人脸识别错误: {str(e)}")
return jsonify({'success': False, 'message': f'处理图像时出错: {str(e)}'})
@app.route('/record-attendance', methods=['POST'])
def record_attendance():
"""记录考勤API"""
if 'user_id' not in session:
return jsonify({'success': False, 'message': '请先登录'})
# 获取请求数据
data = request.get_json()
if not data or 'user_id' not in data:
return jsonify({'success': False, 'message': '无效的请求数据'})
user_id = data.get('user_id')
confidence = data.get('confidence', 0)
# 验证用户身份(确保当前登录用户只能为自己签到)
if int(session['user_id']) != int(user_id) and session.get('role') != 'admin':
return jsonify({'success': False, 'message': '无权为其他用户签到'})
# 检查是否已经签到
today_attendance = Attendance.get_today_attendance(user_id)
if today_attendance:
return jsonify({'success': False, 'message': '今天已经签到,无需重复签到'})
# 记录考勤
attendance_id = Attendance.record_check_in(user_id)
if attendance_id:
# 获取用户信息
user = User.get_user_by_id(user_id)
return jsonify({
'success': True,
'message': f'签到成功!欢迎 {user["name"]}',
'attendance_id': attendance_id,
'check_in_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
return jsonify({'success': False, 'message': '签到失败,请稍后重试'})
@app.route('/get-recent-attendance', methods=['GET'])
def get_recent_attendance():
"""获取最近考勤记录API"""
if 'user_id' not in session:
return jsonify({'success': False, 'message': '请先登录'})
# 获取最近的考勤记录(默认10条)
limit = request.args.get('limit', 10, type=int)
records = Attendance.get_recent_attendance(limit)
return jsonify({
'success': True,
'records': records
})
if __name__ == '__main__':
app.run(debug=True)
face_detection.py
import cv2
import face_recognition
import numpy as np
import os
import pickle
from datetime import datetime
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class FaceDetector:
"""人脸检测与识别类"""
def __init__(self, model_type='hog', tolerance=0.6, known_faces=None):
"""
初始化人脸检测器
参数:
model_type (str): 使用的模型类型,'hog'(CPU)或'cnn'(GPU)
tolerance (float): 人脸匹配的容差值,越小越严格
known_faces (list): 已知人脸编码和对应用户信息的列表
"""
self.model_type = model_type
self.tolerance = tolerance
self.known_faces = known_faces or []
logger.info(f"人脸检测器初始化完成,使用{model_type}模型,容差值为{tolerance}")
def load_known_faces(self, known_faces):
"""
加载已知人脸数据
参数:
known_faces (list): 包含人脸编码和用户信息的列表
"""
self.known_faces = known_faces
logger.info(f"已加载{len(known_faces)}个已知人脸")
def detect_faces(self, image):
"""
检测图像中的人脸位置
参数:
image: 图像数据,可以是文件路径或图像数组
返回:
list: 人脸位置列表,每个位置为(top, right, bottom, left)
"""
# 如果是文件路径,加载图像
if isinstance(image, str):
if not os.path.exists(image):
logger.error(f"图像文件不存在: {image}")
return []
image = face_recognition.load_image_file(image)
# 检测人脸位置
start_time = time.time()
face_locations = face_recognition.face_locations(image, model=self.model_type)
detection_time = time.time() - start_time
logger.info(f"检测到{len(face_locations)}个人脸,耗时{detection_time:.4f}秒")
return face_locations
def encode_faces(self, image, face_locations=None):
"""
提取图像中人脸的编码特征
参数:
image: 图像数据,可以是文件路径或图像数组
face_locations: 可选,人脸位置列表
返回:
list: 人脸编码特征列表
"""
# 如果是文件路径,加载图像
if isinstance(image, str):
if not os.path.exists(image):
logger.error(f"图像文件不存在: {image}")
return []
image = face_recognition.load_image_file(image)
# 如果没有提供人脸位置,先检测人脸
if face_locations is None:
face_locations = self.detect_faces(image)
if not face_locations:
logger.warning("未检测到人脸,无法提取特征")
return []
# 提取人脸编码特征
start_time = time.time()
face_encodings = face_recognition.face_encodings(image, face_locations)
encoding_time = time.time() - start_time
logger.info(f"提取了{len(face_encodings)}个人脸特征,耗时{encoding_time:.4f}秒")
return face_encodings
def recognize_faces(self, face_encodings):
"""
识别人脸,匹配已知人脸
参数:
face_encodings: 待识别的人脸编码特征列表
返回:
list: 识别结果列表,每个结果为(user_info, confidence)或(None, 0)
"""
if not self.known_faces:
logger.warning("没有已知人脸数据,无法进行识别")
return [(None, 0) for _ in face_encodings]
if not face_encodings:
logger.warning("没有提供人脸特征,无法进行识别")
return []
results = []
# 提取已知人脸的编码和用户信息
known_encodings = [face['encoding'] for face in self.known_faces]
for face_encoding in face_encodings:
# 计算与已知人脸的距离
face_distances = face_recognition.face_distance(known_encodings, face_encoding)
if len(face_distances) > 0:
# 找到最小距离及其索引
best_match_index = np.argmin(face_distances)
best_match_distance = face_distances[best_match_index]
# 计算置信度(1 - 距离)
confidence = 1 - best_match_distance
# 如果距离小于容差,认为匹配成功
if best_match_distance <= self.tolerance:
user_info = {
'user_id': self.known_faces[best_match_index]['user_id'],
'student_id': self.known_faces[best_match_index]['student_id'],
'name': self.known_faces[best_match_index]['name']
}
results.append((user_info, confidence))
logger.info(f"识别到用户: {user_info['name']},置信度: {confidence:.4f}")
else:
results.append((None, confidence))
logger.info(f"未能识别人脸,最佳匹配置信度: {confidence:.4f},低于阈值")
else:
results.append((None, 0))
logger.warning("没有已知人脸数据进行比较")
return results
def process_image(self, image):
"""
处理图像,检测、编码并识别人脸
参数:
image: 图像数据,可以是文件路径或图像数组
返回:
tuple: (face_locations, recognition_results)
"""
# 检测人脸
face_locations = self.detect_faces(image)
if not face_locations:
return [], []
# 提取人脸编码
face_encodings = self.encode_faces(image, face_locations)
# 识别人脸
recognition_results = self.recognize_faces(face_encodings)
return face_locations, recognition_results
def process_video_frame(self, frame):
"""
处理视频帧,检测、编码并识别人脸
参数:
frame: 视频帧图像数组
返回:
tuple: (face_locations, recognition_results)
"""
# 将BGR格式转换为RGB格式(OpenCV使用BGR,face_recognition使用RGB)
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 为提高性能,可以缩小图像
small_frame = cv2.resize(rgb_frame, (0, 0), fx=0.25, fy=0.25)
# 检测人脸
face_locations = self.detect_faces(small_frame)
# 调整人脸位置坐标到原始尺寸
original_face_locations = []
for top, right, bottom, left in face_locations:
original_face_locations.append(
(top * 4, right * 4, bottom * 4, left * 4)
)
if not original_face_locations:
return [], []
# 提取人脸编码(使用原始尺寸的图像)
face_encodings = self.encode_faces(rgb_frame, original_face_locations)
# 识别人脸
recognition_results = self.recognize_faces(face_encodings)
return original_face_locations, recognition_results
def draw_results(self, image, face_locations, recognition_results):
"""
在图像上绘制人脸检测和识别结果
参数:
image: 图像数组
face_locations: 人脸位置列表
recognition_results: 识别结果列表
返回:
image: 绘制结果后的图像
"""
# 复制图像,避免修改原图
result_image = image.copy()
# 遍历每个人脸
for i, (top, right, bottom, left) in enumerate(face_locations):
if i < len(recognition_results):
user_info, confidence = recognition_results[i]
# 绘制人脸框
if user_info: # 识别成功
color = (0, 255, 0) # 绿色
else: # 识别失败
color = (0, 0, 255) # 红色
cv2.rectangle(result_image, (left, top), (right, bottom), color, 2)
# 绘制文本背景
cv2.rectangle(result_image, (left, bottom - 35), (right, bottom), color, cv2.FILLED)
# 绘制文本
if user_info:
text = f"{user_info['name']} ({confidence:.2f})"
else:
text = f"Unknown ({confidence:.2f})"
cv2.putText(result_image, text, (left + 6, bottom - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
return result_image
@staticmethod
def save_face_image(image, face_location, output_path):
"""
保存人脸图像
参数:
image: 图像数组
face_location: 人脸位置 (top, right, bottom, left)
output_path: 输出文件路径
返回:
bool: 是否保存成功
"""
try:
top, right, bottom, left = face_location
# 扩大人脸区域,包含更多背景
height, width = image.shape[:2]
margin = int((bottom - top) * 0.5) # 使用人脸高度的50%作为边距
# 确保不超出图像边界
top = max(0, top - margin)
bottom = min(height, bottom + margin)
left = max(0, left - margin)
right = min(width, right + margin)
# 裁剪人脸区域
face_image = image[top:bottom, left:right]
# 保存图像
cv2.imwrite(output_path, face_image)
logger.info(f"人脸图像已保存到: {output_path}")
return True
except Exception as e:
logger.error(f"保存人脸图像失败: {e}")
return False
def test_face_detector():
"""测试人脸检测器功能"""
# 创建人脸检测器
detector = FaceDetector()
# 测试图像路径
test_image_path = "test_image.jpg"
# 检测人脸
face_locations = detector.detect_faces(test_image_path)
print(f"检测到 {len(face_locations)} 个人脸")
# 提取人脸编码
face_encodings = detector.encode_faces(test_image_path, face_locations)
print(f"提取了 {len(face_encodings)} 个人脸特征")
# 加载图像并绘制结果
image = cv2.imread(test_image_path)
result_image = detector.draw_results(image, face_locations, [(None, 0.5) for _ in face_locations])
# 显示结果
cv2.imshow("Face Detection Results", result_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
test_face_detector()
README.md
# 校园人脸识别考勤系统
基于深度学习的校园人脸识别考勤系统,使用Python、Flask、OpenCV和face_recognition库开发。
## 功能特点
- 用户管理:注册、登录、编辑和删除用户
- 人脸识别:通过摄像头或上传图片进行人脸识别
- 考勤管理:记录和查询考勤信息
- 课程管理:创建课程和管理课程考勤
- 权限控制:区分管理员和普通用户权限
## 技术栈
- **后端**:Python、Flask
- **前端**:HTML、CSS、JavaScript、Bootstrap 5
- **数据库**:SQLite
- **人脸识别**:face_recognition、OpenCV
- **其他**:NumPy、Pickle
## 安装指南
1. 克隆仓库
```bash
git clone https://github.com/yourusername/face-attendance-system.git
cd face-attendance-system
- 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
- 安装依赖
pip install -r requirements.txt
- 初始化数据库
python database/init_db.py
- 运行应用
python app.py
- 访问应用
在浏览器中访问 http://localhost:5000
系统要求
- Python 3.7+
- 摄像头(用于人脸识别)
- 现代浏览器(Chrome、Firefox、Edge等)
默认管理员账户
- 学号:admin
- 密码:admin123
项目结构
face_attendance_system/
├── app.py # 主应用入口
├── face_detection.py # 人脸检测和识别模块
├── requirements.txt # 项目依赖
├── README.md # 项目说明
├── database/ # 数据库相关
│ ├── init_db.py # 数据库初始化
│ ├── migrate.py # 数据库迁移
│ └── models.py # 数据模型
├── static/ # 静态资源
│ ├── css/ # CSS样式
│ ├── js/ # JavaScript脚本
│ └── uploads/ # 上传文件存储
│ └── faces/ # 人脸图像存储
└── templates/ # HTML模板
├── base.html # 基础模板
├── login.html # 登录页面
├── register.html # 注册页面
├── user_management.html # 用户管理页面
├── edit_user.html # 编辑用户页面
├── face_registration_admin.html # 管理员人脸注册页面
├── webcam_registration.html # 摄像头人脸注册页面
└── face_recognition_attendance.html # 人脸识别考勤页面
许可证
MIT License
### requirements.txt
```text/plain
Flask==2.0.1
Werkzeug==2.0.1
Jinja2==3.0.1
itsdangerous==2.0.1
MarkupSafe==2.0.1
numpy==1.21.0
opencv-python==4.5.3.56
face-recognition==1.3.0
face-recognition-models==0.3.0
dlib==19.22.1
Pillow==8.3.1
database\db_setup.py
import sqlite3
import os
# Database directory
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(DB_DIR, 'attendance.db')
def init_database():
"""Initialize the database with necessary tables"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
email TEXT UNIQUE,
password TEXT NOT NULL,
registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Create face_encodings table
cursor.execute('''
CREATE TABLE IF NOT EXISTS face_encodings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
encoding BLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Create attendance table
cursor.execute('''
CREATE TABLE IF NOT EXISTS attendance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
check_out_time TIMESTAMP,
date TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
conn.commit()
conn.close()
print("Database initialized successfully!")
if __name__ == "__main__":
init_database()
database\init_db.py
import sqlite3
import os
# Database path
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(DB_DIR, 'attendance.db')
def init_database():
"""Initialize database with required tables"""
print("Initializing database...")
# Connect to database
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL,
registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
role TEXT DEFAULT 'student',
is_active INTEGER DEFAULT 1
)
''')
# Create face_encodings table
cursor.execute('''
CREATE TABLE IF NOT EXISTS face_encodings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
encoding BLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Create attendance table
cursor.execute('''
CREATE TABLE IF NOT EXISTS attendance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
check_out_time TIMESTAMP,
status TEXT DEFAULT 'present',
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Create courses table
cursor.execute('''
CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_code TEXT UNIQUE NOT NULL,
course_name TEXT NOT NULL,
instructor TEXT NOT NULL,
schedule TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Create course_enrollments table
cursor.execute('''
CREATE TABLE IF NOT EXISTS course_enrollments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
UNIQUE(course_id, user_id)
)
''')
# Create course_attendance table
cursor.execute('''
CREATE TABLE IF NOT EXISTS course_attendance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
attendance_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'present',
FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Create admin user if not exists
cursor.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
if not cursor.fetchone():
import hashlib
admin_password = hashlib.sha256('admin123'.encode()).hexdigest()
cursor.execute('''
INSERT INTO users (student_id, name, email, password, role)
VALUES (?, ?, ?, ?, ?)
''', ('admin', 'System Administrator', 'admin@example.com', admin_password, 'admin'))
print("Created default admin user (student_id: admin, password: admin123)")
conn.commit()
print("Database initialized successfully.")
except Exception as e:
print(f"Error during initialization: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
init_database()
database\migrate.py
import sqlite3
import os
import sys
# Database path
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(DB_DIR, 'attendance.db')
def check_column_exists(cursor, table_name, column_name):
"""Check if a column exists in a table"""
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
for column in columns:
if column[1] == column_name:
return True
return False
def migrate_database():
"""Migrate database to latest schema"""
print("Starting database migration...")
# Connect to database
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
# Check if database exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
if not cursor.fetchone():
print("Database not initialized. Please run init_db.py first.")
conn.close()
sys.exit(1)
# Add role column to users table if it doesn't exist
if not check_column_exists(cursor, 'users', 'role'):
print("Adding 'role' column to users table...")
cursor.execute("ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'student'")
conn.commit()
print("Added 'role' column to users table.")
# Add is_active column to users table if it doesn't exist
if not check_column_exists(cursor, 'users', 'is_active'):
print("Adding 'is_active' column to users table...")
cursor.execute("ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1")
conn.commit()
print("Added 'is_active' column to users table.")
# Check if face_encodings table has the correct schema
cursor.execute("PRAGMA table_info(face_encodings)")
columns = cursor.fetchall()
encoding_column_type = None
for column in columns:
if column[1] == 'encoding':
encoding_column_type = column[2]
break
# If encoding column is not BLOB, we need to recreate the table
if encoding_column_type != 'BLOB':
print("Updating face_encodings table schema...")
# Create a backup of the face_encodings table
cursor.execute("CREATE TABLE IF NOT EXISTS face_encodings_backup AS SELECT * FROM face_encodings")
# Drop the original table
cursor.execute("DROP TABLE face_encodings")
# Create the table with the correct schema
cursor.execute('''
CREATE TABLE face_encodings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
encoding BLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# Note: We can't restore the data because the encoding format has changed
# from numpy array bytes to pickle serialized data
print("Updated face_encodings table schema. Note: Previous face encodings have been backed up but not restored.")
print("Users will need to re-register their faces.")
print("Database migration completed successfully.")
except Exception as e:
print(f"Error during migration: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
migrate_database()
database\models.py
import sqlite3
import os
import numpy as np
import hashlib
import pickle
from datetime import datetime
# Database path
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(DB_DIR, 'attendance.db')
class User:
"""User model for handling user-related database operations"""
@staticmethod
def create_user(student_id, name, email, password):
"""Create a new user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Hash the password
hashed_password = hashlib.sha256(password.encode()).hexdigest()
try:
cursor.execute('''
INSERT INTO users (student_id, name, email, password)
VALUES (?, ?, ?, ?)
''', (student_id, name, email, hashed_password))
conn.commit()
user_id = cursor.lastrowid
conn.close()
return user_id
except sqlite3.IntegrityError:
conn.close()
return None
@staticmethod
def get_user_by_id(user_id):
"""Get user by ID"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT id, student_id, name, email, registration_date, role, is_active
FROM users
WHERE id = ?
''', (user_id,))
user = cursor.fetchone()
conn.close()
if user:
return {
'id': user[0],
'student_id': user[1],
'name': user[2],
'email': user[3],
'registration_date': user[4],
'role': user[5] if len(user) > 5 else 'student',
'is_active': bool(user[6]) if len(user) > 6 else True
}
return None
@staticmethod
def get_user_by_student_id(student_id):
"""Get user by student ID"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT id, student_id, name, email, registration_date, role, is_active
FROM users
WHERE student_id = ?
''', (student_id,))
user = cursor.fetchone()
conn.close()
if user:
return {
'id': user[0],
'student_id': user[1],
'name': user[2],
'email': user[3],
'registration_date': user[4],
'role': user[5] if len(user) > 5 else 'student',
'is_active': bool(user[6]) if len(user) > 6 else True
}
return None
@staticmethod
def authenticate(student_id, password):
"""Authenticate a user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Hash the password
hashed_password = hashlib.sha256(password.encode()).hexdigest()
cursor.execute('''
SELECT id, student_id, name, email, registration_date, role, is_active
FROM users
WHERE student_id = ? AND password = ?
''', (student_id, hashed_password))
user = cursor.fetchone()
conn.close()
if user:
return {
'id': user[0],
'student_id': user[1],
'name': user[2],
'email': user[3],
'registration_date': user[4],
'role': user[5] if len(user) > 5 else 'student',
'is_active': bool(user[6]) if len(user) > 6 else True
}
return None
@staticmethod
def get_all_users(page=1, per_page=10):
"""Get all users"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
offset = (page - 1) * per_page
cursor.execute('''
SELECT id, student_id, name, email, registration_date, role, is_active
FROM users
ORDER BY id DESC
LIMIT ? OFFSET ?
''', (per_page, offset))
users = cursor.fetchall()
conn.close()
result = []
for user in users:
result.append({
'id': user[0],
'student_id': user[1],
'name': user[2],
'email': user[3],
'registration_date': user[4],
'role': user[5] if len(user) > 5 else 'student',
'is_active': bool(user[6]) if len(user) > 6 else True
})
return result
@staticmethod
def count_all_users():
"""Count all users"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT COUNT(*)
FROM users
''')
count = cursor.fetchone()[0]
conn.close()
return count
@staticmethod
def search_users(query, page=1, per_page=10):
"""Search users"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
offset = (page - 1) * per_page
search_query = f"%{query}%"
cursor.execute('''
SELECT id, student_id, name, email, registration_date, role, is_active
FROM users
WHERE student_id LIKE ? OR name LIKE ?
ORDER BY id DESC
LIMIT ? OFFSET ?
''', (search_query, search_query, per_page, offset))
users = cursor.fetchall()
conn.close()
result = []
for user in users:
result.append({
'id': user[0],
'student_id': user[1],
'name': user[2],
'email': user[3],
'registration_date': user[4],
'role': user[5] if len(user) > 5 else 'student',
'is_active': bool(user[6]) if len(user) > 6 else True
})
return result
@staticmethod
def count_search_results(query):
"""Count search results"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
search_query = f"%{query}%"
cursor.execute('''
SELECT COUNT(*)
FROM users
WHERE student_id LIKE ? OR name LIKE ?
''', (search_query, search_query))
count = cursor.fetchone()[0]
conn.close()
return count
@staticmethod
def update_user(user_id, student_id, name, email, password=None, role='student', is_active=True):
"""Update user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
if password:
hashed_password = hashlib.sha256(password.encode()).hexdigest()
cursor.execute('''
UPDATE users
SET student_id = ?, name = ?, email = ?, password = ?, role = ?, is_active = ?
WHERE id = ?
''', (student_id, name, email, hashed_password, role, is_active, user_id))
else:
cursor.execute('''
UPDATE users
SET student_id = ?, name = ?, email = ?, role = ?, is_active = ?
WHERE id = ?
''', (student_id, name, email, role, is_active, user_id))
conn.commit()
return True
except Exception as e:
print(f"Error updating user: {e}")
return False
@staticmethod
def delete_user(user_id):
"""Delete user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
# Delete user's face encodings
cursor.execute('''
DELETE FROM face_encodings
WHERE user_id = ?
''', (user_id,))
# Delete user's attendance records
cursor.execute('''
DELETE FROM attendance
WHERE user_id = ?
''', (user_id,))
# Delete user
cursor.execute('''
DELETE FROM users
WHERE id = ?
''', (user_id,))
conn.commit()
return True
except Exception as e:
print(f"Error deleting user: {e}")
return False
class FaceEncoding:
"""Face encoding model for handling face-related database operations"""
@staticmethod
def save_face_encoding(user_id, face_encoding):
"""Save a face encoding for a user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Convert numpy array to bytes for storage
encoding_bytes = pickle.dumps(face_encoding)
cursor.execute('''
INSERT INTO face_encodings (user_id, encoding)
VALUES (?, ?)
''', (user_id, encoding_bytes))
conn.commit()
encoding_id = cursor.lastrowid
conn.close()
return encoding_id
@staticmethod
def get_face_encodings_by_user_id(user_id):
"""Get face encodings for a specific user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT id, user_id, encoding
FROM face_encodings
WHERE user_id = ?
''', (user_id,))
encodings = cursor.fetchall()
conn.close()
result = []
for encoding in encodings:
# Convert bytes back to numpy array
face_encoding = pickle.loads(encoding[2])
result.append({
'id': encoding[0],
'user_id': encoding[1],
'encoding': face_encoding
})
return result
@staticmethod
def get_all_face_encodings():
"""Get all face encodings with user information"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT f.id, f.user_id, f.encoding, u.student_id, u.name
FROM face_encodings f
JOIN users u ON f.user_id = u.id
''')
encodings = cursor.fetchall()
conn.close()
result = []
for encoding in encodings:
# Convert bytes back to numpy array
face_encoding = pickle.loads(encoding[2])
result.append({
'id': encoding[0],
'user_id': encoding[1],
'encoding': face_encoding,
'student_id': encoding[3],
'name': encoding[4]
})
return result
@staticmethod
def delete_face_encodings_by_user_id(user_id):
"""Delete face encodings for a specific user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
cursor.execute('''
DELETE FROM face_encodings
WHERE user_id = ?
''', (user_id,))
conn.commit()
return True
except Exception as e:
print(f"Error deleting face encodings: {e}")
return False
class Attendance:
"""Attendance model for handling attendance-related database operations"""
@staticmethod
def record_check_in(user_id):
"""Record attendance check-in"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
today = datetime.now().strftime('%Y-%m-%d')
# Check if user already checked in today
cursor.execute('''
SELECT id FROM attendance
WHERE user_id = ? AND date = ? AND check_out_time IS NULL
''', (user_id, today))
existing = cursor.fetchone()
if existing:
conn.close()
return False
cursor.execute('''
INSERT INTO attendance (user_id, date)
VALUES (?, ?)
''', (user_id, today))
conn.commit()
attendance_id = cursor.lastrowid
conn.close()
return attendance_id
@staticmethod
def record_check_out(user_id):
"""Record attendance check-out"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
today = datetime.now().strftime('%Y-%m-%d')
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cursor.execute('''
UPDATE attendance
SET check_out_time = ?
WHERE user_id = ? AND date = ? AND check_out_time IS NULL
''', (now, user_id, today))
affected = cursor.rowcount
conn.commit()
conn.close()
return affected > 0
@staticmethod
def get_attendance_by_date(date):
"""Get attendance records for a specific date"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT a.id, a.user_id, u.student_id, u.name, a.check_in_time, a.check_out_time
FROM attendance a
JOIN users u ON a.user_id = u.id
WHERE a.date = ?
ORDER BY a.check_in_time DESC
''', (date,))
records = cursor.fetchall()
conn.close()
result = []
for record in records:
result.append({
'id': record[0],
'user_id': record[1],
'student_id': record[2],
'name': record[3],
'check_in_time': record[4],
'check_out_time': record[5]
})
return result
@staticmethod
def get_attendance_by_user(user_id):
"""Get attendance records for a specific user"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT id, date, check_in_time, check_out_time
FROM attendance
WHERE user_id = ?
ORDER BY date DESC, check_in_time DESC
''', (user_id,))
records = cursor.fetchall()
conn.close()
result = []
for record in records:
result.append({
'id': record[0],
'date': record[1],
'check_in_time': record[2],
'check_out_time': record[3]
})
return result
@staticmethod
def get_today_attendance(user_id):
"""Get user's attendance for today"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Get today's date (format: YYYY-MM-DD)
today = datetime.now().strftime('%Y-%m-%d')
cursor.execute('''
SELECT id, user_id, check_in_time, check_out_time, status
FROM attendance
WHERE user_id = ? AND date(check_in_time) = ?
''', (user_id, today))
attendance = cursor.fetchone()
conn.close()
if attendance:
return {
'id': attendance[0],
'user_id': attendance[1],
'check_in_time': attendance[2],
'check_out_time': attendance[3],
'status': attendance[4]
}
return None
@staticmethod
def get_recent_attendance(limit=10):
"""Get recent attendance records"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT a.id, a.user_id, a.check_in_time, a.status, u.student_id, u.name
FROM attendance a
JOIN users u ON a.user_id = u.id
ORDER BY a.check_in_time DESC
LIMIT ?
''', (limit,))
attendances = cursor.fetchall()
conn.close()
result = []
for attendance in attendances:
result.append({
'id': attendance[0],
'user_id': attendance[1],
'check_in_time': attendance[2],
'status': attendance[3],
'student_id': attendance[4],
'name': attendance[5]
})
return result
static\css\style.css
/* 全局样式 */
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 导航栏样式 */
.navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.navbar-brand {
font-weight: 600;
}
/* 卡片样式 */
.card {
border: none;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
font-weight: 600;
border-bottom: none;
}
/* 按钮样式 */
.btn {
border-radius: 5px;
font-weight: 500;
padding: 8px 16px;
transition: all 0.3s;
}
.btn-primary {
background-color: #4e73df;
border-color: #4e73df;
}
.btn-primary:hover {
background-color: #2e59d9;
border-color: #2e59d9;
}
.btn-success {
background-color: #1cc88a;
border-color: #1cc88a;
}
.btn-success:hover {
background-color: #17a673;
border-color: #17a673;
}
.btn-info {
background-color: #36b9cc;
border-color: #36b9cc;
}
.btn-info:hover {
background-color: #2c9faf;
border-color: #2c9faf;
}
/* 表单样式 */
.form-control {
border-radius: 5px;
padding: 10px 15px;
border: 1px solid #d1d3e2;
}
.form-control:focus {
border-color: #4e73df;
box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.25);
}
.input-group-text {
background-color: #f8f9fc;
border: 1px solid #d1d3e2;
}
/* 摄像头容器 */
#camera-container, #captured-container {
position: relative;
width: 100%;
max-width: 640px;
margin: 0 auto;
border-radius: 10px;
overflow: hidden;
}
#webcam, #captured-image {
width: 100%;
height: auto;
border-radius: 10px;
}
/* 考勤信息样式 */
#attendance-info, #recognition-result {
transition: all 0.3s ease;
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 响应式调整 */
@media (max-width: 768px) {
.card-body {
padding: 1rem;
}
.btn {
padding: 6px 12px;
}
}
/* 页脚样式 */
footer {
margin-top: 3rem;
padding: 1.5rem 0;
color: #6c757d;
border-top: 1px solid #e3e6f0;
}
static\js\main.js
// 全局工具函数
// 格式化日期时间
function formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString();
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString();
}
// 格式化时间
function formatTime(dateString) {
const date = new Date(dateString);
return date.toLocaleTimeString();
}
// 显示加载中状态
function showLoading(element, message = '加载中...') {
element.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">${message}</p>
</div>
`;
}
// 显示错误消息
function showError(element, message) {
element.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>${message}
</div>
`;
}
// 显示成功消息
function showSuccess(element, message) {
element.innerHTML = `
<div class="alert alert-success" role="alert">
<i class="fas fa-check-circle me-2"></i>${message}
</div>
`;
}
// 显示警告消息
function showWarning(element, message) {
element.innerHTML = `
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>${message}
</div>
`;
}
// 显示信息消息
function showInfo(element, message) {
element.innerHTML = `
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle me-2"></i>${message}
</div>
`;
}
// 复制文本到剪贴板
function copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 文档就绪事件
document.addEventListener('DOMContentLoaded', function() {
// 初始化工具提示
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function(tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// 初始化弹出框
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function(popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
// 处理闪现消息自动消失
const flashMessages = document.querySelectorAll('.alert-dismissible');
flashMessages.forEach(function(message) {
setTimeout(function() {
const alert = bootstrap.Alert.getInstance(message);
if (alert) {
alert.close();
} else {
message.classList.add('fade');
setTimeout(() => message.remove(), 500);
}
}, 5000);
});
// 处理表单验证
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
// 处理返回顶部按钮
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
backToTopButton.classList.add('show');
} else {
backToTopButton.classList.remove('show');
}
});
backToTopButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 处理侧边栏切换
const sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function() {
document.body.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', document.body.classList.contains('sidebar-collapsed'));
});
// 从本地存储恢复侧边栏状态
if (localStorage.getItem('sidebar-collapsed') === 'true') {
document.body.classList.add('sidebar-collapsed');
}
}
// 处理暗黑模式切换
const darkModeToggle = document.getElementById('dark-mode-toggle');
if (darkModeToggle) {
darkModeToggle.addEventListener('click', function() {
document.body.classList.toggle('dark-mode');
localStorage.setItem('dark-mode', document.body.classList.contains('dark-mode'));
});
// 从本地存储恢复暗黑模式状态
if (localStorage.getItem('dark-mode') === 'true') {
document.body.classList.add('dark-mode');
}
}
});
templates\attendance.html
{% extends 'base.html' %}
{% block title %}考勤记录 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-clipboard-list me-2"></i>考勤记录</h4>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<form method="GET" action="{{ url_for('attendance') }}" class="d-flex">
<input type="date" class="form-control me-2" name="date" value="{{ selected_date }}" required>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-1"></i>查询
</button>
</form>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-success">
<i class="fas fa-camera me-1"></i>人脸识别考勤
</a>
</div>
</div>
{% if attendance_records %}
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<th>学号</th>
<th>姓名</th>
<th>签到时间</th>
<th>签退时间</th>
<th>状态</th>
<th>时长</th>
</tr>
</thead>
<tbody>
{% for record in attendance_records %}
<tr>
<td>{{ record.student_id }}</td>
<td>{{ record.name }}</td>
<td>{{ record.check_in_time }}</td>
<td>{{ record.check_out_time if record.check_out_time else '未签退' }}</td>
<td>
{% if record.check_out_time %}
<span class="badge bg-success">已完成</span>
{% else %}
<span class="badge bg-warning">进行中</span>
{% endif %}
</td>
<td>
{% if record.check_out_time %}
{% set check_in = record.check_in_time.split(' ')[1] %}
{% set check_out = record.check_out_time.split(' ')[1] %}
{% set hours = (check_out.split(':')[0]|int - check_in.split(':')[0]|int) %}
{% set minutes = (check_out.split(':')[1]|int - check_in.split(':')[1]|int) %}
{% if minutes < 0 %}
{% set hours = hours - 1 %}
{% set minutes = minutes + 60 %}
{% endif %}
{{ hours }}小时{{ minutes }}分钟
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">考勤统计</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-4">
<div class="border-end">
<h3 class="text-primary">{{ attendance_records|length }}</h3>
<p class="text-muted">总人数</p>
</div>
</div>
<div class="col-4">
<div class="border-end">
<h3 class="text-success">
{% set completed = 0 %}
{% for record in attendance_records %}
{% if record.check_out_time %}
{% set completed = completed + 1 %}
{% endif %}
{% endfor %}
{{ completed }}
</h3>
<p class="text-muted">已完成</p>
</div>
</div>
<div class="col-4">
<h3 class="text-warning">
{% set in_progress = 0 %}
{% for record in attendance_records %}
{% if not record.check_out_time %}
{% set in_progress = in_progress + 1 %}
{% endif %}
{% endfor %}
{{ in_progress }}
</h3>
<p class="text-muted">进行中</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">图表统计</h5>
</div>
<div class="card-body">
<canvas id="attendanceChart" width="100%" height="200"></canvas>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>{{ selected_date }} 没有考勤记录
</div>
{% endif %}
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<button class="btn btn-outline-primary" onclick="window.print()">
<i class="fas fa-print me-1"></i>打印记录
</button>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
<a href="#" class="btn btn-outline-success" id="exportBtn">
<i class="fas fa-file-excel me-1"></i>导出Excel
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 考勤统计图表
{% if attendance_records %}
const ctx = document.getElementById('attendanceChart').getContext('2d');
// 计算已完成和进行中的数量
let completed = 0;
let inProgress = 0;
{% for record in attendance_records %}
{% if record.check_out_time %}
completed++;
{% else %}
inProgress++;
{% endif %}
{% endfor %}
const attendanceChart = new Chart(ctx, {
type: 'pie',
data: {
labels: ['已完成', '进行中'],
datasets: [{
data: [completed, inProgress],
backgroundColor: [
'rgba(40, 167, 69, 0.7)',
'rgba(255, 193, 7, 0.7)'
],
borderColor: [
'rgba(40, 167, 69, 1)',
'rgba(255, 193, 7, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
{% endif %}
// 导出Excel功能
document.getElementById('exportBtn').addEventListener('click', function(e) {
e.preventDefault();
alert('导出功能将在完整版中提供');
});
</script>
{% endblock %}
templates\base.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}校园人脸识别考勤系统{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="fas fa-user-check me-2"></i>校园人脸识别考勤系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">首页</a>
</li>
{% if session.get('user_id') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">控制面板</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('face_recognition_attendance') }}">人脸识别考勤</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('attendance') }}">考勤记录</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user-circle me-1"></i>{{ session.get('name') }}
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="{{ url_for('dashboard') }}">个人信息</a></li>
<li><a class="dropdown-item" href="{{ url_for('face_registration') }}">人脸注册</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">退出登录</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">注册</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash Messages -->
<div class="container mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Main Content -->
<main class="container my-4">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="mb-0">© {{ now.year }} 校园人脸识别考勤系统 | 基于深度学习的智能考勤解决方案</p>
</div>
</footer>
<!-- Bootstrap JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Custom JS -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
templates\dashboard.html
{% extends 'base.html' %}
{% block title %}控制面板 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-user me-2"></i>个人信息</h5>
</div>
<div class="card-body">
<div class="text-center mb-3">
{% if has_face_data %}
<div class="avatar-container mb-3">
<i class="fas fa-user-circle fa-6x text-primary"></i>
<span class="badge bg-success position-absolute bottom-0 end-0">
<i class="fas fa-check"></i>
</span>
</div>
<p class="text-success"><i class="fas fa-check-circle me-1"></i>人脸数据已注册</p>
{% else %}
<div class="avatar-container mb-3">
<i class="fas fa-user-circle fa-6x text-secondary"></i>
<span class="badge bg-warning position-absolute bottom-0 end-0">
<i class="fas fa-exclamation"></i>
</span>
</div>
<p class="text-warning"><i class="fas fa-exclamation-circle me-1"></i>尚未注册人脸数据</p>
<a href="{{ url_for('face_registration') }}" class="btn btn-primary btn-sm">
<i class="fas fa-camera me-1"></i>立即注册
</a>
{% endif %}
</div>
<table class="table">
<tbody>
<tr>
<th scope="row"><i class="fas fa-id-card me-2"></i>学号</th>
<td>{{ user.student_id }}</td>
</tr>
<tr>
<th scope="row"><i class="fas fa-user me-2"></i>姓名</th>
<td>{{ user.name }}</td>
</tr>
<tr>
<th scope="row"><i class="fas fa-envelope me-2"></i>邮箱</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th scope="row"><i class="fas fa-calendar-alt me-2"></i>注册日期</th>
<td>{{ user.registration_date }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>快速考勤</h5>
</div>
<div class="card-body text-center">
<div class="row">
<div class="col-6">
<button id="check-in-btn" class="btn btn-success btn-lg w-100 mb-2">
<i class="fas fa-sign-in-alt me-2"></i>签到
</button>
</div>
<div class="col-6">
<button id="check-out-btn" class="btn btn-danger btn-lg w-100 mb-2">
<i class="fas fa-sign-out-alt me-2"></i>签退
</button>
</div>
</div>
<div id="attendance-status" class="mt-2"></div>
<div class="mt-3">
<a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-primary w-100">
<i class="fas fa-camera me-2"></i>人脸识别考勤
</a>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-history me-2"></i>考勤记录</h5>
</div>
<div class="card-body">
{% if attendance_records %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>日期</th>
<th>签到时间</th>
<th>签退时间</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for record in attendance_records %}
<tr>
<td>{{ record.date }}</td>
<td>{{ record.check_in_time }}</td>
<td>{{ record.check_out_time if record.check_out_time else '未签退' }}</td>
<td>
{% if record.check_out_time %}
<span class="badge bg-success">已完成</span>
{% else %}
<span class="badge bg-warning">进行中</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>暂无考勤记录
</div>
{% endif %}
</div>
<div class="card-footer text-end">
<a href="{{ url_for('attendance') }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-list me-1"></i>查看全部记录
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card shadow mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>本月统计</h5>
</div>
<div class="card-body">
<canvas id="monthlyChart" width="100%" height="200"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow mb-4">
<div class="card-header bg-warning text-white">
<h5 class="mb-0"><i class="fas fa-bell me-2"></i>通知</h5>
</div>
<div class="card-body">
<div class="list-group">
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">系统更新通知</h6>
<small>3天前</small>
</div>
<p class="mb-1">系统已更新到最新版本,新增人脸识别算法...</p>
</a>
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">考勤规则变更</h6>
<small>1周前</small>
</div>
<p class="mb-1">根据学校规定,考勤时间调整为8:30-17:30...</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.avatar-container {
position: relative;
display: inline-block;
}
.avatar-container .badge {
width: 25px;
height: 25px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 考勤按钮功能
document.getElementById('check-in-btn').addEventListener('click', function() {
const statusDiv = document.getElementById('attendance-status');
statusDiv.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> 处理中...';
fetch('{{ url_for("process_check_in") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
statusDiv.innerHTML = '<div class="alert alert-warning">' + data.message + '</div>';
}
})
.catch(error => {
console.error('Error:', error);
statusDiv.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
});
});
document.getElementById('check-out-btn').addEventListener('click', function() {
const statusDiv = document.getElementById('attendance-status');
statusDiv.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> 处理中...';
fetch('{{ url_for("check_out") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
statusDiv.innerHTML = '<div class="alert alert-warning">' + data.message + '</div>';
}
})
.catch(error => {
console.error('Error:', error);
statusDiv.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
});
});
// 月度统计图表
const ctx = document.getElementById('monthlyChart').getContext('2d');
const monthlyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日'],
datasets: [{
label: '考勤时长(小时)',
data: [8, 8.5, 7.5, 8, 8, 0, 0, 8.5, 8, 7],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true,
max: 10
}
},
plugins: {
legend: {
display: false
}
},
maintainAspectRatio: false
}
});
</script>
{% endblock %}
templates\edit_user.html
{% extends 'base.html' %}
{% block title %}编辑用户 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-user-edit me-2"></i>编辑用户</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('edit_user', user_id=user.id) }}">
<div class="row">
<div class="col-md-6 mb-3">
<label for="student_id" class="form-label">学号 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-id-card"></i></span>
<input type="text" class="form-control" id="student_id" name="student_id" value="{{ user.student_id }}" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="name" class="form-label">姓名 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">重置密码 <small class="text-muted">(留空表示不修改)</small></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-text">如需重置密码,请在此输入新密码</div>
</div>
<div class="mb-3">
<label for="role" class="form-label">用户角色</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user-tag"></i></span>
<select class="form-select" id="role" name="role">
<option value="student" {% if user.role == 'student' %}selected{% endif %}>学生</option>
<option value="teacher" {% if user.role == 'teacher' %}selected{% endif %}>教师</option>
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>管理员</option>
</select>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" {% if user.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active">账号状态(启用/禁用)</label>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">保存修改</button>
</div>
</form>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<a href="{{ url_for('user_management') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回用户列表
</a>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
{% if user.has_face_data %}
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#resetFaceModal">
<i class="fas fa-trash-alt me-1"></i>重置人脸数据
</button>
{% else %}
<a href="{{ url_for('face_registration_admin', user_id=user.id) }}" class="btn btn-outline-success">
<i class="fas fa-camera me-1"></i>注册人脸数据
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Reset Face Data Modal -->
<div class="modal fade" id="resetFaceModal" tabindex="-1" aria-labelledby="resetFaceModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="resetFaceModalLabel">确认重置人脸数据</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要重置用户 <strong>{{ user.name }}</strong> 的人脸数据吗?</p>
<p class="text-danger">此操作不可逆,用户将需要重新注册人脸数据才能使用人脸识别功能。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('reset_face_data', user_id=user.id) }}" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">确认重置</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
templates\face_recognition_attendance.html
{% extends 'base.html' %}
{% block title %}人脸识别考勤 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-camera me-2"></i>人脸识别考勤</h4>
</div>
<div class="card-body">
<div class="text-center mb-4">
<h5 class="mb-3">请面向摄像头,系统将自动识别您的身份</h5>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>请确保光线充足,面部无遮挡
</div>
</div>
<div class="row">
<div class="col-md-8 mx-auto">
<div id="camera-container" class="position-relative">
<video id="webcam" autoplay playsinline width="100%" class="rounded border"></video>
<div id="face-overlay" class="position-absolute top-0 start-0 w-100 h-100"></div>
<canvas id="canvas" class="d-none"></canvas>
</div>
<div id="recognition-status" class="text-center mt-3">
<div class="alert alert-secondary">
<i class="fas fa-spinner fa-spin me-2"></i>准备中...
</div>
</div>
<div id="recognition-result" class="text-center mt-3 d-none">
<div class="card">
<div class="card-body">
<h5 id="result-name" class="card-title mb-2"></h5>
<p id="result-id" class="card-text text-muted"></p>
<p id="result-time" class="card-text"></p>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 mx-auto">
<div class="d-grid gap-2">
<button id="start-camera" class="btn btn-primary">
<i class="fas fa-video me-2"></i>启动摄像头
</button>
<button id="capture-photo" class="btn btn-success d-none">
<i class="fas fa-camera me-2"></i>拍摄并识别
</button>
<button id="retry-button" class="btn btn-secondary d-none">
<i class="fas fa-redo me-2"></i>重新识别
</button>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回控制面板
</a>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
<a href="{{ url_for('check_in') }}" class="btn btn-outline-primary">
<i class="fas fa-clipboard-check me-1"></i>手动考勤
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
#camera-container {
max-width: 640px;
margin: 0 auto;
border-radius: 0.25rem;
overflow: hidden;
}
#face-overlay {
pointer-events: none;
}
.face-box {
position: absolute;
border: 2px solid #28a745;
border-radius: 4px;
}
.face-label {
position: absolute;
background-color: rgba(40, 167, 69, 0.8);
color: white;
padding: 2px 6px;
border-radius: 2px;
font-size: 12px;
top: -20px;
left: 0;
}
.unknown-face {
border-color: #dc3545;
}
.unknown-face .face-label {
background-color: rgba(220, 53, 69, 0.8);
}
.processing-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
}
.pulse {
animation: pulse 1.5s infinite;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
const startCameraBtn = document.getElementById('start-camera');
const capturePhotoBtn = document.getElementById('capture-photo');
const retryButton = document.getElementById('retry-button');
const webcamVideo = document.getElementById('webcam');
const canvas = document.getElementById('canvas');
const faceOverlay = document.getElementById('face-overlay');
const recognitionStatus = document.getElementById('recognition-status');
const recognitionResult = document.getElementById('recognition-result');
const resultName = document.getElementById('result-name');
const resultId = document.getElementById('result-id');
const resultTime = document.getElementById('result-time');
let stream = null;
let isProcessing = false;
// 启动摄像头
startCameraBtn.addEventListener('click', async function() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
});
webcamVideo.srcObject = stream;
startCameraBtn.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
recognitionStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>摄像头已启动,请面向摄像头</div>';
// 添加脉冲效果
webcamVideo.classList.add('pulse');
} catch (err) {
console.error('摄像头访问失败:', err);
recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>无法访问摄像头: ' + err.message + '</div>';
}
});
// 拍摄照片并识别
capturePhotoBtn.addEventListener('click', function() {
if (isProcessing) return;
isProcessing = true;
// 显示处理中状态
faceOverlay.innerHTML = '<div class="processing-indicator"><i class="fas fa-spinner fa-spin me-2"></i>正在识别...</div>';
recognitionStatus.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin me-2"></i>正在处理,请稍候...</div>';
// 拍摄照片
canvas.width = webcamVideo.videoWidth;
canvas.height = webcamVideo.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
// 获取图像数据
const imageData = canvas.toDataURL('image/jpeg');
// 发送到服务器进行人脸识别
fetch('{{ url_for("process_face_attendance") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'image_data=' + encodeURIComponent(imageData)
})
.then(response => response.json())
.then(data => {
isProcessing = false;
faceOverlay.innerHTML = '';
if (data.success) {
// 识别成功
recognitionStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>' + data.message + '</div>';
// 显示结果
resultName.textContent = data.user.name;
resultId.textContent = '学号: ' + data.user.student_id;
resultTime.textContent = '考勤时间: ' + new Date().toLocaleString();
recognitionResult.classList.remove('d-none');
// 更新按钮状态
capturePhotoBtn.classList.add('d-none');
retryButton.classList.remove('d-none');
// 绘制人脸框
drawFaceBox(true, data.user.name);
// 移除脉冲效果
webcamVideo.classList.remove('pulse');
} else {
// 识别失败
recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>' + data.message + '</div>';
// 绘制未知人脸框
drawFaceBox(false);
}
})
.catch(error => {
console.error('Error:', error);
isProcessing = false;
faceOverlay.innerHTML = '';
recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>服务器错误,请稍后重试</div>';
});
});
// 重新识别
retryButton.addEventListener('click', function() {
recognitionResult.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
retryButton.classList.add('d-none');
faceOverlay.innerHTML = '';
recognitionStatus.innerHTML = '<div class="alert alert-secondary"><i class="fas fa-info-circle me-2"></i>请面向摄像头,准备重新识别</div>';
// 添加脉冲效果
webcamVideo.classList.add('pulse');
});
// 绘制人脸框
function drawFaceBox(isRecognized, name) {
// 模拟人脸位置
const videoWidth = webcamVideo.videoWidth;
const videoHeight = webcamVideo.videoHeight;
const scale = webcamVideo.offsetWidth / videoWidth;
// 人脸框位置(居中)
const faceWidth = videoWidth * 0.4;
const faceHeight = videoHeight * 0.5;
const faceLeft = (videoWidth - faceWidth) / 2;
const faceTop = (videoHeight - faceHeight) / 2;
// 创建人脸框元素
const faceBox = document.createElement('div');
faceBox.className = 'face-box' + (isRecognized ? '' : ' unknown-face');
faceBox.style.left = (faceLeft * scale) + 'px';
faceBox.style.top = (faceTop * scale) + 'px';
faceBox.style.width = (faceWidth * scale) + 'px';
faceBox.style.height = (faceHeight * scale) + 'px';
// 添加标签
const faceLabel = document.createElement('div');
faceLabel.className = 'face-label';
faceLabel.textContent = isRecognized ? name : '未识别';
faceBox.appendChild(faceLabel);
faceOverlay.appendChild(faceBox);
}
// 页面卸载时停止摄像头
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}
templates\face_registration.html
{% extends 'base.html' %}
{% block title %}人脸注册 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-camera me-2"></i>人脸注册</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">上传照片</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('face_registration') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="face_image" class="form-label">选择照片</label>
<input class="form-control" type="file" id="face_image" name="face_image" accept="image/jpeg,image/png,image/jpg" required>
<div class="form-text">请上传清晰的正面照片,确保光线充足,面部无遮挡</div>
</div>
<div class="mb-3">
<div id="image-preview" class="text-center d-none">
<img id="preview-img" src="#" alt="预览图" class="img-fluid rounded mb-2" style="max-height: 300px;">
<button type="button" id="clear-preview" class="btn btn-sm btn-outline-danger">
<i class="fas fa-times"></i> 清除
</button>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>上传并注册
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">使用摄像头</h5>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div id="camera-container">
<video id="webcam" autoplay playsinline width="100%" class="rounded"></video>
<canvas id="canvas" class="d-none"></canvas>
</div>
<div id="captured-container" class="d-none">
<img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded mb-2">
</div>
</div>
<div class="d-grid gap-2">
<button id="start-camera" class="btn btn-info">
<i class="fas fa-video me-2"></i>打开摄像头
</button>
<button id="capture-photo" class="btn btn-primary d-none">
<i class="fas fa-camera me-2"></i>拍摄照片
</button>
<button id="retake-photo" class="btn btn-outline-secondary d-none">
<i class="fas fa-redo me-2"></i>重新拍摄
</button>
<button id="save-photo" class="btn btn-success d-none">
<i class="fas fa-save me-2"></i>保存并注册
</button>
</div>
<div id="webcam-status" class="mt-2 text-center"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="alert alert-info mb-0">
<h5><i class="fas fa-info-circle me-2"></i>人脸注册说明</h5>
<ul>
<li>请确保面部清晰可见,无遮挡物(如口罩、墨镜等)</li>
<li>保持自然表情,正面面对摄像头或照片中心</li>
<li>避免强烈的侧光或背光,确保光线均匀</li>
<li>注册成功后,您可以使用人脸识别功能进行考勤</li>
<li>如遇注册失败,请尝试调整光线或姿势后重新尝试</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 照片上传预览
document.getElementById('face_image').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const previewImg = document.getElementById('preview-img');
previewImg.src = event.target.result;
document.getElementById('image-preview').classList.remove('d-none');
};
reader.readAsDataURL(file);
}
});
document.getElementById('clear-preview').addEventListener('click', function() {
document.getElementById('face_image').value = '';
document.getElementById('image-preview').classList.add('d-none');
});
// 摄像头功能
const startCameraBtn = document.getElementById('start-camera');
const capturePhotoBtn = document.getElementById('capture-photo');
const retakePhotoBtn = document.getElementById('retake-photo');
const savePhotoBtn = document.getElementById('save-photo');
const webcamVideo = document.getElementById('webcam');
const canvas = document.getElementById('canvas');
const capturedImage = document.getElementById('captured-image');
const webcamContainer = document.getElementById('camera-container');
const capturedContainer = document.getElementById('captured-container');
const webcamStatus = document.getElementById('webcam-status');
let stream = null;
// 启动摄像头
startCameraBtn.addEventListener('click', async function() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
});
webcamVideo.srcObject = stream;
startCameraBtn.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
webcamStatus.innerHTML = '<span class="text-success">摄像头已启动</span>';
} catch (err) {
console.error('摄像头访问失败:', err);
webcamStatus.innerHTML = '<span class="text-danger">无法访问摄像头: ' + err.message + '</span>';
}
});
// 拍摄照片
capturePhotoBtn.addEventListener('click', function() {
canvas.width = webcamVideo.videoWidth;
canvas.height = webcamVideo.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
capturedImage.src = canvas.toDataURL('image/jpeg');
webcamContainer.classList.add('d-none');
capturedContainer.classList.remove('d-none');
capturePhotoBtn.classList.add('d-none');
retakePhotoBtn.classList.remove('d-none');
savePhotoBtn.classList.remove('d-none');
});
// 重新拍摄
retakePhotoBtn.addEventListener('click', function() {
webcamContainer.classList.remove('d-none');
capturedContainer.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
retakePhotoBtn.classList.add('d-none');
savePhotoBtn.classList.add('d-none');
});
// 保存照片并注册
savePhotoBtn.addEventListener('click', function() {
const imageData = capturedImage.src;
// 显示加载状态
savePhotoBtn.disabled = true;
savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
// 发送到服务器
fetch('{{ url_for("webcam_registration") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'image_data=' + encodeURIComponent(imageData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 注册成功
webcamStatus.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
// 停止摄像头
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
// 3秒后跳转到控制面板
setTimeout(() => {
window.location.href = '{{ url_for("dashboard") }}';
}, 3000);
} else {
// 注册失败
webcamStatus.innerHTML = '<div class="alert alert-danger">' + data.message + '</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
// 重置为拍摄状态
setTimeout(() => {
retakePhotoBtn.click();
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
webcamStatus.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
});
});
// 页面卸载时停止摄像头
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}
templates\face_registration_admin.html
{% extends 'base.html' %}
{% block title %}管理员人脸注册 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-camera me-2"></i>为用户注册人脸数据</h4>
</div>
<div class="card-body">
<div class="alert alert-info mb-4">
<h5 class="mb-2"><i class="fas fa-info-circle me-2"></i>用户信息</h5>
<div class="row">
<div class="col-md-6">
<p><strong>学号:</strong> {{ user.student_id }}</p>
<p><strong>姓名:</strong> {{ user.name }}</p>
</div>
<div class="col-md-6">
<p><strong>邮箱:</strong> {{ user.email }}</p>
<p><strong>注册日期:</strong> {{ user.registration_date }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">上传照片</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('face_registration_admin', user_id=user.id) }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="face_image" class="form-label">选择照片</label>
<input class="form-control" type="file" id="face_image" name="face_image" accept="image/jpeg,image/png,image/jpg" required>
<div class="form-text">请上传清晰的正面照片,确保光线充足,面部无遮挡</div>
</div>
<div class="mb-3">
<div id="image-preview" class="text-center d-none">
<img id="preview-img" src="#" alt="预览图" class="img-fluid rounded mb-2" style="max-height: 300px;">
<button type="button" id="clear-preview" class="btn btn-sm btn-outline-danger">
<i class="fas fa-times"></i> 清除
</button>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>上传并注册
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">使用摄像头</h5>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div id="camera-container">
<video id="webcam" autoplay playsinline width="100%" class="rounded"></video>
<canvas id="canvas" class="d-none"></canvas>
</div>
<div id="captured-container" class="d-none">
<img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded mb-2">
</div>
</div>
<div class="d-grid gap-2">
<button id="start-camera" class="btn btn-info">
<i class="fas fa-video me-2"></i>打开摄像头
</button>
<button id="capture-photo" class="btn btn-primary d-none">
<i class="fas fa-camera me-2"></i>拍摄照片
</button>
<button id="retake-photo" class="btn btn-outline-secondary d-none">
<i class="fas fa-redo me-2"></i>重新拍摄
</button>
<button id="save-photo" class="btn btn-success d-none">
<i class="fas fa-save me-2"></i>保存并注册
</button>
</div>
<div id="webcam-status" class="mt-2 text-center"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回用户编辑
</a>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
<a href="{{ url_for('user_management') }}" class="btn btn-outline-primary">
<i class="fas fa-users me-1"></i>返回用户列表
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 照片上传预览
document.getElementById('face_image').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const previewImg = document.getElementById('preview-img');
previewImg.src = event.target.result;
document.getElementById('image-preview').classList.remove('d-none');
};
reader.readAsDataURL(file);
}
});
document.getElementById('clear-preview').addEventListener('click', function() {
document.getElementById('face_image').value = '';
document.getElementById('image-preview').classList.add('d-none');
});
// 摄像头功能
const startCameraBtn = document.getElementById('start-camera');
const capturePhotoBtn = document.getElementById('capture-photo');
const retakePhotoBtn = document.getElementById('retake-photo');
const savePhotoBtn = document.getElementById('save-photo');
const webcamVideo = document.getElementById('webcam');
const canvas = document.getElementById('canvas');
const capturedImage = document.getElementById('captured-image');
const webcamContainer = document.getElementById('camera-container');
const capturedContainer = document.getElementById('captured-container');
const webcamStatus = document.getElementById('webcam-status');
let stream = null;
// 启动摄像头
startCameraBtn.addEventListener('click', async function() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
});
webcamVideo.srcObject = stream;
startCameraBtn.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
webcamStatus.innerHTML = '<span class="text-success">摄像头已启动</span>';
} catch (err) {
console.error('摄像头访问失败:', err);
webcamStatus.innerHTML = '<span class="text-danger">无法访问摄像头: ' + err.message + '</span>';
}
});
// 拍摄照片
capturePhotoBtn.addEventListener('click', function() {
canvas.width = webcamVideo.videoWidth;
canvas.height = webcamVideo.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
capturedImage.src = canvas.toDataURL('image/jpeg');
webcamContainer.classList.add('d-none');
capturedContainer.classList.remove('d-none');
capturePhotoBtn.classList.add('d-none');
retakePhotoBtn.classList.remove('d-none');
savePhotoBtn.classList.remove('d-none');
});
// 重新拍摄
retakePhotoBtn.addEventListener('click', function() {
webcamContainer.classList.remove('d-none');
capturedContainer.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
retakePhotoBtn.classList.add('d-none');
savePhotoBtn.classList.add('d-none');
});
// 保存照片并注册
savePhotoBtn.addEventListener('click', function() {
const imageData = capturedImage.src;
// 显示加载状态
savePhotoBtn.disabled = true;
savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
// 发送到服务器
fetch('{{ url_for("webcam_registration") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'image_data=' + encodeURIComponent(imageData) + '&user_id={{ user.id }}'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 注册成功
webcamStatus.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
// 停止摄像头
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
// 3秒后跳转到用户编辑页面
setTimeout(() => {
window.location.href = '{{ url_for("edit_user", user_id=user.id) }}';
}, 3000);
} else {
// 注册失败
webcamStatus.innerHTML = '<div class="alert alert-danger">' + data.message + '</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
// 重置为拍摄状态
setTimeout(() => {
retakePhotoBtn.click();
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
webcamStatus.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
});
});
// 页面卸载时停止摄像头
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}
templates\index.html
{% extends 'base.html' %}
{% block title %}首页 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold mb-4">智能校园考勤系统</h1>
<p class="lead mb-4">基于深度学习的人脸识别技术,为校园考勤带来全新体验。告别传统签到方式,实现快速、准确、高效的智能考勤管理。</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-start mb-4">
{% if session.get('user_id') %}
<a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-primary btn-lg px-4 me-md-2">开始考勤</a>
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary btn-lg px-4">控制面板</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg px-4 me-md-2">登录系统</a>
<a href="{{ url_for('register') }}" class="btn btn-outline-secondary btn-lg px-4">注册账号</a>
{% endif %}
</div>
</div>
<div class="col-lg-6">
<img src="https://source.unsplash.com/random/600x400/?face,technology" class="img-fluid rounded shadow" alt="人脸识别技术">
</div>
</div>
<div class="row mt-5 pt-5">
<div class="col-12 text-center">
<h2 class="mb-4">系统特点</h2>
</div>
</div>
<div class="row g-4 py-3">
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-bolt text-primary fa-3x mb-3"></i>
<h3 class="card-title">快速识别</h3>
<p class="card-text">采用先进的深度学习算法,实现毫秒级人脸识别,大幅提高考勤效率。</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-shield-alt text-primary fa-3x mb-3"></i>
<h3 class="card-title">安全可靠</h3>
<p class="card-text">人脸特征加密存储,确保用户隐私安全,防止冒名顶替,提高考勤准确性。</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-chart-line text-primary fa-3x mb-3"></i>
<h3 class="card-title">数据分析</h3>
<p class="card-text">自动生成考勤统计报表,提供直观的数据可视化,辅助教学管理决策。</p>
</div>
</div>
</div>
</div>
<div class="row mt-5 pt-3">
<div class="col-12 text-center">
<h2 class="mb-4">使用流程</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="steps">
<div class="step-item">
<div class="step-number">1</div>
<div class="step-content">
<h4>注册账号</h4>
<p>创建个人账号,填写基本信息</p>
</div>
</div>
<div class="step-item">
<div class="step-number">2</div>
<div class="step-content">
<h4>人脸录入</h4>
<p>上传照片或使用摄像头采集人脸数据</p>
</div>
</div>
<div class="step-item">
<div class="step-number">3</div>
<div class="step-content">
<h4>日常考勤</h4>
<p>通过人脸识别快速完成签到签退</p>
</div>
</div>
<div class="step-item">
<div class="step-number">4</div>
<div class="step-content">
<h4>查看记录</h4>
<p>随时查看个人考勤记录和统计数据</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.steps {
display: flex;
justify-content: space-between;
margin: 2rem 0;
position: relative;
}
.steps:before {
content: '';
position: absolute;
top: 30px;
left: 0;
right: 0;
height: 2px;
background: #e9ecef;
z-index: -1;
}
.step-item {
text-align: center;
flex: 1;
position: relative;
}
.step-number {
width: 60px;
height: 60px;
border-radius: 50%;
background: #0d6efd;
color: white;
font-size: 1.5rem;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
}
.step-content h4 {
margin-bottom: 0.5rem;
}
.step-content p {
color: #6c757d;
}
</style>
{% endblock %}
templates\login.html
{% extends 'base.html' %}
{% block title %}登录 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-3">
<label for="student_id" class="form-label">学号</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-id-card"></i></span>
<input type="text" class="form-control" id="student_id" name="student_id" required autofocus>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">还没有账号? <a href="{{ url_for('register') }}">立即注册</a></p>
</div>
</div>
<div class="card mt-4 shadow">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>人脸识别登录</h5>
</div>
<div class="card-body text-center">
<p>您也可以使用人脸识别功能直接考勤</p>
<a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-info">
<i class="fas fa-camera me-2"></i>人脸识别考勤
</a>
</div>
</div>
</div>
</div>
{% endblock %}
templates\register.html
{% extends 'base.html' %}
{% block title %}注册 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-user-plus me-2"></i>用户注册</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('register') }}">
<div class="row">
<div class="col-md-6 mb-3">
<label for="student_id" class="form-label">学号 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-id-card"></i></span>
<input type="text" class="form-control" id="student_id" name="student_id" required autofocus>
</div>
<div class="form-text">请输入您的学号,将作为登录账号使用</div>
</div>
<div class="col-md-6 mb-3">
<label for="name" class="form-label">姓名 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="name" name="name" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="form-text">请输入有效的电子邮箱,用于接收系统通知</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="form-text">密码长度至少为6位,包含字母和数字</div>
</div>
<div class="col-md-6 mb-3">
<label for="confirm_password" class="form-label">确认密码 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<div class="form-text">请再次输入密码进行确认</div>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="terms" required>
<label class="form-check-label" for="terms">我已阅读并同意 <a href="#" data-bs-toggle="modal" data-bs-target="#termsModal">用户协议</a> 和 <a href="#" data-bs-toggle="modal" data-bs-target="#privacyModal">隐私政策</a></label>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">注册账号</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">已有账号? <a href="{{ url_for('login') }}">立即登录</a></p>
</div>
</div>
</div>
</div>
<!-- Terms Modal -->
<div class="modal fade" id="termsModal" tabindex="-1" aria-labelledby="termsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="termsModalLabel">用户协议</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h5>校园人脸识别考勤系统用户协议</h5>
<p>欢迎使用校园人脸识别考勤系统。请仔细阅读以下条款,注册即表示您同意接受本协议的所有条款。</p>
<h6>1. 服务说明</h6>
<p>校园人脸识别考勤系统(以下简称"本系统")是一款基于深度学习的人脸识别考勤系统,为用户提供自动化考勤服务。</p>
<h6>2. 用户注册与账号安全</h6>
<p>2.1 用户在注册时需要提供真实、准确、完整的个人资料。<br>
2.2 用户应妥善保管账号和密码,因账号和密码泄露导致的一切损失由用户自行承担。<br>
2.3 用户注册成功后,需要上传本人的人脸数据用于识别。</p>
<h6>3. 用户行为规范</h6>
<p>3.1 用户不得利用本系统进行任何违法或不当的活动。<br>
3.2 用户不得尝试破解、篡改或干扰本系统的正常运行。<br>
3.3 用户不得上传非本人的人脸数据,或尝试冒充他人进行考勤。</p>
<h6>4. 隐私保护</h6>
<p>4.1 本系统重视用户隐私保护,收集的个人信息和人脸数据仅用于考勤目的。<br>
4.2 未经用户同意,本系统不会向第三方披露用户个人信息。<br>
4.3 详细隐私政策请参阅《隐私政策》。</p>
<h6>5. 免责声明</h6>
<p>5.1 本系统不保证服务不会中断,对系统的及时性、安全性、准确性也不作保证。<br>
5.2 因网络状况、通讯线路、第三方网站或管理部门的要求等任何原因而导致的服务中断或其他缺陷,本系统不承担任何责任。</p>
<h6>6. 协议修改</h6>
<p>本系统有权在必要时修改本协议条款,修改后的协议一旦公布即代替原协议。用户可在本系统查阅最新版协议条款。</p>
<h6>7. 适用法律</h6>
<p>本协议的订立、执行和解释及争议的解决均应适用中国法律。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- Privacy Modal -->
<div class="modal fade" id="privacyModal" tabindex="-1" aria-labelledby="privacyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="privacyModalLabel">隐私政策</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h5>校园人脸识别考勤系统隐私政策</h5>
<p>本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。请在使用本系统前仔细阅读本政策。</p>
<h6>1. 信息收集</h6>
<p>1.1 基本信息:我们收集您的学号、姓名、电子邮箱等基本信息。<br>
1.2 人脸数据:我们收集您的人脸图像并提取特征向量用于身份识别。<br>
1.3 考勤记录:我们记录您的考勤时间和考勤状态。</p>
<h6>2. 信息使用</h6>
<p>2.1 您的个人信息和人脸数据仅用于身份验证和考勤记录目的。<br>
2.2 我们不会将您的个人信息用于与考勤无关的其他目的。<br>
2.3 未经您的明确许可,我们不会向任何第三方提供您的个人信息。</p>
<h6>3. 信息存储与保护</h6>
<p>3.1 您的人脸特征数据以加密形式存储在我们的数据库中。<br>
3.2 我们采取适当的技术和组织措施来保护您的个人信息不被未经授权的访问、使用或泄露。<br>
3.3 我们定期审查我们的信息收集、存储和处理实践,以防止未经授权的访问和使用。</p>
<h6>4. 信息保留</h6>
<p>4.1 我们仅在必要的时间内保留您的个人信息,以实现本政策中所述的目的。<br>
4.2 当您不再使用本系统时,您可以要求我们删除您的个人信息和人脸数据。</p>
<h6>5. 您的权利</h6>
<p>5.1 您有权访问、更正或删除您的个人信息。<br>
5.2 您有权随时撤回您对收集和使用您个人信息的同意。<br>
5.3 如需行使上述权利,请联系系统管理员。</p>
<h6>6. 政策更新</h6>
<p>我们可能会不时更新本隐私政策。任何重大变更都会通过电子邮件或系统通知的形式通知您。</p>
<h6>7. 联系我们</h6>
<p>如果您对本隐私政策有任何疑问或建议,请联系系统管理员。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 密码一致性验证
document.getElementById('confirm_password').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
if (password !== confirmPassword) {
this.setCustomValidity('两次输入的密码不一致');
} else {
this.setCustomValidity('');
}
});
// 密码强度验证
document.getElementById('password').addEventListener('input', function() {
const password = this.value;
const hasLetter = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const isLongEnough = password.length >= 6;
if (!hasLetter || !hasNumber || !isLongEnough) {
this.setCustomValidity('密码必须至少包含6个字符,包括字母和数字');
} else {
this.setCustomValidity('');
}
});
</script>
{% endblock %}
templates\user_management.html
{% extends 'base.html' %}
{% block title %}用户管理 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-users-cog me-2"></i>用户管理</h4>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<form method="GET" action="{{ url_for('user_management') }}" class="d-flex">
<input type="text" class="form-control me-2" name="search" placeholder="搜索学号或姓名" value="{{ search_query }}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-1"></i>搜索
</button>
</form>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<a href="{{ url_for('register') }}" class="btn btn-success">
<i class="fas fa-user-plus me-1"></i>添加用户
</a>
</div>
</div>
{% if users %}
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<th>学号</th>
<th>姓名</th>
<th>邮箱</th>
<th>注册日期</th>
<th>人脸数据</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.student_id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.registration_date }}</td>
<td>
{% if user.has_face_data %}
<span class="badge bg-success">已注册</span>
{% else %}
<span class="badge bg-warning">未注册</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-outline-primary">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ user.id }}">
<i class="fas fa-trash-alt"></i>
</button>
{% if not user.has_face_data %}
<a href="{{ url_for('face_registration_admin', user_id=user.id) }}" class="btn btn-outline-success">
<i class="fas fa-camera"></i>
</a>
{% endif %}
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ user.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ user.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ user.id }}">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除用户 <strong>{{ user.name }}</strong> ({{ user.student_id }}) 吗?</p>
<p class="text-danger">此操作不可逆,用户的所有数据(包括考勤记录和人脸数据)将被永久删除。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('delete_user', user_id=user.id) }}" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {{ 'disabled' if current_page == 1 else '' }}">
<a class="page-link" href="{{ url_for('user_management', page=current_page-1, search=search_query) }}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% for i in range(1, total_pages + 1) %}
<li class="page-item {{ 'active' if i == current_page else '' }}">
<a class="page-link" href="{{ url_for('user_management', page=i, search=search_query) }}">{{ i }}</a>
</li>
{% endfor %}
<li class="page-item {{ 'disabled' if current_page == total_pages else '' }}">
<a class="page-link" href="{{ url_for('user_management', page=current_page+1, search=search_query) }}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>没有找到用户记录
</div>
{% endif %}
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<button class="btn btn-outline-primary" onclick="window.print()">
<i class="fas fa-print me-1"></i>打印用户列表
</button>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
<a href="#" class="btn btn-outline-success" id="exportBtn">
<i class="fas fa-file-excel me-1"></i>导出Excel
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 导出Excel功能
document.getElementById('exportBtn').addEventListener('click', function(e) {
e.preventDefault();
alert('导出功能将在完整版中提供');
});
</script>
{% endblock %}
templates\webcam_registration.html
{% extends 'base.html' %}
{% block title %}摄像头人脸注册 - 校园人脸识别考勤系统{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-camera me-2"></i>摄像头人脸注册</h4>
</div>
<div class="card-body">
<div class="text-center mb-4">
<h5 class="mb-3">请面向摄像头,确保光线充足,面部清晰可见</h5>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>请保持自然表情,正面面对摄像头
</div>
</div>
<div class="row">
<div class="col-md-8 mx-auto">
<div id="camera-container" class="position-relative">
<video id="webcam" autoplay playsinline width="100%" class="rounded border"></video>
<div id="face-overlay" class="position-absolute top-0 start-0 w-100 h-100"></div>
<canvas id="canvas" class="d-none"></canvas>
</div>
<div id="captured-container" class="d-none text-center mt-3">
<img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded border" style="max-height: 300px;">
</div>
<div id="registration-status" class="text-center mt-3">
<div class="alert alert-secondary">
<i class="fas fa-info-circle me-2"></i>请点击下方按钮启动摄像头
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-8 mx-auto">
<div class="d-grid gap-2">
<button id="start-camera" class="btn btn-primary">
<i class="fas fa-video me-2"></i>启动摄像头
</button>
<button id="capture-photo" class="btn btn-success d-none">
<i class="fas fa-camera me-2"></i>拍摄照片
</button>
<button id="retake-photo" class="btn btn-outline-secondary d-none">
<i class="fas fa-redo me-2"></i>重新拍摄
</button>
<button id="save-photo" class="btn btn-primary d-none">
<i class="fas fa-save me-2"></i>保存并注册
</button>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<a href="{{ url_for('face_registration') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回上传方式
</a>
</div>
<div class="col-md-6 text-md-end mt-2 mt-md-0">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">
<i class="fas fa-home me-1"></i>返回控制面板
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
#camera-container {
max-width: 640px;
margin: 0 auto;
border-radius: 0.25rem;
overflow: hidden;
}
#face-overlay {
pointer-events: none;
}
.face-box {
position: absolute;
border: 2px solid #28a745;
border-radius: 4px;
}
.face-label {
position: absolute;
background-color: rgba(40, 167, 69, 0.8);
color: white;
padding: 2px 6px;
border-radius: 2px;
font-size: 12px;
top: -20px;
left: 0;
}
.processing-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
}
.pulse {
animation: pulse 1.5s infinite;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
const startCameraBtn = document.getElementById('start-camera');
const capturePhotoBtn = document.getElementById('capture-photo');
const retakePhotoBtn = document.getElementById('retake-photo');
const savePhotoBtn = document.getElementById('save-photo');
const webcamVideo = document.getElementById('webcam');
const canvas = document.getElementById('canvas');
const capturedImage = document.getElementById('captured-image');
const cameraContainer = document.getElementById('camera-container');
const capturedContainer = document.getElementById('captured-container');
const faceOverlay = document.getElementById('face-overlay');
const registrationStatus = document.getElementById('registration-status');
let stream = null;
// 启动摄像头
startCameraBtn.addEventListener('click', async function() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
});
webcamVideo.srcObject = stream;
startCameraBtn.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>摄像头已启动,请面向摄像头</div>';
// 添加脉冲效果
webcamVideo.classList.add('pulse');
// 检测人脸
detectFace();
} catch (err) {
console.error('摄像头访问失败:', err);
registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>无法访问摄像头: ' + err.message + '</div>';
}
});
// 模拟人脸检测
function detectFace() {
// 这里仅作为UI示例,实际人脸检测应在服务器端进行
setTimeout(() => {
if (stream && stream.active) {
const videoWidth = webcamVideo.videoWidth;
const videoHeight = webcamVideo.videoHeight;
const scale = webcamVideo.offsetWidth / videoWidth;
// 人脸框位置(居中)
const faceWidth = videoWidth * 0.4;
const faceHeight = videoHeight * 0.5;
const faceLeft = (videoWidth - faceWidth) / 2;
const faceTop = (videoHeight - faceHeight) / 2;
// 创建人脸框元素
const faceBox = document.createElement('div');
faceBox.className = 'face-box';
faceBox.style.left = (faceLeft * scale) + 'px';
faceBox.style.top = (faceTop * scale) + 'px';
faceBox.style.width = (faceWidth * scale) + 'px';
faceBox.style.height = (faceHeight * scale) + 'px';
faceOverlay.innerHTML = '';
faceOverlay.appendChild(faceBox);
registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>检测到人脸,可以进行拍摄</div>';
}
}, 1500);
}
// 拍摄照片
capturePhotoBtn.addEventListener('click', function() {
canvas.width = webcamVideo.videoWidth;
canvas.height = webcamVideo.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
capturedImage.src = canvas.toDataURL('image/jpeg');
cameraContainer.classList.add('d-none');
capturedContainer.classList.remove('d-none');
capturePhotoBtn.classList.add('d-none');
retakePhotoBtn.classList.remove('d-none');
savePhotoBtn.classList.remove('d-none');
registrationStatus.innerHTML = '<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>请确认照片清晰可见,如不满意可重新拍摄</div>';
});
// 重新拍摄
retakePhotoBtn.addEventListener('click', function() {
cameraContainer.classList.remove('d-none');
capturedContainer.classList.add('d-none');
capturePhotoBtn.classList.remove('d-none');
retakePhotoBtn.classList.add('d-none');
savePhotoBtn.classList.add('d-none');
faceOverlay.innerHTML = '';
registrationStatus.innerHTML = '<div class="alert alert-secondary"><i class="fas fa-info-circle me-2"></i>请重新面向摄像头</div>';
// 重新检测人脸
detectFace();
});
// 保存照片并注册
savePhotoBtn.addEventListener('click', function() {
const imageData = capturedImage.src;
// 显示加载状态
savePhotoBtn.disabled = true;
savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
// 发送到服务器
fetch('{{ url_for("webcam_registration") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'image_data=' + encodeURIComponent(imageData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 注册成功
registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>' + data.message + '</div>';
// 停止摄像头
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
// 禁用所有按钮
retakePhotoBtn.disabled = true;
savePhotoBtn.disabled = true;
// 3秒后跳转到控制面板
setTimeout(() => {
window.location.href = '{{ url_for("dashboard") }}';
}, 3000);
} else {
// 注册失败
registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>' + data.message + '</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
// 重置为拍摄状态
setTimeout(() => {
retakePhotoBtn.click();
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>服务器错误,请稍后重试</div>';
savePhotoBtn.disabled = false;
savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
});
});
// 页面卸载时停止摄像头
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}