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_recognitiondef 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 sqlite3def 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 = rowface_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 *= 4right *= 4bottom *= 4left *= 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_DUPLEXcv2.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 base64app = 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, timeFROM attendanceORDER 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 countFROM attendanceGROUP BY dateORDER 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.pyface_detection.pyREADME.mdrequirements.txtdatabase/db_setup.pyinit_db.pymigrate.pymodels.pystatic/css/style.cssjs/main.jsuploads/templates/attendance.htmlbase.htmldashboard.htmledit_user.htmlface_recognition_attendance.htmlface_registration.htmlface_registration_admin.htmlindex.htmllogin.htmlregister.htmluser_management.htmlwebcam_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 inputif 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 existsexisting_user = User.get_user_by_student_id(student_id)if existing_user:flash('Student ID already registered', 'danger')return render_template('register.html')# Create useruser_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 encodingsface_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)has_face_data = len(face_encodings) > 0# Get user's attendance recordsattendance_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 partif '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 filenameif file.filename == '':flash('No selected file', 'danger')return redirect(request.url)if file and allowed_file(file.filename):# Generate a unique filenamefilename = 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 detectionimage = 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 detectedflash('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 detectedflash('Multiple faces detected in the image. Please upload an image with only your face.', 'danger')return redirect(request.url)# Extract face encodingface_encoding = face_recognition.face_encodings(image, face_locations)[0]# Save face encoding to databaseencoding_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 requestimage_data = request.form.get('image_data')if not image_data:return jsonify({'success': False, 'message': 'No image data received'})# Remove the data URL prefiximage_data = image_data.split(',')[1]# Decode the base64 imageimage_bytes = base64.b64decode(image_data)# Generate a unique filenamefilename = f"{session['student_id']}_{uuid.uuid4().hex}.jpg"filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)# Save the imagewith open(filepath, 'wb') as f:f.write(image_bytes)# Process the image for face detectionimage = 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 detectedreturn 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 detectedreturn jsonify({'success': False, 'message': 'Multiple faces detected in the image. Please ensure only your face is visible.'})# Extract face encodingface_encoding = face_recognition.face_encodings(image, face_locations)[0]# Save face encoding to databaseencoding_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 formimage_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 datauser = User.get_user_by_id(user_id)if not user:return jsonify({'success': False, 'message': 'User not found'})try:# Remove header from the base64 stringimage_data = image_data.split(',')[1]# Decode base64 string to imageimage_bytes = base64.b64decode(image_data)# Create a temporary file to save the imagetemp_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 detectionimage = 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 encodingface_encoding = face_recognition.face_encodings(image, face_locations)[0]# Save face encoding to databaseencoding_id = FaceEncoding.save_face_encoding(user_id, face_encoding)if encoding_id:# Save the processed image with a proper filenamefinal_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 fileos.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 errorif 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-inattendance_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-outsuccess = 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 requestimage_data = request.form.get('image_data')if not image_data:return jsonify({'success': False, 'message': 'No image data received'})# Remove the data URL prefiximage_data = image_data.split(',')[1]# Decode the base64 imageimage_bytes = base64.b64decode(image_data)# Generate a temporary filenametemp_filename = f"temp_{uuid.uuid4().hex}.jpg"temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)# Save the imagewith open(temp_filepath, 'wb') as f:f.write(image_bytes)try:# Process the image for face detectionimage = 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 encodingface_encoding = face_recognition.face_encodings(image, face_locations)[0]# Get all face encodings from databaseall_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 encodingsknown_encodings = [enc['encoding'] for enc in all_encodings]matches = face_recognition.compare_faces(known_encodings, face_encoding)if True in matches:# Find the matching indexmatch_index = matches.index(True)matched_user = all_encodings[match_index]# Record attendanceattendance_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 fileif 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 parameterssearch_query = request.args.get('search', '')page = int(request.args.get('page', 1))per_page = 10# Get users based on search queryif 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 pagestotal_pages = (total_users + per_page - 1) // per_page# Check if each user has face datafor user in users:face_encodings = FaceEncoding.get_face_encodings_by_user_id(user['id'])user['has_face_data'] = len(face_encodings) > 0return 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 datauser = 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 dataface_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)user['has_face_data'] = len(face_encodings) > 0if 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 usersuccess = 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 usersuccess = 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 encodingssuccess = 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 datauser = 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 partif '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 filenameif file.filename == '':flash('No selected file', 'danger')return redirect(request.url)if file and allowed_file(file.filename):# Generate a unique filenamefilename = 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 detectionimage = 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 detectedflash('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 detectedflash('Multiple faces detected in the image. Please upload an image with only one face.', 'danger')return redirect(request.url)# Extract face encodingface_encoding = face_recognition.face_encodings(image, face_locations)[0]# Save face encoding to databaseencoding_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_typeself.tolerance = toleranceself.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_faceslogger.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_timelogger.info(f"检测到{len(face_locations)}个人脸,耗时{detection_time:.4f}秒")return face_locationsdef 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_timelogger.info(f"提取了{len(face_encodings)}个人脸特征,耗时{encoding_time:.4f}秒")return face_encodingsdef 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 resultsdef 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_resultsdef 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_resultsdef 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@staticmethoddef 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 Trueexcept Exception as e:logger.error(f"保存人脸图像失败: {e}")return Falsedef 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 tablecursor.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 tablecursor.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 tablecursor.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 databaseconn = sqlite3.connect(DB_PATH)cursor = conn.cursor()try:# Create users tablecursor.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 tablecursor.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 tablecursor.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 tablecursor.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 tablecursor.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 tablecursor.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 existscursor.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1")if not cursor.fetchone():import hashlibadmin_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 Truereturn Falsedef migrate_database():"""Migrate database to latest schema"""print("Starting database migration...")# Connect to databaseconn = sqlite3.connect(DB_PATH)cursor = conn.cursor()try:# Check if database existscursor.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 existif 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 existif 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 schemacursor.execute("PRAGMA table_info(face_encodings)")columns = cursor.fetchall()encoding_column_type = Nonefor column in columns:if column[1] == 'encoding':encoding_column_type = column[2]break# If encoding column is not BLOB, we need to recreate the tableif encoding_column_type != 'BLOB':print("Updating face_encodings table schema...")# Create a backup of the face_encodings tablecursor.execute("CREATE TABLE IF NOT EXISTS face_encodings_backup AS SELECT * FROM face_encodings")# Drop the original tablecursor.execute("DROP TABLE face_encodings")# Create the table with the correct schemacursor.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 dataprint("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"""@staticmethoddef create_user(student_id, name, email, password):"""Create a new user"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()# Hash the passwordhashed_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.lastrowidconn.close()return user_idexcept sqlite3.IntegrityError:conn.close()return None@staticmethoddef 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_activeFROM usersWHERE 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@staticmethoddef 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_activeFROM usersWHERE 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@staticmethoddef authenticate(student_id, password):"""Authenticate a user"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()# Hash the passwordhashed_password = hashlib.sha256(password.encode()).hexdigest()cursor.execute('''SELECT id, student_id, name, email, registration_date, role, is_activeFROM usersWHERE 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@staticmethoddef get_all_users(page=1, per_page=10):"""Get all users"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()offset = (page - 1) * per_pagecursor.execute('''SELECT id, student_id, name, email, registration_date, role, is_activeFROM usersORDER BY id DESCLIMIT ? 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@staticmethoddef 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@staticmethoddef search_users(query, page=1, per_page=10):"""Search users"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()offset = (page - 1) * per_pagesearch_query = f"%{query}%"cursor.execute('''SELECT id, student_id, name, email, registration_date, role, is_activeFROM usersWHERE student_id LIKE ? OR name LIKE ?ORDER BY id DESCLIMIT ? 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@staticmethoddef count_search_results(query):"""Count search results"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()search_query = f"%{query}%"cursor.execute('''SELECT COUNT(*)FROM usersWHERE student_id LIKE ? OR name LIKE ?''', (search_query, search_query))count = cursor.fetchone()[0]conn.close()return count@staticmethoddef 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 usersSET 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 usersSET student_id = ?, name = ?, email = ?, role = ?, is_active = ?WHERE id = ?''', (student_id, name, email, role, is_active, user_id))conn.commit()return Trueexcept Exception as e:print(f"Error updating user: {e}")return False@staticmethoddef delete_user(user_id):"""Delete user"""conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()try:# Delete user's face encodingscursor.execute('''DELETE FROM face_encodingsWHERE user_id = ?''', (user_id,))# Delete user's attendance recordscursor.execute('''DELETE FROM attendanceWHERE user_id = ?''', (user_id,))# Delete usercursor.execute('''DELETE FROM usersWHERE id = ?''', (user_id,))conn.commit()return Trueexcept Exception as e:print(f"Error deleting user: {e}")return Falseclass FaceEncoding:"""Face encoding model for handling face-related database operations"""@staticmethoddef 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 storageencoding_bytes = pickle.dumps(face_encoding)cursor.execute('''INSERT INTO face_encodings (user_id, encoding)VALUES (?, ?)''', (user_id, encoding_bytes))conn.commit()encoding_id = cursor.lastrowidconn.close()return encoding_id@staticmethoddef 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, encodingFROM face_encodingsWHERE user_id = ?''', (user_id,))encodings = cursor.fetchall()conn.close()result = []for encoding in encodings:# Convert bytes back to numpy arrayface_encoding = pickle.loads(encoding[2])result.append({'id': encoding[0],'user_id': encoding[1],'encoding': face_encoding})return result@staticmethoddef 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.nameFROM face_encodings fJOIN users u ON f.user_id = u.id''')encodings = cursor.fetchall()conn.close()result = []for encoding in encodings:# Convert bytes back to numpy arrayface_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@staticmethoddef 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_encodingsWHERE user_id = ?''', (user_id,))conn.commit()return Trueexcept Exception as e:print(f"Error deleting face encodings: {e}")return Falseclass Attendance:"""Attendance model for handling attendance-related database operations"""@staticmethoddef 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 todaycursor.execute('''SELECT id FROM attendanceWHERE user_id = ? AND date = ? AND check_out_time IS NULL''', (user_id, today))existing = cursor.fetchone()if existing:conn.close()return Falsecursor.execute('''INSERT INTO attendance (user_id, date)VALUES (?, ?)''', (user_id, today))conn.commit()attendance_id = cursor.lastrowidconn.close()return attendance_id@staticmethoddef 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 attendanceSET check_out_time = ?WHERE user_id = ? AND date = ? AND check_out_time IS NULL''', (now, user_id, today))affected = cursor.rowcountconn.commit()conn.close()return affected > 0@staticmethoddef 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_timeFROM attendance aJOIN users u ON a.user_id = u.idWHERE 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@staticmethoddef 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_timeFROM attendanceWHERE 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@staticmethoddef 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, statusFROM attendanceWHERE 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@staticmethoddef 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.nameFROM attendance aJOIN users u ON a.user_id = u.idORDER BY a.check_in_time DESCLIMIT ?''', (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 %}
相关文章:
Python项目-基于深度学习的校园人脸识别考勤系统
引言 随着人工智能技术的快速发展,深度学习在计算机视觉领域的应用日益广泛。人脸识别作为其中的一个重要分支,已经在安防、金融、教育等多个领域展现出巨大的应用价值。本文将详细介绍如何使用Python和深度学习技术构建一个校园人脸识别考勤系统&#…...
浅谈C++函数特性
C的函数特性 前言 在C中,函数加入了许多特性,例如:a、函数缺省参数 b、函数重载 c、内联函数 等等……,这里我会和大家详细去探讨这些特性。以及探讨这些特性的一些细节,同时在内联部分,我们还会把C语言的…...
Python----数据分析(Matplotlib三:绘图二:箱图,散点图,饼图,热力图,3D图)
一、箱图 箱图(Box Plot),又称为箱形图、箱线图、盒式图、盒状图或盒须图,是一种用于展示数据分布情况的统计图表 箱图通过显示数据的中位数、上下四分位数(Q1和Q3)、异常值和数据的分布范围,提…...
高性能PHP框架webman爬虫引擎插件,如何爬取数据
文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons:JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram,自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 ? 5 IDEA必装的插件&…...
【2025年后端开发终极指南:云原生、AI融合与性能优化实战】
一、2025年后端开发的五大核心趋势 1. 云原生架构的全面普及 云原生(Cloud Native)已经成为企业级应用的核心底座。通过容器化技术(DockerKubernetes)和微服务架构,开发者能够实现应用的快速部署、弹性伸缩和故障自愈…...
健康养生:开启活力人生的钥匙
在这个瞬息万变的时代,人们愈发珍视健康。健康养生,宛如一把神奇的钥匙,为我们打开通往活力人生的大门,全方位呵护身心,提升生活品质。 从饮食层面看,均衡膳食是核心。每餐力求包含碳水化合物、蛋白质、脂…...
vue2+ele-ui实践
前言:真理先于实践,实践发现真理,再实践检验真理 环境:vue2 & element-ui 正片: Select 选择器 简称 下拉框 下拉框完整的使用循环 下拉框 → 点击下拉框 → 展示数据 → 选择数据 → 下拉框显示数据 核心具有…...
三维重建(十五)——多尺度(coarse-to-fine)
文章目录 一、多尺度与图像金字塔:从全局结构到局部细节二、特征提取与匹配2.1 从数据采集的角度2.2 从数据增强的角度2.3 从特征提取的方式三、以多尺度的方式使用特征3.1 特征提取与匹配3.1.1 多尺度特征检测3.1.2 金字塔匹配3.2 深度估计与立体匹配3.2.1 多尺度立体匹配3.2…...
SparkStreaming之04:调优
SparkStreaming调优 一 、要点 4.1 SparkStreaming运行原理 深入理解 4.2 调优策略 4.2.1 调整BlockReceiver的数量 案例演示: object MultiReceiverNetworkWordCount {def main(args: Array[String]) {val sparkConf new SparkConf().setAppName("Networ…...
勿以危小而为之勿以避率而不为
《故事汇之:所见/所闻/所历/所想》:《公园散步与小雨遇记》(二) 就差一点到山顶了,路上碰到一阿姨,她说等会儿要下大雨了,让我不要往上走了,我犹豫了一会儿,还是听劝地返…...
JavaWeb后端基础(4)
这一篇就开始是做一个项目了,在项目里学习,我主要记录在学习过程中遇到的问题,以及一些知识点 Restful风格 一种软件架构风格 在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。 GET : 查询 …...
SpringBoot调用DeepSeek
引入依赖 <dependency><groupId>io.github.pig-mesh.ai</groupId><artifactId>deepseek-spring-boot-starter</artifactId><version>1.4.5</version> </dependency>配置 deepseek:api-key: sk-******base-url: https://api.…...
记录一下本地部署Dify的坑
1. 截止2025-3-4为止,请注意,不要直接拉Dify的1.0.0版本。请先试用0.15.3版本。1.0.0有一个bug需要解决。[PANIC]failed to init dify plugin db: failed to connect to hostdb userpostgres databasepostgres Issue #14707 langgenius/dify GitHub …...
LC109. 有序链表转换平衡二叉搜索树
LC109. 有序链表转换平衡二叉搜索树 题目要求(一)快慢指针1. 理解问题2. 解决思路3. 具体步骤4. 代码实现5. 复杂度分析6. 示例解释7. 总结 LC109. 有序链表转换平衡二叉搜索树 题目要求 (一)快慢指针 要将一个按升序排列的单链表转换为平衡的二叉搜索树(BST&…...
Hutool一个类型转换工具类 `Convert`,
Hutool 是一个非常实用的Java工具库,旨在简化Java开发中的常见任务。它包含了一个类型转换工具类 Convert,可以帮助开发者轻松地进行各种类型之间的转换。以下是一些使用 Convert 类进行类型转换的例子: 基本类型转换 假设你需要将一个字符…...
基于eRDMA实测DeepSeek开源的3FS
DeepSeek昨天开源了3FS分布式文件系统, 通过180个存储节点提供了 6.6TiB/s的存储性能, 全面支持大模型的训练和推理的KVCache转存以及向量数据库等能力, 每个客户端节点支持40GB/s峰值吞吐用于KVCache查找. 发布后, 我们在阿里云ECS上进行了快速的复现, 并进行了性能测试, ECS…...
【Linux篇】第一个系统程序 - 进度条
文章目录 1.回车与换行2.行缓冲区3.倒计时程序4.进度条 1.回车与换行 回车的概念: 回到当前行的最开始 \r换行的概念: 换到当前行的下一行\n 2.行缓冲区 当我们运行下面这段程序时,我们会发现屏幕上首先会打印出hello world!,再过两秒后程序结束。 当我们把\n去掉…...
VLM-E2E:通过多模态驾驶员注意融合增强端到端自动驾驶
25年2月来自香港科大广州分校、理想汽车和厦门大学的论文“VLM-E2E: Enhancing End-to-End Autonomous Driving with Multimodal Driver Attention Fusion”。 人类驾驶员能够利用丰富的注意语义,熟练地应对复杂场景,但当前的自动驾驶系统难以复制这种能…...
如何将飞书多维表格与DeepSeek R1结合使用:效率提升的完美搭档
将飞书的多维表格与DeepSeek R1结合使用,就像为你的数据管理和分析之旅装上一台涡轮增压器。两者的合作,不仅仅在速度上让人耳目一新,更是将智能化分析带入了日常的工作场景。以下是它们如何相辅相成并改变我们工作方式的一些分享。 --- 在…...
Kali CentOs 7代理
工具v2↓ kali_IP段v2端口例子<1> kali_IP段v2端口例子<2> CentOs 7 //编辑配置文件 vi /etc/profile//在该配置文件的最后添加代理配置 export http_proxyhttp://ip:port //代理服务器ip地址和端口号 export https_proxyhttp://ip:port //代理服务器ip地址和…...
遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
Matlab实现任意伪彩色图像可视化显示
Matlab实现任意伪彩色图像可视化显示 1、灰度原始图像2、RGB彩色原始图像 在科研研究中,如何展示好看的实验结果图像非常重要!!! 1、灰度原始图像 灰度图像每个像素点只有一个数值,代表该点的亮度(或…...
渗透实战PortSwigger Labs指南:自定义标签XSS和SVG XSS利用
阻止除自定义标签之外的所有标签 先输入一些标签测试,说是全部标签都被禁了 除了自定义的 自定义<my-tag onmouseoveralert(xss)> <my-tag idx onfocusalert(document.cookie) tabindex1> onfocus 当元素获得焦点时(如通过点击或键盘导航&…...
李沐--动手学深度学习--GRU
1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...
