查看网站域名购物网站制作教程

当前位置: 首页 > news >正文

查看网站域名,购物网站制作教程,wordpress数据主机名,简洁大方的网站首页引言 随着人工智能技术的快速发展#xff0c;深度学习在计算机视觉领域的应用日益广泛。人脸识别作为其中的一个重要分支#xff0c;已经在安防、金融、教育等多个领域展现出巨大的应用价值。本文将详细介绍如何使用Python和深度学习技术构建一个校园人脸识别考勤系统#…引言 随着人工智能技术的快速发展深度学习在计算机视觉领域的应用日益广泛。人脸识别作为其中的一个重要分支已经在安防、金融、教育等多个领域展现出巨大的应用价值。本文将详细介绍如何使用Python和深度学习技术构建一个校园人脸识别考勤系统该系统能够自动识别学生身份并记录考勤信息大大提高了考勤效率减轻了教师的工作负担。 系统概述 功能特点 实时人脸检测与识别能够从摄像头视频流中实时检测并识别人脸自动考勤记录识别学生身份后自动记录考勤信息数据可视化提供直观的考勤统计和数据分析功能管理员后台方便教师和管理员查看和管理考勤记录用户友好界面简洁直观的用户界面易于操作 技术栈 编程语言Python 3.8深度学习框架TensorFlow/Keras、PyTorch人脸检测与识别dlib、face_recognition、OpenCVWeb框架Flask/Django数据库SQLite/MySQL前端技术HTML、CSS、JavaScript、Bootstrap 系统设计 系统架构 系统采用经典的三层架构设计 表示层用户界面包括学生签到界面和管理员后台业务逻辑层核心算法实现包括人脸检测、特征提取和身份识别数据访问层负责数据的存储和检索包括学生信息和考勤记录 数据流程 摄像头捕获实时视频流人脸检测模块从视频帧中检测人脸特征提取模块提取人脸特征身份识别模块将提取的特征与数据库中的特征进行比对考勤记录模块记录识别结果和时间信息数据分析模块生成考勤统计报表 核心技术实现

  1. 人脸检测 人脸检测是整个系统的第一步我们使用HOGHistogram 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_locations2. 人脸特征提取 检测到人脸后我们需要提取人脸的特征向量这里使用深度学习模型如FaceNet、ArcFace来提取高维特征。 import face_recognitiondef extract_face_features(image, face_locations):# 提取人脸特征face_encodings face_recognition.face_encodings(image, face_locations)return face_encodings3. 人脸识别 将提取的特征与数据库中已存储的特征进行比对找出最匹配的身份。 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_names4. 考勤记录 识别到学生身份后系统会自动记录考勤信息包括学生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, dtypenp.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), fx0.25, fy0.25)# 将BGR转换为RGBface_recognition使用RGBrgb_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 Unknownstudent_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 DESCdf pd.read_sql_query(query, conn)conn.close()return render_template(attendance.html, recordsdf.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 datedf 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(rotation45)# 将图表转换为base64编码img io.BytesIO()plt.savefig(img, formatpng)img.seek(0)plot_url base64.b64encode(img.getvalue()).decode()return render_template(statistics.html, plot_urlplot_url)if name main:app.run(debugTrue)系统部署 环境配置 安装必要的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.htmlFile 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 extensionreturn . in filename and filename.rsplit(., 1)[1].lower() in ALLOWED_EXTENSIONSapp.route(/) def index():Home page routeif user_id in session:return redirect(url_for(dashboard))return render_template(index.html)app.route(/login, methods[GET, POST]) def login():Login routeif 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(fWelcome 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 routeif 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 routesession.clear()flash(You have been logged out, info)return redirect(url_for(index))app.route(/dashboard) def dashboard():User dashboard routeif 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 users face encodingsface_encodings FaceEncoding.get_face_encodings_by_user_id(user_id)has_face_data len(face_encodings) 0# Get users attendance recordsattendance_records Attendance.get_attendance_by_user(user_id)return render_template(dashboard.html, useruser, has_face_datahas_face_data, attendance_recordsattendance_records)app.route(/face-registration, methods[GET, POST]) def face_registration():Face registration routeif 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[studentid]}{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 webcamif 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[studentid]}{uuid.uuid4().hex}.jpgfilepath 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 dataif 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[UPLOADFOLDER], ftemp{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[studentid]}{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: fAn error occurred: {str(e)}})app.route(/attendance, methods[GET]) def attendance():View attendance recordsif 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_recordsattendance_records, selected_datedate)app.route(/check-in, methods[GET]) def check_in():Manual check-in pageif 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-inif 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-outif 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 pageif 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 filenametempfilename ftemp{uuid.uuid4().hex}.jpgtemp_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: fWelcome, {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: fWelcome 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 adminsif 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, well 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,usersusers,search_querysearch_query,current_pagepage,total_pagestotal_pages)app.route(/edit-user/int:user_id, methods[GET, POST]) def edit_user(user_id):Edit user routeif 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, well 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, useruser)app.route(/delete-user/int:user_id, methods[POST]) def delete_user(user_id):Delete user routeif 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, well 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 users face dataif 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, well 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_iduser_id))app.route(/face-registration-admin/int:user_id, methods[GET, POST]) def face_registration_admin(user_id):Face registration for admin to register users faceif 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, well 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[studentid]}{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_iduser_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, useruser)app.route(/detect-face, methods[POST]) def detect_face():检测人脸APIif 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)# 转换为RGBOpenCV使用BGRrgb_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():识别人脸APIif 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)# 转换为RGBOpenCV使用BGRrgb_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():记录考勤APIif 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():获取最近考勤记录APIif user_id not in session:return jsonify({success: False, message: 请先登录})# 获取最近的考勤记录默认10条limit request.args.get(limit, 10, typeint)records Attendance.get_recent_attendance(limit)return jsonify({success: True,records: records})if name main:app.run(debugTrue)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(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(name)class FaceDetector:人脸检测与识别类def init(self, model_typehog, tolerance0.6, known_facesNone):初始化人脸检测器参数:model_type (str): 使用的模型类型hogCPU或cnnGPUtolerance (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, modelself.model_type)detection_time time.time() - start_timelogger.info(f检测到{len(face_locations)}个人脸耗时{detection_time:.4f}秒)return face_locationsdef encode_faces(self, image, face_locationsNone):提取图像中人脸的编码特征参数: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使用BGRface_recognition使用RGBrgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)# 为提高性能可以缩小图像small_frame cv2.resize(rgb_frame, (0, 0), fx0.25, fy0.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 fUnknown ({confidence:.2f})cv2.putText(result_image, text, (left 6, bottom - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)return result_imagestaticmethoddef 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.txttext/plain Flask2.0.1 Werkzeug2.0.1 Jinja23.0.1 itsdangerous2.0.1 MarkupSafe2.0.1 numpy1.21.0 opencv-python4.5.3.56 face-recognition1.3.0 face-recognition-models0.3.0 dlib19.22.1 Pillow8.3.1database\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 tablesconn 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 tablesprint(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, adminexample.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(fError 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 tablecursor.execute(fPRAGMA 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 schemaprint(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 typetable AND nameusers)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 doesnt 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 doesnt 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 cant 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(fError 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 operationsstaticmethoddef create_user(student_id, name, email, password):Create a new userconn 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 Nonestaticmethoddef get_user_by_id(user_id):Get user by IDconn 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 Nonestaticmethoddef get_user_by_student_id(student_id):Get user by student IDconn 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 Nonestaticmethoddef authenticate(student_id, password):Authenticate a userconn 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 Nonestaticmethoddef get_all_users(page1, per_page10):Get all usersconn 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 resultstaticmethoddef count_all_users():Count all usersconn sqlite3.connect(DB_PATH)cursor conn.cursor()cursor.execute(SELECT COUNT(*)FROM users)count cursor.fetchone()[0]conn.close()return countstaticmethoddef search_users(query, page1, per_page10):Search usersconn 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 resultstaticmethoddef count_search_results(query):Count search resultsconn 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 countstaticmethoddef update_user(user_id, student_id, name, email, passwordNone, rolestudent, is_activeTrue):Update userconn 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(fError updating user: {e})return Falsestaticmethoddef delete_user(user_id):Delete userconn sqlite3.connect(DB_PATH)cursor conn.cursor()try:# Delete users face encodingscursor.execute(DELETE FROM face_encodingsWHERE user_id ?, (user_id,))# Delete users 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(fError deleting user: {e})return Falseclass FaceEncoding:Face encoding model for handling face-related database operationsstaticmethoddef save_face_encoding(user_id, face_encoding):Save a face encoding for a userconn 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_idstaticmethoddef get_face_encodings_by_user_id(user_id):Get face encodings for a specific userconn 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 resultstaticmethoddef get_all_face_encodings():Get all face encodings with user informationconn 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 resultstaticmethoddef delete_face_encodings_by_user_id(user_id):Delete face encodings for a specific userconn 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(fError deleting face encodings: {e})return Falseclass Attendance:Attendance model for handling attendance-related database operationsstaticmethoddef record_check_in(user_id):Record attendance check-inconn 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_idstaticmethoddef record_check_out(user_id):Record attendance check-outconn 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 0staticmethoddef get_attendance_by_date(date):Get attendance records for a specific dateconn 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 resultstaticmethoddef get_attendance_by_user(user_id):Get attendance records for a specific userconn 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 resultstaticmethoddef get_today_attendance(user_id):Get users attendance for todayconn sqlite3.connect(DB_PATH)cursor conn.cursor()# Get todays 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 Nonestaticmethoddef get_recent_attendance(limit10):Get recent attendance recordsconn 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 resultstatic\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 classtext-center py-4div classspinner-border text-primary rolestatusspan classvisually-hiddenLoading…/span/divp classmt-2\({message}/p/div; }// 显示错误消息 function showError(element, message) {element.innerHTML div classalert alert-danger rolealerti classfas fa-exclamation-circle me-2/i\){message}/div; }// 显示成功消息 function showSuccess(element, message) {element.innerHTML div classalert alert-success rolealerti classfas fa-check-circle me-2/i\({message}/div; }// 显示警告消息 function showWarning(element, message) {element.innerHTML div classalert alert-warning rolealerti classfas fa-exclamation-triangle me-2/i\){message}/div; }// 显示信息消息 function showInfo(element, message) {element.innerHTML div classalert alert-info rolealerti classfas 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-toggletooltip]));tooltipTriggerList.map(function(tooltipTriggerEl) {return new bootstrap.Tooltip(tooltipTriggerEl);});// 初始化弹出框const popoverTriggerList [].slice.call(document.querySelectorAll([data-bs-togglepopover]));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 classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-clipboard-list me-2/i考勤记录/h4/divdiv classcard-bodydiv classrow mb-4div classcol-md-6form methodGET action{{ url_for(attendance) }} classd-flexinput typedate classform-control me-2 namedate value{{ selected_date }} requiredbutton typesubmit classbtn btn-primaryi classfas fa-search me-1/i查询/button/form/divdiv classcol-md-6 text-md-end mt-3 mt-md-0a href{{ url_for(face_recognition_attendance) }} classbtn btn-successi classfas fa-camera me-1/i人脸识别考勤/a/div/div{% if attendance_records %}div classtable-responsivetable classtable table-hover table-stripedthead classtable-lighttrth学号/thth姓名/thth签到时间/thth签退时间/thth状态/thth时长/th/tr/theadtbody{% for record in attendance_records %}trtd{{ record.student_id }}/tdtd{{ record.name }}/tdtd{{ record.check_in_time }}/tdtd{{ record.check_out_time if record.check_out_time else 未签退 }}/tdtd{% if record.check_out_time %}span classbadge bg-success已完成/span{% else %}span classbadge bg-warning进行中/span{% endif %}/tdtd{% 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/divdiv classrow mt-4div classcol-md-6div classcarddiv classcard-header bg-lighth5 classmb-0考勤统计/h5/divdiv classcard-bodydiv classrow text-centerdiv classcol-4div classborder-endh3 classtext-primary{{ attendance_records|length }}/h3p classtext-muted总人数/p/div/divdiv classcol-4div classborder-endh3 classtext-success{% set completed 0 %}{% for record in attendance_records %}{% if record.check_out_time %}{% set completed completed 1 %}{% endif %}{% endfor %}{{ completed }}/h3p classtext-muted已完成/p/div/divdiv classcol-4h3 classtext-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 }}/h3p classtext-muted进行中/p/div/div/div/div/divdiv classcol-md-6 mt-3 mt-md-0div classcarddiv classcard-header bg-lighth5 classmb-0图表统计/h5/divdiv classcard-bodycanvas idattendanceChart width100% height200/canvas/div/div/div/div{% else %}div classalert alert-infoi classfas fa-info-circle me-2/i{{ selected_date }} 没有考勤记录/div{% endif %}/divdiv classcard-footerdiv classrowdiv classcol-md-6button classbtn btn-outline-primary onclickwindow.print()i classfas fa-print me-1/i打印记录/button/divdiv classcol-md-6 text-md-end mt-2 mt-md-0a href# classbtn btn-outline-success idexportBtni classfas fa-file-excel me-1/i导出Excel/a/div/div/div /div {% endblock %}{% block extra_js %} script srchttps://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 langzh-CN headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title{% block title %}校园人脸识别考勤系统{% endblock %}/title!– Bootstrap CSS –link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0-alpha1/dist/css/bootstrap.min.css relstylesheet!– Font Awesome –link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css!– Custom CSS –link relstylesheet href{{ url_for(static, filenamecss/style.css) }}{% block extra_css %}{% endblock %} /head body!– Navigation –nav classnavbar navbar-expand-lg navbar-dark bg-primarydiv classcontainera classnavbar-brand href{{ url_for(index) }}i classfas fa-user-check me-2/i校园人脸识别考勤系统/abutton classnavbar-toggler typebutton data-bs-togglecollapse data-bs-target#navbarNavaria-controlsnavbarNav aria-expandedfalse aria-labelToggle navigationspan classnavbar-toggler-icon/span/buttondiv classcollapse navbar-collapse idnavbarNavul classnavbar-nav ms-autoli classnav-itema classnav-link href{{ url_for(index) }}首页/a/li{% if session.get(user_id) %}li classnav-itema classnav-link href{{ url_for(dashboard) }}控制面板/a/lili classnav-itema classnav-link href{{ url_for(face_recognition_attendance) }}人脸识别考勤/a/lili classnav-itema classnav-link href{{ url_for(attendance) }}考勤记录/a/lili classnav-item dropdowna classnav-link dropdown-toggle href# idnavbarDropdown rolebuttondata-bs-toggledropdown aria-expandedfalsei classfas fa-user-circle me-1/i{{ session.get(name) }}/aul classdropdown-menu aria-labelledbynavbarDropdownlia classdropdown-item href{{ url_for(dashboard) }}个人信息/a/lilia classdropdown-item href{{ url_for(face_registration) }}人脸注册/a/lilihr classdropdown-divider/lilia classdropdown-item href{{ url_for(logout) }}退出登录/a/li/ul/li{% else %}li classnav-itema classnav-link href{{ url_for(login) }}登录/a/lili classnav-itema classnav-link href{{ url_for(register) }}注册/a/li{% endif %}/ul/div/div/nav!– Flash Messages –div classcontainer mt-3{% with messages get_flashed_messages(with_categoriestrue) %}{% if messages %}{% for category, message in messages %}div classalert alert-{{ category }} alert-dismissible fade show rolealert{{ message }}button typebutton classbtn-close data-bs-dismissalert aria-labelClose/button/div{% endfor %}{% endif %}{% endwith %}/div!– Main Content –main classcontainer my-4{% block content %}{% endblock %}/main!– Footer –footer classbg-light py-4 mt-5div classcontainer text-centerp classmb-0copy; {{ now.year }} 校园人脸识别考勤系统 | 基于深度学习的智能考勤解决方案/p/div/footer!– Bootstrap JS Bundle with Popper –script srchttps://cdn.jsdelivr.net/npm/bootstrap5.3.0-alpha1/dist/js/bootstrap.bundle.min.js/script!– jQuery –script srchttps://code.jquery.com/jquery-3.6.0.min.js/script!– Custom JS –script src{{ url_for(static, filenamejs/main.js) }}/script{% block extra_js %}{% endblock %} /body /htmltemplates\dashboard.html {% extends base.html %}{% block title %}控制面板 - 校园人脸识别考勤系统{% endblock %}{% block content %} div classrowdiv classcol-md-4div classcard shadow mb-4div classcard-header bg-primary text-whiteh5 classmb-0i classfas fa-user me-2/i个人信息/h5/divdiv classcard-bodydiv classtext-center mb-3{% if has_face_data %}div classavatar-container mb-3i classfas fa-user-circle fa-6x text-primary/ispan classbadge bg-success position-absolute bottom-0 end-0i classfas fa-check/i/span/divp classtext-successi classfas fa-check-circle me-1/i人脸数据已注册/p{% else %}div classavatar-container mb-3i classfas fa-user-circle fa-6x text-secondary/ispan classbadge bg-warning position-absolute bottom-0 end-0i classfas fa-exclamation/i/span/divp classtext-warningi classfas fa-exclamation-circle me-1/i尚未注册人脸数据/pa href{{ url_for(face_registration) }} classbtn btn-primary btn-smi classfas fa-camera me-1/i立即注册/a{% endif %}/divtable classtabletbodytrth scoperowi classfas fa-id-card me-2/i学号/thtd{{ user.student_id }}/td/trtrth scoperowi classfas fa-user me-2/i姓名/thtd{{ user.name }}/td/trtrth scoperowi classfas fa-envelope me-2/i邮箱/thtd{{ user.email }}/td/trtrth scoperowi classfas fa-calendar-alt me-2/i注册日期/thtd{{ user.registration_date }}/td/tr/tbody/table/div/divdiv classcard shadow mb-4div classcard-header bg-info text-whiteh5 classmb-0i classfas fa-clock me-2/i快速考勤/h5/divdiv classcard-body text-centerdiv classrowdiv classcol-6button idcheck-in-btn classbtn btn-success btn-lg w-100 mb-2i classfas fa-sign-in-alt me-2/i签到/button/divdiv classcol-6button idcheck-out-btn classbtn btn-danger btn-lg w-100 mb-2i classfas fa-sign-out-alt me-2/i签退/button/div/divdiv idattendance-status classmt-2/divdiv classmt-3a href{{ url_for(face_recognition_attendance) }} classbtn btn-primary w-100i classfas fa-camera me-2/i人脸识别考勤/a/div/div/div/divdiv classcol-md-8div classcard shadow mb-4div classcard-header bg-primary text-whiteh5 classmb-0i classfas fa-history me-2/i考勤记录/h5/divdiv classcard-body{% if attendance_records %}div classtable-responsivetable classtable table-hovertheadtrth日期/thth签到时间/thth签退时间/thth状态/th/tr/theadtbody{% for record in attendance_records %}trtd{{ record.date }}/tdtd{{ record.check_in_time }}/tdtd{{ record.check_out_time if record.check_out_time else 未签退 }}/tdtd{% if record.check_out_time %}span classbadge bg-success已完成/span{% else %}span classbadge bg-warning进行中/span{% endif %}/td/tr{% endfor %}/tbody/table/div{% else %}div classalert alert-infoi classfas fa-info-circle me-2/i暂无考勤记录/div{% endif %}/divdiv classcard-footer text-enda href{{ url_for(attendance) }} classbtn btn-outline-primary btn-smi classfas fa-list me-1/i查看全部记录/a/div/divdiv classrowdiv classcol-md-6div classcard shadow mb-4div classcard-header bg-success text-whiteh5 classmb-0i classfas fa-chart-bar me-2/i本月统计/h5/divdiv classcard-bodycanvas idmonthlyChart width100% height200/canvas/div/div/divdiv classcol-md-6div classcard shadow mb-4div classcard-header bg-warning text-whiteh5 classmb-0i classfas fa-bell me-2/i通知/h5/divdiv classcard-bodydiv classlist-groupa href# classlist-group-item list-group-item-actiondiv classd-flex w-100 justify-content-betweenh6 classmb-1系统更新通知/h6small3天前/small/divp classmb-1系统已更新到最新版本新增人脸识别算法…/p/aa href# classlist-group-item list-group-item-actiondiv classd-flex w-100 justify-content-betweenh6 classmb-1考勤规则变更/h6small1周前/small/divp classmb-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 srchttps://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 classspinner-border spinner-border-sm text-primary rolestatusspan classvisually-hiddenLoading…/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 classalert alert-success data.message /div;setTimeout(() {window.location.reload();}, 2000);} else {statusDiv.innerHTML div classalert alert-warning data.message /div;}}).catch(error {console.error(Error:, error);statusDiv.innerHTML div classalert alert-danger服务器错误请稍后重试/div;});});document.getElementById(check-out-btn).addEventListener(click, function() {const statusDiv document.getElementById(attendance-status);statusDiv.innerHTML div classspinner-border spinner-border-sm text-primary rolestatusspan classvisually-hiddenLoading…/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 classalert alert-success data.message /div;setTimeout(() {window.location.reload();}, 2000);} else {statusDiv.innerHTML div classalert alert-warning data.message /div;}}).catch(error {console.error(Error:, error);statusDiv.innerHTML div classalert 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 classrow justify-content-centerdiv classcol-md-8div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-user-edit me-2/i编辑用户/h4/divdiv classcard-bodyform methodPOST action{{ url_for(edit_user, user_iduser.id) }}div classrowdiv classcol-md-6 mb-3label forstudent_id classform-label学号 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-id-card/i/spaninput typetext classform-control idstudent_id namestudent_id value{{ user.student_id }} required/div/divdiv classcol-md-6 mb-3label forname classform-label姓名 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-user/i/spaninput typetext classform-control idname namename value{{ user.name }} required/div/div/divdiv classmb-3label foremail classform-label电子邮箱 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-envelope/i/spaninput typeemail classform-control idemail nameemail value{{ user.email }} required/div/divdiv classmb-3label forpassword classform-label重置密码 small classtext-muted(留空表示不修改)/small/labeldiv classinput-groupspan classinput-group-texti classfas fa-lock/i/spaninput typepassword classform-control idpassword namepassword/divdiv classform-text如需重置密码请在此输入新密码/div/divdiv classmb-3label forrole classform-label用户角色/labeldiv classinput-groupspan classinput-group-texti classfas fa-user-tag/i/spanselect classform-select idrole nameroleoption valuestudent {% if user.role student %}selected{% endif %}学生/optionoption valueteacher {% if user.role teacher %}selected{% endif %}教师/optionoption valueadmin {% if user.role admin %}selected{% endif %}管理员/option/select/div/divdiv classmb-3div classform-check form-switchinput classform-check-input typecheckbox idis_active nameis_active {% if user.is_active %}checked{% endif %}label classform-check-label foris_active账号状态启用/禁用/label/div/divdiv classd-grid gap-2button typesubmit classbtn btn-primary保存修改/button/div/form/divdiv classcard-footerdiv classrowdiv classcol-md-6a href{{ url_for(user_management) }} classbtn btn-outline-secondaryi classfas fa-arrow-left me-1/i返回用户列表/a/divdiv classcol-md-6 text-md-end mt-2 mt-md-0{% if user.has_face_data %}button typebutton classbtn btn-outline-danger data-bs-togglemodal data-bs-target#resetFaceModali classfas fa-trash-alt me-1/i重置人脸数据/button{% else %}a href{{ url_for(face_registration_admin, user_iduser.id) }} classbtn btn-outline-successi classfas fa-camera me-1/i注册人脸数据/a{% endif %}/div/div/div/div/div /div!– Reset Face Data Modal – div classmodal fade idresetFaceModal tabindex-1 aria-labelledbyresetFaceModalLabel aria-hiddentruediv classmodal-dialogdiv classmodal-contentdiv classmodal-headerh5 classmodal-title idresetFaceModalLabel确认重置人脸数据/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodyp确定要重置用户 strong{{ user.name }}/strong 的人脸数据吗/pp classtext-danger此操作不可逆用户将需要重新注册人脸数据才能使用人脸识别功能。/p/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal取消/buttonform action{{ url_for(reset_face_data, user_iduser.id) }} methodPOST styledisplay: inline;button typesubmit classbtn btn-danger确认重置/button/form/div/div/div /div {% endblock %}templates\face_recognition_attendance.html {% extends base.html %}{% block title %}人脸识别考勤 - 校园人脸识别考勤系统{% endblock %}{% block content %} div classrow justify-content-centerdiv classcol-md-8div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-camera me-2/i人脸识别考勤/h4/divdiv classcard-bodydiv classtext-center mb-4h5 classmb-3请面向摄像头系统将自动识别您的身份/h5div classalert alert-infoi classfas fa-info-circle me-2/i请确保光线充足面部无遮挡/div/divdiv classrowdiv classcol-md-8 mx-autodiv idcamera-container classposition-relativevideo idwebcam autoplay playsinline width100% classrounded border/videodiv idface-overlay classposition-absolute top-0 start-0 w-100 h-100/divcanvas idcanvas classd-none/canvas/divdiv idrecognition-status classtext-center mt-3div classalert alert-secondaryi classfas fa-spinner fa-spin me-2/i准备中…/div/divdiv idrecognition-result classtext-center mt-3 d-nonediv classcarddiv classcard-bodyh5 idresult-name classcard-title mb-2/h5p idresult-id classcard-text text-muted/pp idresult-time classcard-text/p/div/div/div/div/divdiv classrow mt-4div classcol-md-8 mx-autodiv classd-grid gap-2button idstart-camera classbtn btn-primaryi classfas fa-video me-2/i启动摄像头/buttonbutton idcapture-photo classbtn btn-success d-nonei classfas fa-camera me-2/i拍摄并识别/buttonbutton idretry-button classbtn btn-secondary d-nonei classfas fa-redo me-2/i重新识别/button/div/div/div/divdiv classcard-footerdiv classrowdiv classcol-md-6a href{{ url_for(dashboard) }} classbtn btn-outline-secondaryi classfas fa-arrow-left me-1/i返回控制面板/a/divdiv classcol-md-6 text-md-end mt-2 mt-md-0a href{{ url_for(check_in) }} classbtn btn-outline-primaryi classfas 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 %} scriptconst 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 classalert alert-successi classfas fa-check-circle me-2/i摄像头已启动请面向摄像头/div;// 添加脉冲效果webcamVideo.classList.add(pulse);} catch (err) {console.error(摄像头访问失败:, err);recognitionStatus.innerHTML div classalert alert-dangeri classfas fa-exclamation-circle me-2/i无法访问摄像头: err.message /div;}});// 拍摄照片并识别capturePhotoBtn.addEventListener(click, function() {if (isProcessing) return;isProcessing true;// 显示处理中状态faceOverlay.innerHTML div classprocessing-indicatori classfas fa-spinner fa-spin me-2/i正在识别…/div;recognitionStatus.innerHTML div classalert alert-infoi classfas 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 classalert alert-successi classfas 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 classalert alert-dangeri classfas fa-exclamation-circle me-2/i data.message /div;// 绘制未知人脸框drawFaceBox(false);}}).catch(error {console.error(Error:, error);isProcessing false;faceOverlay.innerHTML ;recognitionStatus.innerHTML div classalert alert-dangeri classfas 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 classalert alert-secondaryi classfas 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 classrow justify-content-centerdiv classcol-md-10div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-camera me-2/i人脸注册/h4/divdiv classcard-bodydiv classrowdiv classcol-md-6div classcard mb-4div classcard-header bg-lighth5 classmb-0上传照片/h5/divdiv classcard-bodyform methodPOST action{{ url_for(face_registration) }} enctypemultipart/form-datadiv classmb-3label forface_image classform-label选择照片/labelinput classform-control typefile idface_image nameface_image acceptimage/jpeg,image/png,image/jpg requireddiv classform-text请上传清晰的正面照片确保光线充足面部无遮挡/div/divdiv classmb-3div idimage-preview classtext-center d-noneimg idpreview-img src# alt预览图 classimg-fluid rounded mb-2 stylemax-height: 300px;button typebutton idclear-preview classbtn btn-sm btn-outline-dangeri classfas fa-times/i 清除/button/div/divdiv classd-gridbutton typesubmit classbtn btn-primaryi classfas fa-upload me-2/i上传并注册/button/div/form/div/div/divdiv classcol-md-6div classcarddiv classcard-header bg-lighth5 classmb-0使用摄像头/h5/divdiv classcard-bodydiv classtext-center mb-3div idcamera-containervideo idwebcam autoplay playsinline width100% classrounded/videocanvas idcanvas classd-none/canvas/divdiv idcaptured-container classd-noneimg idcaptured-image src# alt已拍摄照片 classimg-fluid rounded mb-2/div/divdiv classd-grid gap-2button idstart-camera classbtn btn-infoi classfas fa-video me-2/i打开摄像头/buttonbutton idcapture-photo classbtn btn-primary d-nonei classfas fa-camera me-2/i拍摄照片/buttonbutton idretake-photo classbtn btn-outline-secondary d-nonei classfas fa-redo me-2/i重新拍摄/buttonbutton idsave-photo classbtn btn-success d-nonei classfas fa-save me-2/i保存并注册/button/divdiv idwebcam-status classmt-2 text-center/div/div/div/div/div/divdiv classcard-footerdiv classalert alert-info mb-0h5i classfas fa-info-circle me-2/i人脸注册说明/h5ulli请确保面部清晰可见无遮挡物如口罩、墨镜等/lili保持自然表情正面面对摄像头或照片中心/lili避免强烈的侧光或背光确保光线均匀/lili注册成功后您可以使用人脸识别功能进行考勤/lili如遇注册失败请尝试调整光线或姿势后重新尝试/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 classtext-success摄像头已启动/span;} catch (err) {console.error(摄像头访问失败:, err);webcamStatus.innerHTML span classtext-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 classspinner-border spinner-border-sm rolestatus aria-hiddentrue/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 classalert 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 classalert alert-danger data.message /div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas fa-save me-2/i保存并注册;// 重置为拍摄状态setTimeout(() {retakePhotoBtn.click();}, 2000);}}).catch(error {console.error(Error:, error);webcamStatus.innerHTML div classalert alert-danger服务器错误请稍后重试/div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas 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 classrow justify-content-centerdiv classcol-md-8div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-camera me-2/i为用户注册人脸数据/h4/divdiv classcard-bodydiv classalert alert-info mb-4h5 classmb-2i classfas fa-info-circle me-2/i用户信息/h5div classrowdiv classcol-md-6pstrong学号/strong {{ user.student_id }}/ppstrong姓名/strong {{ user.name }}/p/divdiv classcol-md-6pstrong邮箱/strong {{ user.email }}/ppstrong注册日期/strong {{ user.registration_date }}/p/div/div/divdiv classrowdiv classcol-md-6div classcard mb-4div classcard-header bg-lighth5 classmb-0上传照片/h5/divdiv classcard-bodyform methodPOST action{{ url_for(face_registration_admin, user_iduser.id) }} enctypemultipart/form-datadiv classmb-3label forface_image classform-label选择照片/labelinput classform-control typefile idface_image nameface_image acceptimage/jpeg,image/png,image/jpg requireddiv classform-text请上传清晰的正面照片确保光线充足面部无遮挡/div/divdiv classmb-3div idimage-preview classtext-center d-noneimg idpreview-img src# alt预览图 classimg-fluid rounded mb-2 stylemax-height: 300px;button typebutton idclear-preview classbtn btn-sm btn-outline-dangeri classfas fa-times/i 清除/button/div/divdiv classd-gridbutton typesubmit classbtn btn-primaryi classfas fa-upload me-2/i上传并注册/button/div/form/div/div/divdiv classcol-md-6div classcarddiv classcard-header bg-lighth5 classmb-0使用摄像头/h5/divdiv classcard-bodydiv classtext-center mb-3div idcamera-containervideo idwebcam autoplay playsinline width100% classrounded/videocanvas idcanvas classd-none/canvas/divdiv idcaptured-container classd-noneimg idcaptured-image src# alt已拍摄照片 classimg-fluid rounded mb-2/div/divdiv classd-grid gap-2button idstart-camera classbtn btn-infoi classfas fa-video me-2/i打开摄像头/buttonbutton idcapture-photo classbtn btn-primary d-nonei classfas fa-camera me-2/i拍摄照片/buttonbutton idretake-photo classbtn btn-outline-secondary d-nonei classfas fa-redo me-2/i重新拍摄/buttonbutton idsave-photo classbtn btn-success d-nonei classfas fa-save me-2/i保存并注册/button/divdiv idwebcam-status classmt-2 text-center/div/div/div/div/div/divdiv classcard-footerdiv classrowdiv classcol-md-6a href{{ url_for(edit_user, user_iduser.id) }} classbtn btn-outline-secondaryi classfas fa-arrow-left me-1/i返回用户编辑/a/divdiv classcol-md-6 text-md-end mt-2 mt-md-0a href{{ url_for(user_management) }} classbtn btn-outline-primaryi classfas 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 classtext-success摄像头已启动/span;} catch (err) {console.error(摄像头访问失败:, err);webcamStatus.innerHTML span classtext-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 classspinner-border spinner-border-sm rolestatus aria-hiddentrue/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 classalert alert-success data.message /div;// 停止摄像头if (stream) {stream.getTracks().forEach(track track.stop());}// 3秒后跳转到用户编辑页面setTimeout(() {window.location.href {{ url_for(edit_user, user_iduser.id) }};}, 3000);} else {// 注册失败webcamStatus.innerHTML div classalert alert-danger data.message /div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas fa-save me-2/i保存并注册;// 重置为拍摄状态setTimeout(() {retakePhotoBtn.click();}, 2000);}}).catch(error {console.error(Error:, error);webcamStatus.innerHTML div classalert alert-danger服务器错误请稍后重试/div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas 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 classrow align-items-centerdiv classcol-lg-6h1 classdisplay-4 fw-bold mb-4智能校园考勤系统/h1p classlead mb-4基于深度学习的人脸识别技术为校园考勤带来全新体验。告别传统签到方式实现快速、准确、高效的智能考勤管理。/pdiv classd-grid gap-2 d-md-flex justify-content-md-start mb-4{% if session.get(user_id) %}a href{{ url_for(face_recognition_attendance) }} classbtn btn-primary btn-lg px-4 me-md-2开始考勤/aa href{{ url_for(dashboard) }} classbtn btn-outline-secondary btn-lg px-4控制面板/a{% else %}a href{{ url_for(login) }} classbtn btn-primary btn-lg px-4 me-md-2登录系统/aa href{{ url_for(register) }} classbtn btn-outline-secondary btn-lg px-4注册账号/a{% endif %}/div/divdiv classcol-lg-6img srchttps://source.unsplash.com/random/600x400/?face,technology classimg-fluid rounded shadow alt人脸识别技术/div /divdiv classrow mt-5 pt-5div classcol-12 text-centerh2 classmb-4系统特点/h2/div /divdiv classrow g-4 py-3div classcol-md-4div classcard h-100 shadow-smdiv classcard-body text-centeri classfas fa-bolt text-primary fa-3x mb-3/ih3 classcard-title快速识别/h3p classcard-text采用先进的深度学习算法实现毫秒级人脸识别大幅提高考勤效率。/p/div/div/divdiv classcol-md-4div classcard h-100 shadow-smdiv classcard-body text-centeri classfas fa-shield-alt text-primary fa-3x mb-3/ih3 classcard-title安全可靠/h3p classcard-text人脸特征加密存储确保用户隐私安全防止冒名顶替提高考勤准确性。/p/div/div/divdiv classcol-md-4div classcard h-100 shadow-smdiv classcard-body text-centeri classfas fa-chart-line text-primary fa-3x mb-3/ih3 classcard-title数据分析/h3p classcard-text自动生成考勤统计报表提供直观的数据可视化辅助教学管理决策。/p/div/div/div /divdiv classrow mt-5 pt-3div classcol-12 text-centerh2 classmb-4使用流程/h2/div /divdiv classrowdiv classcol-12div classstepsdiv classstep-itemdiv classstep-number1/divdiv classstep-contenth4注册账号/h4p创建个人账号填写基本信息/p/div/divdiv classstep-itemdiv classstep-number2/divdiv classstep-contenth4人脸录入/h4p上传照片或使用摄像头采集人脸数据/p/div/divdiv classstep-itemdiv classstep-number3/divdiv classstep-contenth4日常考勤/h4p通过人脸识别快速完成签到签退/p/div/divdiv classstep-itemdiv classstep-number4/divdiv classstep-contenth4查看记录/h4p随时查看个人考勤记录和统计数据/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 classrow justify-content-centerdiv classcol-md-6div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-sign-in-alt me-2/i用户登录/h4/divdiv classcard-bodyform methodPOST action{{ url_for(login) }}div classmb-3label forstudent_id classform-label学号/labeldiv classinput-groupspan classinput-group-texti classfas fa-id-card/i/spaninput typetext classform-control idstudent_id namestudent_id required autofocus/div/divdiv classmb-3label forpassword classform-label密码/labeldiv classinput-groupspan classinput-group-texti classfas fa-lock/i/spaninput typepassword classform-control idpassword namepassword required/div/divdiv classd-grid gap-2button typesubmit classbtn btn-primary登录/button/div/form/divdiv classcard-footer text-centerp classmb-0还没有账号 a href{{ url_for(register) }}立即注册/a/p/div/divdiv classcard mt-4 shadowdiv classcard-header bg-info text-whiteh5 classmb-0i classfas fa-info-circle me-2/i人脸识别登录/h5/divdiv classcard-body text-centerp您也可以使用人脸识别功能直接考勤/pa href{{ url_for(face_recognition_attendance) }} classbtn btn-infoi classfas fa-camera me-2/i人脸识别考勤/a/div/div/div /div {% endblock %}templates\register.html {% extends base.html %}{% block title %}注册 - 校园人脸识别考勤系统{% endblock %}{% block content %} div classrow justify-content-centerdiv classcol-md-8div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-user-plus me-2/i用户注册/h4/divdiv classcard-bodyform methodPOST action{{ url_for(register) }}div classrowdiv classcol-md-6 mb-3label forstudent_id classform-label学号 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-id-card/i/spaninput typetext classform-control idstudent_id namestudent_id required autofocus/divdiv classform-text请输入您的学号将作为登录账号使用/div/divdiv classcol-md-6 mb-3label forname classform-label姓名 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-user/i/spaninput typetext classform-control idname namename required/div/div/divdiv classmb-3label foremail classform-label电子邮箱 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-envelope/i/spaninput typeemail classform-control idemail nameemail required/divdiv classform-text请输入有效的电子邮箱用于接收系统通知/div/divdiv classrowdiv classcol-md-6 mb-3label forpassword classform-label密码 span classtext-danger/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-lock/i/spaninput typepassword classform-control idpassword namepassword required/divdiv classform-text密码长度至少为6位包含字母和数字/div/divdiv classcol-md-6 mb-3label forconfirm_password classform-label确认密码 span classtext-danger*/span/labeldiv classinput-groupspan classinput-group-texti classfas fa-lock/i/spaninput typepassword classform-control idconfirm_password nameconfirm_password required/divdiv classform-text请再次输入密码进行确认/div/div/divdiv classmb-3 form-checkinput typecheckbox classform-check-input idterms requiredlabel classform-check-label forterms我已阅读并同意 a href# data-bs-togglemodal data-bs-target#termsModal用户协议/a 和 a href# data-bs-togglemodal data-bs-target#privacyModal隐私政策/a/label/divdiv classd-grid gap-2button typesubmit classbtn btn-primary btn-lg注册账号/button/div/form/divdiv classcard-footer text-centerp classmb-0已有账号 a href{{ url_for(login) }}立即登录/a/p/div/div/div /div!– Terms Modal – div classmodal fade idtermsModal tabindex-1 aria-labelledbytermsModalLabel aria-hiddentruediv classmodal-dialog modal-lgdiv classmodal-contentdiv classmodal-headerh5 classmodal-title idtermsModalLabel用户协议/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodyh5校园人脸识别考勤系统用户协议/h5p欢迎使用校园人脸识别考勤系统。请仔细阅读以下条款注册即表示您同意接受本协议的所有条款。/ph61. 服务说明/h6p校园人脸识别考勤系统以下简称本系统是一款基于深度学习的人脸识别考勤系统为用户提供自动化考勤服务。/ph62. 用户注册与账号安全/h6p2.1 用户在注册时需要提供真实、准确、完整的个人资料。br2.2 用户应妥善保管账号和密码因账号和密码泄露导致的一切损失由用户自行承担。br2.3 用户注册成功后需要上传本人的人脸数据用于识别。/ph63. 用户行为规范/h6p3.1 用户不得利用本系统进行任何违法或不当的活动。br3.2 用户不得尝试破解、篡改或干扰本系统的正常运行。br3.3 用户不得上传非本人的人脸数据或尝试冒充他人进行考勤。/ph64. 隐私保护/h6p4.1 本系统重视用户隐私保护收集的个人信息和人脸数据仅用于考勤目的。br4.2 未经用户同意本系统不会向第三方披露用户个人信息。br4.3 详细隐私政策请参阅《隐私政策》。/ph65. 免责声明/h6p5.1 本系统不保证服务不会中断对系统的及时性、安全性、准确性也不作保证。br5.2 因网络状况、通讯线路、第三方网站或管理部门的要求等任何原因而导致的服务中断或其他缺陷本系统不承担任何责任。/ph66. 协议修改/h6p本系统有权在必要时修改本协议条款修改后的协议一旦公布即代替原协议。用户可在本系统查阅最新版协议条款。/ph67. 适用法律/h6p本协议的订立、执行和解释及争议的解决均应适用中国法律。/p/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal关闭/button/div/div/div /div!– Privacy Modal – div classmodal fade idprivacyModal tabindex-1 aria-labelledbyprivacyModalLabel aria-hiddentruediv classmodal-dialog modal-lgdiv classmodal-contentdiv classmodal-headerh5 classmodal-title idprivacyModalLabel隐私政策/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodyh5校园人脸识别考勤系统隐私政策/h5p本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。请在使用本系统前仔细阅读本政策。/ph61. 信息收集/h6p1.1 基本信息我们收集您的学号、姓名、电子邮箱等基本信息。br1.2 人脸数据我们收集您的人脸图像并提取特征向量用于身份识别。br1.3 考勤记录我们记录您的考勤时间和考勤状态。/ph62. 信息使用/h6p2.1 您的个人信息和人脸数据仅用于身份验证和考勤记录目的。br2.2 我们不会将您的个人信息用于与考勤无关的其他目的。br2.3 未经您的明确许可我们不会向任何第三方提供您的个人信息。/ph63. 信息存储与保护/h6p3.1 您的人脸特征数据以加密形式存储在我们的数据库中。br3.2 我们采取适当的技术和组织措施来保护您的个人信息不被未经授权的访问、使用或泄露。br3.3 我们定期审查我们的信息收集、存储和处理实践以防止未经授权的访问和使用。/ph64. 信息保留/h6p4.1 我们仅在必要的时间内保留您的个人信息以实现本政策中所述的目的。br4.2 当您不再使用本系统时您可以要求我们删除您的个人信息和人脸数据。/ph65. 您的权利/h6p5.1 您有权访问、更正或删除您的个人信息。br5.2 您有权随时撤回您对收集和使用您个人信息的同意。br5.3 如需行使上述权利请联系系统管理员。/ph66. 政策更新/h6p我们可能会不时更新本隐私政策。任何重大变更都会通过电子邮件或系统通知的形式通知您。/ph67. 联系我们/h6p如果您对本隐私政策有任何疑问或建议请联系系统管理员。/p/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal关闭/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 classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-users-cog me-2/i用户管理/h4/divdiv classcard-bodydiv classrow mb-4div classcol-md-6form methodGET action{{ url_for(user_management) }} classd-flexinput typetext classform-control me-2 namesearch placeholder搜索学号或姓名 value{{ search_query }}button typesubmit classbtn btn-primaryi classfas fa-search me-1/i搜索/button/form/divdiv classcol-md-6 text-md-end mt-3 mt-md-0a href{{ url_for(register) }} classbtn btn-successi classfas fa-user-plus me-1/i添加用户/a/div/div{% if users %}div classtable-responsivetable classtable table-hover table-stripedthead classtable-lighttrth学号/thth姓名/thth邮箱/thth注册日期/thth人脸数据/thth操作/th/tr/theadtbody{% for user in users %}trtd{{ user.student_id }}/tdtd{{ user.name }}/tdtd{{ user.email }}/tdtd{{ user.registration_date }}/tdtd{% if user.has_face_data %}span classbadge bg-success已注册/span{% else %}span classbadge bg-warning未注册/span{% endif %}/tdtddiv classbtn-group btn-group-sma href{{ url_for(edit_user, user_iduser.id) }} classbtn btn-outline-primaryi classfas fa-edit/i/abutton typebutton classbtn btn-outline-danger data-bs-togglemodal data-bs-target#deleteModal{{ user.id }}i classfas fa-trash-alt/i/button{% if not user.has_face_data %}a href{{ url_for(face_registration_admin, user_iduser.id) }} classbtn btn-outline-successi classfas fa-camera/i/a{% endif %}/div!– Delete Modal –div classmodal fade iddeleteModal{{ user.id }} tabindex-1 aria-labelledbydeleteModalLabel{{ user.id }} aria-hiddentruediv classmodal-dialogdiv classmodal-contentdiv classmodal-headerh5 classmodal-title iddeleteModalLabel{{ user.id }}确认删除/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodyp确定要删除用户 strong{{ user.name }}/strong ({{ user.student_id }}) 吗/pp classtext-danger此操作不可逆用户的所有数据包括考勤记录和人脸数据将被永久删除。/p/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal取消/buttonform action{{ url_for(delete_user, user_iduser.id) }} methodPOST styledisplay: inline;button typesubmit classbtn btn-danger确认删除/button/form/div/div/div/div/td/tr{% endfor %}/tbody/table/div!– Pagination –{% if total_pages 1 %}nav aria-labelPage navigationul classpagination justify-content-centerli classpage-item {{ disabled if current_page 1 else }}a classpage-link href{{ url_for(user_management, pagecurrent_page-1, searchsearch_query) }} aria-labelPreviousspan aria-hiddentruelaquo;/span/a/li{% for i in range(1, total_pages 1) %}li classpage-item {{ active if i current_page else }}a classpage-link href{{ url_for(user_management, pagei, searchsearch_query) }}{{ i }}/a/li{% endfor %}li classpage-item {{ disabled if current_page total_pages else }}a classpage-link href{{ url_for(user_management, pagecurrent_page1, searchsearch_query) }} aria-labelNextspan aria-hiddentrueraquo;/span/a/li/ul/nav{% endif %}{% else %}div classalert alert-infoi classfas fa-info-circle me-2/i没有找到用户记录/div{% endif %}/divdiv classcard-footerdiv classrowdiv classcol-md-6button classbtn btn-outline-primary onclickwindow.print()i classfas fa-print me-1/i打印用户列表/button/divdiv classcol-md-6 text-md-end mt-2 mt-md-0a href# classbtn btn-outline-success idexportBtni classfas 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 classrow justify-content-centerdiv classcol-md-8div classcard shadowdiv classcard-header bg-primary text-whiteh4 classmb-0i classfas fa-camera me-2/i摄像头人脸注册/h4/divdiv classcard-bodydiv classtext-center mb-4h5 classmb-3请面向摄像头确保光线充足面部清晰可见/h5div classalert alert-infoi classfas fa-info-circle me-2/i请保持自然表情正面面对摄像头/div/divdiv classrowdiv classcol-md-8 mx-autodiv idcamera-container classposition-relativevideo idwebcam autoplay playsinline width100% classrounded border/videodiv idface-overlay classposition-absolute top-0 start-0 w-100 h-100/divcanvas idcanvas classd-none/canvas/divdiv idcaptured-container classd-none text-center mt-3img idcaptured-image src# alt已拍摄照片 classimg-fluid rounded border stylemax-height: 300px;/divdiv idregistration-status classtext-center mt-3div classalert alert-secondaryi classfas fa-info-circle me-2/i请点击下方按钮启动摄像头/div/div/div/divdiv classrow mt-4div classcol-md-8 mx-autodiv classd-grid gap-2button idstart-camera classbtn btn-primaryi classfas fa-video me-2/i启动摄像头/buttonbutton idcapture-photo classbtn btn-success d-nonei classfas fa-camera me-2/i拍摄照片/buttonbutton idretake-photo classbtn btn-outline-secondary d-nonei classfas fa-redo me-2/i重新拍摄/buttonbutton idsave-photo classbtn btn-primary d-nonei classfas fa-save me-2/i保存并注册/button/div/div/div/divdiv classcard-footerdiv classrowdiv classcol-md-6a href{{ url_for(face_registration) }} classbtn btn-outline-secondaryi classfas fa-arrow-left me-1/i返回上传方式/a/divdiv classcol-md-6 text-md-end mt-2 mt-md-0a href{{ url_for(dashboard) }} classbtn btn-outline-primaryi classfas 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 %} scriptconst 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 classalert alert-successi classfas fa-check-circle me-2/i摄像头已启动请面向摄像头/div;// 添加脉冲效果webcamVideo.classList.add(pulse);// 检测人脸detectFace();} catch (err) {console.error(摄像头访问失败:, err);registrationStatus.innerHTML div classalert alert-dangeri classfas 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 classalert alert-successi classfas 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 classalert alert-infoi classfas 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 classalert alert-secondaryi classfas fa-info-circle me-2/i请重新面向摄像头/div;// 重新检测人脸detectFace();});// 保存照片并注册savePhotoBtn.addEventListener(click, function() {const imageData capturedImage.src;// 显示加载状态savePhotoBtn.disabled true;savePhotoBtn.innerHTML span classspinner-border spinner-border-sm rolestatus aria-hiddentrue/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 classalert alert-successi classfas 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 classalert alert-dangeri classfas fa-exclamation-circle me-2/i data.message /div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas fa-save me-2/i保存并注册;// 重置为拍摄状态setTimeout(() {retakePhotoBtn.click();}, 2000);}}).catch(error {console.error(Error:, error);registrationStatus.innerHTML div classalert alert-dangeri classfas fa-exclamation-circle me-2/i服务器错误请稍后重试/div;savePhotoBtn.disabled false;savePhotoBtn.innerHTML i classfas fa-save me-2/i保存并注册;});});// 页面卸载时停止摄像头window.addEventListener(beforeunload, function() {if (stream) {stream.getTracks().forEach(track track.stop());}}); /script {% endblock %}