[项目实践] Python项目-基于深度学习的校园人脸识别考勤系统

603 0
Honkers 2025-3-5 16:15:21 | 显示全部楼层 |阅读模式

引言

随着人工智能技术的快速发展,深度学习在计算机视觉领域的应用日益广泛。人脸识别作为其中的一个重要分支,已经在安防、金融、教育等多个领域展现出巨大的应用价值。本文将详细介绍如何使用Python和深度学习技术构建一个校园人脸识别考勤系统,该系统能够自动识别学生身份并记录考勤信息,大大提高了考勤效率,减轻了教师的工作负担。

系统概述

功能特点

  • 实时人脸检测与识别:能够从摄像头视频流中实时检测并识别人脸
  • 自动考勤记录:识别学生身份后自动记录考勤信息
  • 数据可视化:提供直观的考勤统计和数据分析功能
  • 管理员后台:方便教师和管理员查看和管理考勤记录
  • 用户友好界面:简洁直观的用户界面,易于操作

技术栈

  • 编程语言:Python 3.8+
  • 深度学习框架:TensorFlow/Keras、PyTorch
  • 人脸检测与识别:dlib、face_recognition、OpenCV
  • Web框架:Flask/Django
  • 数据库:SQLite/MySQL
  • 前端技术:HTML、CSS、JavaScript、Bootstrap

系统设计

系统架构

系统采用经典的三层架构设计:

  1. 表示层:用户界面,包括学生签到界面和管理员后台
  2. 业务逻辑层:核心算法实现,包括人脸检测、特征提取和身份识别
  3. 数据访问层:负责数据的存储和检索,包括学生信息和考勤记录

数据流程

  1. 摄像头捕获实时视频流
  2. 人脸检测模块从视频帧中检测人脸
  3. 特征提取模块提取人脸特征
  4. 身份识别模块将提取的特征与数据库中的特征进行比对
  5. 考勤记录模块记录识别结果和时间信息
  6. 数据分析模块生成考勤统计报表

核心技术实现

1. 人脸检测

人脸检测是整个系统的第一步,我们使用HOG(Histogram of Oriented Gradients)算法或基于深度学习的方法(如MTCNN、RetinaFace)来检测图像中的人脸。

  1. import cv2
  2. import dlib
  3. # 使用dlib的人脸检测器
  4. detector = dlib.get_frontal_face_detector()
  5. def detect_faces(image):
  6. # 转换为灰度图
  7. gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  8. # 检测人脸
  9. faces = detector(gray, 1)
  10. # 返回人脸位置列表
  11. face_locations = []
  12. for face in faces:
  13. x, y, w, h = face.left(), face.top(), face.width(), face.height()
  14. face_locations.append((y, x + w, y + h, x))
  15. return face_locations
复制代码

2. 人脸特征提取

检测到人脸后,我们需要提取人脸的特征向量,这里使用深度学习模型(如FaceNet、ArcFace)来提取高维特征。

  1. import face_recognition
  2. def extract_face_features(image, face_locations):
  3. # 提取人脸特征
  4. face_encodings = face_recognition.face_encodings(image, face_locations)
  5. return face_encodings
复制代码

3. 人脸识别

将提取的特征与数据库中已存储的特征进行比对,找出最匹配的身份。

  1. def recognize_faces(face_encodings, known_face_encodings, known_face_names):
  2. recognized_names = []
  3. for face_encoding in face_encodings:
  4. # 比较人脸特征与已知特征的距离
  5. matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
  6. name = "Unknown"
  7. # 找出距离最小的匹配
  8. face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
  9. best_match_index = np.argmin(face_distances)
  10. if matches[best_match_index]:
  11. name = known_face_names[best_match_index]
  12. recognized_names.append(name)
  13. return recognized_names
复制代码

4. 考勤记录

识别到学生身份后,系统会自动记录考勤信息,包括学生ID、姓名、时间等。

  1. import datetime
  2. import sqlite3
  3. def record_attendance(student_id, student_name):
  4. conn = sqlite3.connect('attendance.db')
  5. cursor = conn.cursor()
  6. # 获取当前时间
  7. now = datetime.datetime.now()
  8. date = now.strftime("%Y-%m-%d")
  9. time = now.strftime("%H:%M:%S")
  10. # 插入考勤记录
  11. cursor.execute("""
  12. INSERT INTO attendance (student_id, student_name, date, time)
  13. VALUES (?, ?, ?, ?)
  14. """, (student_id, student_name, date, time))
  15. conn.commit()
  16. conn.close()
复制代码

系统集成

将上述模块集成到一个完整的系统中,下面是主程序的示例代码:

  1. import cv2
  2. import numpy as np
  3. import face_recognition
  4. import os
  5. from datetime import datetime
  6. import sqlite3
  7. # 初始化数据库
  8. def init_database():
  9. conn = sqlite3.connect('attendance.db')
  10. cursor = conn.cursor()
  11. # 创建学生表
  12. cursor.execute('''
  13. CREATE TABLE IF NOT EXISTS students (
  14. id INTEGER PRIMARY KEY,
  15. student_id TEXT,
  16. name TEXT,
  17. face_encoding BLOB
  18. )
  19. ''')
  20. # 创建考勤记录表
  21. cursor.execute('''
  22. CREATE TABLE IF NOT EXISTS attendance (
  23. id INTEGER PRIMARY KEY,
  24. student_id TEXT,
  25. student_name TEXT,
  26. date TEXT,
  27. time TEXT
  28. )
  29. ''')
  30. conn.commit()
  31. conn.close()
  32. # 加载已知学生人脸特征
  33. def load_known_faces():
  34. conn = sqlite3.connect('attendance.db')
  35. cursor = conn.cursor()
  36. cursor.execute("SELECT student_id, name, face_encoding FROM students")
  37. rows = cursor.fetchall()
  38. known_face_encodings = []
  39. known_face_ids = []
  40. known_face_names = []
  41. for row in rows:
  42. student_id, name, face_encoding_blob = row
  43. face_encoding = np.frombuffer(face_encoding_blob, dtype=np.float64)
  44. known_face_encodings.append(face_encoding)
  45. known_face_ids.append(student_id)
  46. known_face_names.append(name)
  47. conn.close()
  48. return known_face_encodings, known_face_ids, known_face_names
  49. # 主程序
  50. def main():
  51. # 初始化数据库
  52. init_database()
  53. # 加载已知人脸
  54. known_face_encodings, known_face_ids, known_face_names = load_known_faces()
  55. # 打开摄像头
  56. video_capture = cv2.VideoCapture(0)
  57. # 记录已识别的学生,避免重复记录
  58. recognized_students = set()
  59. while True:
  60. # 读取一帧视频
  61. ret, frame = video_capture.read()
  62. # 缩小图像以加快处理速度
  63. small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
  64. # 将BGR转换为RGB(face_recognition使用RGB)
  65. rgb_small_frame = small_frame[:, :, ::-1]
  66. # 检测人脸
  67. face_locations = face_recognition.face_locations(rgb_small_frame)
  68. face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)
  69. face_names = []
  70. for face_encoding in face_encodings:
  71. # 比较人脸
  72. matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
  73. name = "Unknown"
  74. student_id = "Unknown"
  75. # 找出最匹配的人脸
  76. face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
  77. best_match_index = np.argmin(face_distances)
  78. if matches[best_match_index]:
  79. name = known_face_names[best_match_index]
  80. student_id = known_face_ids[best_match_index]
  81. # 记录考勤
  82. if student_id not in recognized_students:
  83. record_attendance(student_id, name)
  84. recognized_students.add(student_id)
  85. face_names.append(name)
  86. # 显示结果
  87. for (top, right, bottom, left), name in zip(face_locations, face_names):
  88. # 放大回原始大小
  89. top *= 4
  90. right *= 4
  91. bottom *= 4
  92. left *= 4
  93. # 绘制人脸框
  94. cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
  95. # 绘制名字标签
  96. cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
  97. font = cv2.FONT_HERSHEY_DUPLEX
  98. cv2.putText(frame, name, (left + 6, bottom - 6), font, 1.0, (255, 255, 255), 1)
  99. # 显示结果图像
  100. cv2.imshow('Video', frame)
  101. # 按q退出
  102. if cv2.waitKey(1) & 0xFF == ord('q'):
  103. break
  104. # 释放资源
  105. video_capture.release()
  106. cv2.destroyAllWindows()
  107. if __name__ == "__main__":
  108. main()
复制代码

Web界面实现

使用Flask框架构建Web界面,方便用户操作和查看考勤记录。

  1. from flask import Flask, render_template, request, redirect, url_for
  2. import sqlite3
  3. import pandas as pd
  4. import matplotlib.pyplot as plt
  5. import io
  6. import base64
  7. app = Flask(__name__)
  8. @app.route('/')
  9. def index():
  10. return render_template('index.html')
  11. @app.route('/attendance')
  12. def attendance():
  13. conn = sqlite3.connect('attendance.db')
  14. # 获取考勤记录
  15. query = """
  16. SELECT student_id, student_name, date, time
  17. FROM attendance
  18. ORDER BY date DESC, time DESC
  19. """
  20. df = pd.read_sql_query(query, conn)
  21. conn.close()
  22. return render_template('attendance.html', records=df.to_dict('records'))
  23. @app.route('/statistics')
  24. def statistics():
  25. conn = sqlite3.connect('attendance.db')
  26. # 获取考勤统计
  27. query = """
  28. SELECT date, COUNT(DISTINCT student_id) as count
  29. FROM attendance
  30. GROUP BY date
  31. ORDER BY date
  32. """
  33. df = pd.read_sql_query(query, conn)
  34. conn.close()
  35. # 生成统计图表
  36. plt.figure(figsize=(10, 6))
  37. plt.bar(df['date'], df['count'])
  38. plt.xlabel('日期')
  39. plt.ylabel('出勤人数')
  40. plt.title('每日出勤统计')
  41. plt.xticks(rotation=45)
  42. # 将图表转换为base64编码
  43. img = io.BytesIO()
  44. plt.savefig(img, format='png')
  45. img.seek(0)
  46. plot_url = base64.b64encode(img.getvalue()).decode()
  47. return render_template('statistics.html', plot_url=plot_url)
  48. if __name__ == '__main__':
  49. app.run(debug=True)
复制代码

系统部署

环境配置

  1. 安装必要的Python库:
  1. pip install opencv-python dlib face_recognition numpy flask pandas matplotlib
复制代码
  1. 准备学生人脸数据库:
  1. def register_new_student(student_id, name, image_path):
  2. # 加载图像
  3. image = face_recognition.load_image_file(image_path)
  4. # 检测人脸
  5. face_locations = face_recognition.face_locations(image)
  6. if len(face_locations) != 1:
  7. return False, "图像中没有检测到人脸或检测到多个人脸"
  8. # 提取人脸特征
  9. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  10. # 将特征存入数据库
  11. conn = sqlite3.connect('attendance.db')
  12. cursor = conn.cursor()
  13. cursor.execute("""
  14. INSERT INTO students (student_id, name, face_encoding)
  15. VALUES (?, ?, ?)
  16. """, (student_id, name, face_encoding.tobytes()))
  17. conn.commit()
  18. conn.close()
  19. return True, "学生注册成功"
复制代码
  1. 启动系统:
  1. python app.py
复制代码

硬件要求

  • 摄像头:支持720p或更高分辨率
  • 处理器:建议Intel Core i5或更高性能
  • 内存:至少8GB RAM
  • 存储:至少100GB可用空间(用于存储学生数据和考勤记录)

系统优化与扩展

性能优化

  1. 模型压缩:使用模型量化和剪枝技术减小模型体积,提高推理速度
  2. GPU加速:利用GPU进行并行计算,加快人脸检测和识别过程
  3. 批处理:同时处理多个人脸,减少模型加载和初始化时间

功能扩展

  1. 活体检测:防止照片欺骗,提高系统安全性
  2. 表情识别:分析学生表情,评估课堂专注度
  3. 移动端应用:开发移动应用,支持远程考勤
  4. 多模态融合:结合声纹识别等多种生物特征,提高识别准确率

安全与隐私保护

在实施人脸识别系统时,必须高度重视用户隐私和数据安全:

  1. 数据加密:对存储的人脸特征和个人信息进行加密
  2. 权限控制:严格控制系统访问权限,防止未授权访问
  3. 数据最小化:只收集和存储必要的个人信息
  4. 透明度:向用户明确说明数据收集和使用方式
  5. 合规性:确保系统符合相关法律法规要求

结论

基于深度学习的校园人脸识别考勤系统是人工智能技术在教育领域的一个典型应用。通过整合计算机视觉、深度学习和Web开发技术,我们构建了一个高效、准确的自动考勤系统,不仅大大提高了考勤效率,还为教育管理提供了数据支持。

随着深度学习技术的不断发展,人脸识别系统的准确率和性能将进一步提升,应用场景也将更加广泛。同时,我们也需要关注系统在实际应用中可能面临的挑战,如隐私保护、环境适应性等问题,不断优化和完善系统功能。

源代码

Directory Content Summary

Source Directory: ./face_attendance_system

Directory Structure

  1. face_attendance_system/
  2. app.py
  3. face_detection.py
  4. README.md
  5. requirements.txt
  6. database/
  7. db_setup.py
  8. init_db.py
  9. migrate.py
  10. models.py
  11. static/
  12. css/
  13. style.css
  14. js/
  15. main.js
  16. uploads/
  17. templates/
  18. attendance.html
  19. base.html
  20. dashboard.html
  21. edit_user.html
  22. face_recognition_attendance.html
  23. face_registration.html
  24. face_registration_admin.html
  25. index.html
  26. login.html
  27. register.html
  28. user_management.html
  29. webcam_registration.html
复制代码

File Contents

app.py

  1. import os
  2. import numpy as np
  3. import face_recognition
  4. import cv2
  5. from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
  6. from werkzeug.utils import secure_filename
  7. import base64
  8. from datetime import datetime
  9. import json
  10. import uuid
  11. import shutil
  12. # Import database models
  13. from database.models import User, FaceEncoding, Attendance
  14. from database.db_setup import init_database
  15. # Initialize the Flask application
  16. app = Flask(__name__)
  17. app.secret_key = 'your_secret_key_here' # Change this to a random secret key in production
  18. # Initialize database
  19. init_database()
  20. # Configure upload folder
  21. UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'uploads')
  22. if not os.path.exists(UPLOAD_FOLDER):
  23. os.makedirs(UPLOAD_FOLDER)
  24. app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
  25. app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size
  26. # Allowed file extensions
  27. ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
  28. def allowed_file(filename):
  29. """Check if file has allowed extension"""
  30. return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  31. @app.route('/')
  32. def index():
  33. """Home page route"""
  34. if 'user_id' in session:
  35. return redirect(url_for('dashboard'))
  36. return render_template('index.html')
  37. @app.route('/login', methods=['GET', 'POST'])
  38. def login():
  39. """Login route"""
  40. if request.method == 'POST':
  41. student_id = request.form.get('student_id')
  42. password = request.form.get('password')
  43. if not student_id or not password:
  44. flash('Please provide both student ID and password', 'danger')
  45. return render_template('login.html')
  46. user = User.authenticate(student_id, password)
  47. if user:
  48. session['user_id'] = user['id']
  49. session['student_id'] = user['student_id']
  50. session['name'] = user['name']
  51. flash(f'Welcome back, {user["name"]}!', 'success')
  52. return redirect(url_for('dashboard'))
  53. else:
  54. flash('Invalid student ID or password', 'danger')
  55. return render_template('login.html')
  56. @app.route('/register', methods=['GET', 'POST'])
  57. def register():
  58. """User registration route"""
  59. if request.method == 'POST':
  60. student_id = request.form.get('student_id')
  61. name = request.form.get('name')
  62. email = request.form.get('email')
  63. password = request.form.get('password')
  64. confirm_password = request.form.get('confirm_password')
  65. # Validate input
  66. if not all([student_id, name, email, password, confirm_password]):
  67. flash('Please fill in all fields', 'danger')
  68. return render_template('register.html')
  69. if password != confirm_password:
  70. flash('Passwords do not match', 'danger')
  71. return render_template('register.html')
  72. # Check if student ID already exists
  73. existing_user = User.get_user_by_student_id(student_id)
  74. if existing_user:
  75. flash('Student ID already registered', 'danger')
  76. return render_template('register.html')
  77. # Create user
  78. user_id = User.create_user(student_id, name, email, password)
  79. if user_id:
  80. flash('Registration successful! Please login.', 'success')
  81. return redirect(url_for('login'))
  82. else:
  83. flash('Registration failed. Please try again.', 'danger')
  84. return render_template('register.html')
  85. @app.route('/logout')
  86. def logout():
  87. """Logout route"""
  88. session.clear()
  89. flash('You have been logged out', 'info')
  90. return redirect(url_for('index'))
  91. @app.route('/dashboard')
  92. def dashboard():
  93. """User dashboard route"""
  94. if 'user_id' not in session:
  95. flash('Please login first', 'warning')
  96. return redirect(url_for('login'))
  97. user_id = session['user_id']
  98. user = User.get_user_by_id(user_id)
  99. # Get user's face encodings
  100. face_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)
  101. has_face_data = len(face_encodings) > 0
  102. # Get user's attendance records
  103. attendance_records = Attendance.get_attendance_by_user(user_id)
  104. return render_template('dashboard.html',
  105. user=user,
  106. has_face_data=has_face_data,
  107. attendance_records=attendance_records)
  108. @app.route('/face-registration', methods=['GET', 'POST'])
  109. def face_registration():
  110. """Face registration route"""
  111. if 'user_id' not in session:
  112. flash('Please login first', 'warning')
  113. return redirect(url_for('login'))
  114. if request.method == 'POST':
  115. # Check if the post request has the file part
  116. if 'face_image' not in request.files:
  117. flash('No file part', 'danger')
  118. return redirect(request.url)
  119. file = request.files['face_image']
  120. # If user does not select file, browser also
  121. # submit an empty part without filename
  122. if file.filename == '':
  123. flash('No selected file', 'danger')
  124. return redirect(request.url)
  125. if file and allowed_file(file.filename):
  126. # Generate a unique filename
  127. filename = secure_filename(f"{session['student_id']}_{uuid.uuid4().hex}.jpg")
  128. filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  129. file.save(filepath)
  130. # Process the image for face detection
  131. image = face_recognition.load_image_file(filepath)
  132. face_locations = face_recognition.face_locations(image)
  133. if not face_locations:
  134. os.remove(filepath) # Remove the file if no face is detected
  135. flash('No face detected in the image. Please try again.', 'danger')
  136. return redirect(request.url)
  137. if len(face_locations) > 1:
  138. os.remove(filepath) # Remove the file if multiple faces are detected
  139. flash('Multiple faces detected in the image. Please upload an image with only your face.', 'danger')
  140. return redirect(request.url)
  141. # Extract face encoding
  142. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  143. # Save face encoding to database
  144. encoding_id = FaceEncoding.save_face_encoding(session['user_id'], face_encoding)
  145. if encoding_id:
  146. flash('Face registered successfully!', 'success')
  147. return redirect(url_for('dashboard'))
  148. else:
  149. flash('Failed to register face. Please try again.', 'danger')
  150. else:
  151. flash('Invalid file type. Please upload a JPG, JPEG or PNG image.', 'danger')
  152. return render_template('face_registration.html')
  153. @app.route('/webcam-registration', methods=['GET', 'POST'])
  154. def webcam_registration():
  155. """Face registration using webcam"""
  156. if 'user_id' not in session:
  157. flash('Please login first', 'warning')
  158. return redirect(url_for('login'))
  159. if request.method == 'POST':
  160. # Get the base64 encoded image from the request
  161. image_data = request.form.get('image_data')
  162. if not image_data:
  163. return jsonify({'success': False, 'message': 'No image data received'})
  164. # Remove the data URL prefix
  165. image_data = image_data.split(',')[1]
  166. # Decode the base64 image
  167. image_bytes = base64.b64decode(image_data)
  168. # Generate a unique filename
  169. filename = f"{session['student_id']}_{uuid.uuid4().hex}.jpg"
  170. filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  171. # Save the image
  172. with open(filepath, 'wb') as f:
  173. f.write(image_bytes)
  174. # Process the image for face detection
  175. image = face_recognition.load_image_file(filepath)
  176. face_locations = face_recognition.face_locations(image)
  177. if not face_locations:
  178. os.remove(filepath) # Remove the file if no face is detected
  179. return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
  180. if len(face_locations) > 1:
  181. os.remove(filepath) # Remove the file if multiple faces are detected
  182. return jsonify({'success': False, 'message': 'Multiple faces detected in the image. Please ensure only your face is visible.'})
  183. # Extract face encoding
  184. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  185. # Save face encoding to database
  186. encoding_id = FaceEncoding.save_face_encoding(session['user_id'], face_encoding)
  187. if encoding_id:
  188. return jsonify({'success': True, 'message': 'Face registered successfully!'})
  189. else:
  190. os.remove(filepath)
  191. return jsonify({'success': False, 'message': 'Failed to register face. Please try again.'})
  192. return render_template('webcam_registration.html')
  193. @app.route('/webcam-registration-admin', methods=['POST'])
  194. def webcam_registration_admin():
  195. """Process webcam registration for face data"""
  196. if 'user_id' not in session:
  197. return jsonify({'success': False, 'message': 'Please login first'})
  198. # Get image data from form
  199. image_data = request.form.get('image_data')
  200. user_id = request.form.get('user_id')
  201. if not image_data:
  202. return jsonify({'success': False, 'message': 'No image data provided'})
  203. # Check if user_id is provided (for admin registration)
  204. if not user_id:
  205. user_id = session['user_id']
  206. # Get user data
  207. user = User.get_user_by_id(user_id)
  208. if not user:
  209. return jsonify({'success': False, 'message': 'User not found'})
  210. try:
  211. # Remove header from the base64 string
  212. image_data = image_data.split(',')[1]
  213. # Decode base64 string to image
  214. image_bytes = base64.b64decode(image_data)
  215. # Create a temporary file to save the image
  216. temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"temp_{uuid.uuid4().hex}.jpg")
  217. with open(temp_filepath, 'wb') as f:
  218. f.write(image_bytes)
  219. # Process the image for face detection
  220. image = face_recognition.load_image_file(temp_filepath)
  221. face_locations = face_recognition.face_locations(image)
  222. if not face_locations:
  223. os.remove(temp_filepath)
  224. return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
  225. if len(face_locations) > 1:
  226. os.remove(temp_filepath)
  227. return jsonify({'success': False, 'message': 'Multiple faces detected in the image. Please ensure only one face is visible.'})
  228. # Extract face encoding
  229. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  230. # Save face encoding to database
  231. encoding_id = FaceEncoding.save_face_encoding(user_id, face_encoding)
  232. if encoding_id:
  233. # Save the processed image with a proper filename
  234. final_filename = secure_filename(f"{user['student_id']}_{uuid.uuid4().hex}.jpg")
  235. final_filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
  236. shutil.copy(temp_filepath, final_filepath)
  237. # Remove temporary file
  238. os.remove(temp_filepath)
  239. return jsonify({'success': True, 'message': 'Face registered successfully!'})
  240. else:
  241. os.remove(temp_filepath)
  242. return jsonify({'success': False, 'message': 'Failed to register face. Please try again.'})
  243. except Exception as e:
  244. # Clean up if there was an error
  245. if os.path.exists(temp_filepath):
  246. os.remove(temp_filepath)
  247. return jsonify({'success': False, 'message': f'An error occurred: {str(e)}'})
  248. @app.route('/attendance', methods=['GET'])
  249. def attendance():
  250. """View attendance records"""
  251. if 'user_id' not in session:
  252. flash('Please login first', 'warning')
  253. return redirect(url_for('login'))
  254. date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
  255. attendance_records = Attendance.get_attendance_by_date(date)
  256. return render_template('attendance.html',
  257. attendance_records=attendance_records,
  258. selected_date=date)
  259. @app.route('/check-in', methods=['GET'])
  260. def check_in():
  261. """Manual check-in page"""
  262. if 'user_id' not in session:
  263. flash('Please login first', 'warning')
  264. return redirect(url_for('login'))
  265. return render_template('check_in.html')
  266. @app.route('/process-check-in', methods=['POST'])
  267. def process_check_in():
  268. """Process manual check-in"""
  269. if 'user_id' not in session:
  270. return jsonify({'success': False, 'message': 'Please login first'})
  271. user_id = session['user_id']
  272. # Record check-in
  273. attendance_id = Attendance.record_check_in(user_id)
  274. if attendance_id:
  275. return jsonify({'success': True, 'message': 'Check-in successful!'})
  276. else:
  277. return jsonify({'success': False, 'message': 'You have already checked in today'})
  278. @app.route('/check-out', methods=['POST'])
  279. def check_out():
  280. """Process check-out"""
  281. if 'user_id' not in session:
  282. return jsonify({'success': False, 'message': 'Please login first'})
  283. user_id = session['user_id']
  284. # Record check-out
  285. success = Attendance.record_check_out(user_id)
  286. if success:
  287. return jsonify({'success': True, 'message': 'Check-out successful!'})
  288. else:
  289. return jsonify({'success': False, 'message': 'No active check-in found for today'})
  290. @app.route('/face-recognition-attendance', methods=['GET'])
  291. def face_recognition_attendance():
  292. """Face recognition attendance page"""
  293. if 'user_id' not in session:
  294. flash('Please login first', 'warning')
  295. return redirect(url_for('login'))
  296. return render_template('face_recognition_attendance.html')
  297. @app.route('/process-face-attendance', methods=['POST'])
  298. def process_face_attendance():
  299. """Process face recognition attendance"""
  300. # Get the base64 encoded image from the request
  301. image_data = request.form.get('image_data')
  302. if not image_data:
  303. return jsonify({'success': False, 'message': 'No image data received'})
  304. # Remove the data URL prefix
  305. image_data = image_data.split(',')[1]
  306. # Decode the base64 image
  307. image_bytes = base64.b64decode(image_data)
  308. # Generate a temporary filename
  309. temp_filename = f"temp_{uuid.uuid4().hex}.jpg"
  310. temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
  311. # Save the image
  312. with open(temp_filepath, 'wb') as f:
  313. f.write(image_bytes)
  314. try:
  315. # Process the image for face detection
  316. image = face_recognition.load_image_file(temp_filepath)
  317. face_locations = face_recognition.face_locations(image)
  318. if not face_locations:
  319. return jsonify({'success': False, 'message': 'No face detected in the image. Please try again.'})
  320. if len(face_locations) > 1:
  321. return jsonify({'success': False, 'message': 'Multiple faces detected. Please ensure only one person is in the frame.'})
  322. # Extract face encoding
  323. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  324. # Get all face encodings from database
  325. all_encodings = FaceEncoding.get_all_face_encodings()
  326. if not all_encodings:
  327. return jsonify({'success': False, 'message': 'No registered faces found in the database.'})
  328. # Compare with known face encodings
  329. known_encodings = [enc['encoding'] for enc in all_encodings]
  330. matches = face_recognition.compare_faces(known_encodings, face_encoding)
  331. if True in matches:
  332. # Find the matching index
  333. match_index = matches.index(True)
  334. matched_user = all_encodings[match_index]
  335. # Record attendance
  336. attendance_id = Attendance.record_check_in(matched_user['user_id'])
  337. if attendance_id:
  338. return jsonify({
  339. 'success': True,
  340. 'message': f'Welcome, {matched_user["name"]}! Your attendance has been recorded.',
  341. 'user': {
  342. 'name': matched_user['name'],
  343. 'student_id': matched_user['student_id']
  344. }
  345. })
  346. else:
  347. return jsonify({
  348. 'success': True,
  349. 'message': f'Welcome back, {matched_user["name"]}! You have already checked in today.',
  350. 'user': {
  351. 'name': matched_user['name'],
  352. 'student_id': matched_user['student_id']
  353. }
  354. })
  355. else:
  356. return jsonify({'success': False, 'message': 'Face not recognized. Please register your face or try again.'})
  357. finally:
  358. # Clean up the temporary file
  359. if os.path.exists(temp_filepath):
  360. os.remove(temp_filepath)
  361. @app.route('/user-management', methods=['GET'])
  362. def user_management():
  363. """User management route for admins"""
  364. if 'user_id' not in session:
  365. flash('Please login first', 'warning')
  366. return redirect(url_for('login'))
  367. # Check if user is admin (in a real app, you would check user role)
  368. # For demo purposes, we'll allow all logged-in users to access this page
  369. # Get search query and pagination parameters
  370. search_query = request.args.get('search', '')
  371. page = int(request.args.get('page', 1))
  372. per_page = 10
  373. # Get users based on search query
  374. if search_query:
  375. users = User.search_users(search_query, page, per_page)
  376. total_users = User.count_search_results(search_query)
  377. else:
  378. users = User.get_all_users(page, per_page)
  379. total_users = User.count_all_users()
  380. # Calculate total pages
  381. total_pages = (total_users + per_page - 1) // per_page
  382. # Check if each user has face data
  383. for user in users:
  384. face_encodings = FaceEncoding.get_face_encodings_by_user_id(user['id'])
  385. user['has_face_data'] = len(face_encodings) > 0
  386. return render_template('user_management.html',
  387. users=users,
  388. search_query=search_query,
  389. current_page=page,
  390. total_pages=total_pages)
  391. @app.route('/edit-user/<int:user_id>', methods=['GET', 'POST'])
  392. def edit_user(user_id):
  393. """Edit user route"""
  394. if 'user_id' not in session:
  395. flash('Please login first', 'warning')
  396. return redirect(url_for('login'))
  397. # Check if user is admin (in a real app, you would check user role)
  398. # For demo purposes, we'll allow all logged-in users to access this page
  399. # Get user data
  400. user = User.get_user_by_id(user_id)
  401. if not user:
  402. flash('User not found', 'danger')
  403. return redirect(url_for('user_management'))
  404. # Check if user has face data
  405. face_encodings = FaceEncoding.get_face_encodings_by_user_id(user_id)
  406. user['has_face_data'] = len(face_encodings) > 0
  407. if request.method == 'POST':
  408. student_id = request.form.get('student_id')
  409. name = request.form.get('name')
  410. email = request.form.get('email')
  411. password = request.form.get('password')
  412. role = request.form.get('role')
  413. is_active = 'is_active' in request.form
  414. # Update user
  415. success = User.update_user(user_id, student_id, name, email, password, role, is_active)
  416. if success:
  417. flash('User updated successfully', 'success')
  418. return redirect(url_for('user_management'))
  419. else:
  420. flash('Failed to update user', 'danger')
  421. return render_template('edit_user.html', user=user)
  422. @app.route('/delete-user/<int:user_id>', methods=['POST'])
  423. def delete_user(user_id):
  424. """Delete user route"""
  425. if 'user_id' not in session:
  426. flash('Please login first', 'warning')
  427. return redirect(url_for('login'))
  428. # Check if user is admin (in a real app, you would check user role)
  429. # For demo purposes, we'll allow all logged-in users to access this page
  430. # Delete user
  431. success = User.delete_user(user_id)
  432. if success:
  433. flash('User deleted successfully', 'success')
  434. else:
  435. flash('Failed to delete user', 'danger')
  436. return redirect(url_for('user_management'))
  437. @app.route('/reset-face-data/<int:user_id>', methods=['POST'])
  438. def reset_face_data(user_id):
  439. """Reset user's face data"""
  440. if 'user_id' not in session:
  441. flash('Please login first', 'warning')
  442. return redirect(url_for('login'))
  443. # Check if user is admin (in a real app, you would check user role)
  444. # For demo purposes, we'll allow all logged-in users to access this page
  445. # Delete face encodings
  446. success = FaceEncoding.delete_face_encodings_by_user_id(user_id)
  447. if success:
  448. flash('Face data reset successfully', 'success')
  449. else:
  450. flash('Failed to reset face data', 'danger')
  451. return redirect(url_for('edit_user', user_id=user_id))
  452. @app.route('/face-registration-admin/<int:user_id>', methods=['GET', 'POST'])
  453. def face_registration_admin(user_id):
  454. """Face registration for admin to register user's face"""
  455. if 'user_id' not in session:
  456. flash('Please login first', 'warning')
  457. return redirect(url_for('login'))
  458. # Check if user is admin (in a real app, you would check user role)
  459. # For demo purposes, we'll allow all logged-in users to access this page
  460. # Get user data
  461. user = User.get_user_by_id(user_id)
  462. if not user:
  463. flash('User not found', 'danger')
  464. return redirect(url_for('user_management'))
  465. if request.method == 'POST':
  466. # Check if the post request has the file part
  467. if 'face_image' not in request.files:
  468. flash('No file part', 'danger')
  469. return redirect(request.url)
  470. file = request.files['face_image']
  471. # If user does not select file, browser also
  472. # submit an empty part without filename
  473. if file.filename == '':
  474. flash('No selected file', 'danger')
  475. return redirect(request.url)
  476. if file and allowed_file(file.filename):
  477. # Generate a unique filename
  478. filename = secure_filename(f"{user['student_id']}_{uuid.uuid4().hex}.jpg")
  479. filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  480. file.save(filepath)
  481. # Process the image for face detection
  482. image = face_recognition.load_image_file(filepath)
  483. face_locations = face_recognition.face_locations(image)
  484. if not face_locations:
  485. os.remove(filepath) # Remove the file if no face is detected
  486. flash('No face detected in the image. Please try again.', 'danger')
  487. return redirect(request.url)
  488. if len(face_locations) > 1:
  489. os.remove(filepath) # Remove the file if multiple faces are detected
  490. flash('Multiple faces detected in the image. Please upload an image with only one face.', 'danger')
  491. return redirect(request.url)
  492. # Extract face encoding
  493. face_encoding = face_recognition.face_encodings(image, face_locations)[0]
  494. # Save face encoding to database
  495. encoding_id = FaceEncoding.save_face_encoding(user_id, face_encoding)
  496. if encoding_id:
  497. flash('Face registered successfully!', 'success')
  498. return redirect(url_for('edit_user', user_id=user_id))
  499. else:
  500. flash('Failed to register face. Please try again.', 'danger')
  501. else:
  502. flash('Invalid file type. Please upload a JPG, JPEG or PNG image.', 'danger')
  503. return render_template('face_registration_admin.html', user=user)
  504. @app.route('/detect-face', methods=['POST'])
  505. def detect_face():
  506. """检测人脸API"""
  507. if 'image_data' not in request.form:
  508. return jsonify({'success': False, 'message': '未提供图像数据'})
  509. # 获取图像数据
  510. image_data = request.form.get('image_data')
  511. try:
  512. # 移除base64头部
  513. if ',' in image_data:
  514. image_data = image_data.split(',')[1]
  515. # 解码base64图像
  516. image_bytes = base64.b64decode(image_data)
  517. nparr = np.frombuffer(image_bytes, np.uint8)
  518. image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
  519. # 转换为RGB(OpenCV使用BGR)
  520. rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  521. # 检测人脸
  522. face_locations = face_recognition.face_locations(rgb_image)
  523. return jsonify({
  524. 'success': True,
  525. 'message': '人脸检测完成',
  526. 'face_count': len(face_locations)
  527. })
  528. except Exception as e:
  529. app.logger.error(f"人脸检测错误: {str(e)}")
  530. return jsonify({'success': False, 'message': f'处理图像时出错: {str(e)}'})
  531. @app.route('/recognize-face', methods=['POST'])
  532. def recognize_face():
  533. """识别人脸API"""
  534. if 'image_data' not in request.form:
  535. return jsonify({'success': False, 'message': '未提供图像数据'})
  536. # 获取图像数据
  537. image_data = request.form.get('image_data')
  538. try:
  539. # 移除base64头部
  540. if ',' in image_data:
  541. image_data = image_data.split(',')[1]
  542. # 解码base64图像
  543. image_bytes = base64.b64decode(image_data)
  544. nparr = np.frombuffer(image_bytes, np.uint8)
  545. image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
  546. # 转换为RGB(OpenCV使用BGR)
  547. rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  548. # 检测人脸
  549. face_locations = face_recognition.face_locations(rgb_image)
  550. if not face_locations:
  551. return jsonify({'success': False, 'message': '未检测到人脸,请确保脸部清晰可见'})
  552. if len(face_locations) > 1:
  553. return jsonify({'success': False, 'message': '检测到多个人脸,请确保画面中只有一个人脸'})
  554. # 提取人脸特征
  555. face_encoding = face_recognition.face_encodings(rgb_image, face_locations)[0]
  556. # 加载所有已知人脸编码
  557. known_faces = FaceEncoding.get_all_face_encodings()
  558. if not known_faces:
  559. return jsonify({'success': False, 'message': '数据库中没有注册的人脸'})
  560. # 比较人脸
  561. known_encodings = [face['encoding'] for face in known_faces]
  562. matches = face_recognition.compare_faces(known_encodings, face_encoding)
  563. face_distances = face_recognition.face_distance(known_encodings, face_encoding)
  564. if True in matches:
  565. # 找到最佳匹配
  566. best_match_index = np.argmin(face_distances)
  567. confidence = 1 - face_distances[best_match_index]
  568. if confidence >= 0.6: # 置信度阈值
  569. matched_user = known_faces[best_match_index]
  570. # 返回识别结果
  571. return jsonify({
  572. 'success': True,
  573. 'message': f'成功识别为 {matched_user["name"]}',
  574. 'user': {
  575. 'user_id': matched_user['user_id'],
  576. 'student_id': matched_user['student_id'],
  577. 'name': matched_user['name']
  578. },
  579. 'confidence': float(confidence)
  580. })
  581. else:
  582. return jsonify({'success': False, 'message': '识别置信度过低,请重新尝试'})
  583. else:
  584. return jsonify({'success': False, 'message': '无法识别您的身份,请确保您已注册人脸数据'})
  585. except Exception as e:
  586. app.logger.error(f"人脸识别错误: {str(e)}")
  587. return jsonify({'success': False, 'message': f'处理图像时出错: {str(e)}'})
  588. @app.route('/record-attendance', methods=['POST'])
  589. def record_attendance():
  590. """记录考勤API"""
  591. if 'user_id' not in session:
  592. return jsonify({'success': False, 'message': '请先登录'})
  593. # 获取请求数据
  594. data = request.get_json()
  595. if not data or 'user_id' not in data:
  596. return jsonify({'success': False, 'message': '无效的请求数据'})
  597. user_id = data.get('user_id')
  598. confidence = data.get('confidence', 0)
  599. # 验证用户身份(确保当前登录用户只能为自己签到)
  600. if int(session['user_id']) != int(user_id) and session.get('role') != 'admin':
  601. return jsonify({'success': False, 'message': '无权为其他用户签到'})
  602. # 检查是否已经签到
  603. today_attendance = Attendance.get_today_attendance(user_id)
  604. if today_attendance:
  605. return jsonify({'success': False, 'message': '今天已经签到,无需重复签到'})
  606. # 记录考勤
  607. attendance_id = Attendance.record_check_in(user_id)
  608. if attendance_id:
  609. # 获取用户信息
  610. user = User.get_user_by_id(user_id)
  611. return jsonify({
  612. 'success': True,
  613. 'message': f'签到成功!欢迎 {user["name"]}',
  614. 'attendance_id': attendance_id,
  615. 'check_in_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  616. })
  617. else:
  618. return jsonify({'success': False, 'message': '签到失败,请稍后重试'})
  619. @app.route('/get-recent-attendance', methods=['GET'])
  620. def get_recent_attendance():
  621. """获取最近考勤记录API"""
  622. if 'user_id' not in session:
  623. return jsonify({'success': False, 'message': '请先登录'})
  624. # 获取最近的考勤记录(默认10条)
  625. limit = request.args.get('limit', 10, type=int)
  626. records = Attendance.get_recent_attendance(limit)
  627. return jsonify({
  628. 'success': True,
  629. 'records': records
  630. })
  631. if __name__ == '__main__':
  632. app.run(debug=True)
复制代码

face_detection.py

  1. import cv2
  2. import face_recognition
  3. import numpy as np
  4. import os
  5. import pickle
  6. from datetime import datetime
  7. import time
  8. import logging
  9. # 配置日志
  10. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  11. logger = logging.getLogger(__name__)
  12. class FaceDetector:
  13. """人脸检测与识别类"""
  14. def __init__(self, model_type='hog', tolerance=0.6, known_faces=None):
  15. """
  16. 初始化人脸检测器
  17. 参数:
  18. model_type (str): 使用的模型类型,'hog'(CPU)或'cnn'(GPU)
  19. tolerance (float): 人脸匹配的容差值,越小越严格
  20. known_faces (list): 已知人脸编码和对应用户信息的列表
  21. """
  22. self.model_type = model_type
  23. self.tolerance = tolerance
  24. self.known_faces = known_faces or []
  25. logger.info(f"人脸检测器初始化完成,使用{model_type}模型,容差值为{tolerance}")
  26. def load_known_faces(self, known_faces):
  27. """
  28. 加载已知人脸数据
  29. 参数:
  30. known_faces (list): 包含人脸编码和用户信息的列表
  31. """
  32. self.known_faces = known_faces
  33. logger.info(f"已加载{len(known_faces)}个已知人脸")
  34. def detect_faces(self, image):
  35. """
  36. 检测图像中的人脸位置
  37. 参数:
  38. image: 图像数据,可以是文件路径或图像数组
  39. 返回:
  40. list: 人脸位置列表,每个位置为(top, right, bottom, left)
  41. """
  42. # 如果是文件路径,加载图像
  43. if isinstance(image, str):
  44. if not os.path.exists(image):
  45. logger.error(f"图像文件不存在: {image}")
  46. return []
  47. image = face_recognition.load_image_file(image)
  48. # 检测人脸位置
  49. start_time = time.time()
  50. face_locations = face_recognition.face_locations(image, model=self.model_type)
  51. detection_time = time.time() - start_time
  52. logger.info(f"检测到{len(face_locations)}个人脸,耗时{detection_time:.4f}秒")
  53. return face_locations
  54. def encode_faces(self, image, face_locations=None):
  55. """
  56. 提取图像中人脸的编码特征
  57. 参数:
  58. image: 图像数据,可以是文件路径或图像数组
  59. face_locations: 可选,人脸位置列表
  60. 返回:
  61. list: 人脸编码特征列表
  62. """
  63. # 如果是文件路径,加载图像
  64. if isinstance(image, str):
  65. if not os.path.exists(image):
  66. logger.error(f"图像文件不存在: {image}")
  67. return []
  68. image = face_recognition.load_image_file(image)
  69. # 如果没有提供人脸位置,先检测人脸
  70. if face_locations is None:
  71. face_locations = self.detect_faces(image)
  72. if not face_locations:
  73. logger.warning("未检测到人脸,无法提取特征")
  74. return []
  75. # 提取人脸编码特征
  76. start_time = time.time()
  77. face_encodings = face_recognition.face_encodings(image, face_locations)
  78. encoding_time = time.time() - start_time
  79. logger.info(f"提取了{len(face_encodings)}个人脸特征,耗时{encoding_time:.4f}秒")
  80. return face_encodings
  81. def recognize_faces(self, face_encodings):
  82. """
  83. 识别人脸,匹配已知人脸
  84. 参数:
  85. face_encodings: 待识别的人脸编码特征列表
  86. 返回:
  87. list: 识别结果列表,每个结果为(user_info, confidence)或(None, 0)
  88. """
  89. if not self.known_faces:
  90. logger.warning("没有已知人脸数据,无法进行识别")
  91. return [(None, 0) for _ in face_encodings]
  92. if not face_encodings:
  93. logger.warning("没有提供人脸特征,无法进行识别")
  94. return []
  95. results = []
  96. # 提取已知人脸的编码和用户信息
  97. known_encodings = [face['encoding'] for face in self.known_faces]
  98. for face_encoding in face_encodings:
  99. # 计算与已知人脸的距离
  100. face_distances = face_recognition.face_distance(known_encodings, face_encoding)
  101. if len(face_distances) > 0:
  102. # 找到最小距离及其索引
  103. best_match_index = np.argmin(face_distances)
  104. best_match_distance = face_distances[best_match_index]
  105. # 计算置信度(1 - 距离)
  106. confidence = 1 - best_match_distance
  107. # 如果距离小于容差,认为匹配成功
  108. if best_match_distance <= self.tolerance:
  109. user_info = {
  110. 'user_id': self.known_faces[best_match_index]['user_id'],
  111. 'student_id': self.known_faces[best_match_index]['student_id'],
  112. 'name': self.known_faces[best_match_index]['name']
  113. }
  114. results.append((user_info, confidence))
  115. logger.info(f"识别到用户: {user_info['name']},置信度: {confidence:.4f}")
  116. else:
  117. results.append((None, confidence))
  118. logger.info(f"未能识别人脸,最佳匹配置信度: {confidence:.4f},低于阈值")
  119. else:
  120. results.append((None, 0))
  121. logger.warning("没有已知人脸数据进行比较")
  122. return results
  123. def process_image(self, image):
  124. """
  125. 处理图像,检测、编码并识别人脸
  126. 参数:
  127. image: 图像数据,可以是文件路径或图像数组
  128. 返回:
  129. tuple: (face_locations, recognition_results)
  130. """
  131. # 检测人脸
  132. face_locations = self.detect_faces(image)
  133. if not face_locations:
  134. return [], []
  135. # 提取人脸编码
  136. face_encodings = self.encode_faces(image, face_locations)
  137. # 识别人脸
  138. recognition_results = self.recognize_faces(face_encodings)
  139. return face_locations, recognition_results
  140. def process_video_frame(self, frame):
  141. """
  142. 处理视频帧,检测、编码并识别人脸
  143. 参数:
  144. frame: 视频帧图像数组
  145. 返回:
  146. tuple: (face_locations, recognition_results)
  147. """
  148. # 将BGR格式转换为RGB格式(OpenCV使用BGR,face_recognition使用RGB)
  149. rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  150. # 为提高性能,可以缩小图像
  151. small_frame = cv2.resize(rgb_frame, (0, 0), fx=0.25, fy=0.25)
  152. # 检测人脸
  153. face_locations = self.detect_faces(small_frame)
  154. # 调整人脸位置坐标到原始尺寸
  155. original_face_locations = []
  156. for top, right, bottom, left in face_locations:
  157. original_face_locations.append(
  158. (top * 4, right * 4, bottom * 4, left * 4)
  159. )
  160. if not original_face_locations:
  161. return [], []
  162. # 提取人脸编码(使用原始尺寸的图像)
  163. face_encodings = self.encode_faces(rgb_frame, original_face_locations)
  164. # 识别人脸
  165. recognition_results = self.recognize_faces(face_encodings)
  166. return original_face_locations, recognition_results
  167. def draw_results(self, image, face_locations, recognition_results):
  168. """
  169. 在图像上绘制人脸检测和识别结果
  170. 参数:
  171. image: 图像数组
  172. face_locations: 人脸位置列表
  173. recognition_results: 识别结果列表
  174. 返回:
  175. image: 绘制结果后的图像
  176. """
  177. # 复制图像,避免修改原图
  178. result_image = image.copy()
  179. # 遍历每个人脸
  180. for i, (top, right, bottom, left) in enumerate(face_locations):
  181. if i < len(recognition_results):
  182. user_info, confidence = recognition_results[i]
  183. # 绘制人脸框
  184. if user_info: # 识别成功
  185. color = (0, 255, 0) # 绿色
  186. else: # 识别失败
  187. color = (0, 0, 255) # 红色
  188. cv2.rectangle(result_image, (left, top), (right, bottom), color, 2)
  189. # 绘制文本背景
  190. cv2.rectangle(result_image, (left, bottom - 35), (right, bottom), color, cv2.FILLED)
  191. # 绘制文本
  192. if user_info:
  193. text = f"{user_info['name']} ({confidence:.2f})"
  194. else:
  195. text = f"Unknown ({confidence:.2f})"
  196. cv2.putText(result_image, text, (left + 6, bottom - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
  197. return result_image
  198. @staticmethod
  199. def save_face_image(image, face_location, output_path):
  200. """
  201. 保存人脸图像
  202. 参数:
  203. image: 图像数组
  204. face_location: 人脸位置 (top, right, bottom, left)
  205. output_path: 输出文件路径
  206. 返回:
  207. bool: 是否保存成功
  208. """
  209. try:
  210. top, right, bottom, left = face_location
  211. # 扩大人脸区域,包含更多背景
  212. height, width = image.shape[:2]
  213. margin = int((bottom - top) * 0.5) # 使用人脸高度的50%作为边距
  214. # 确保不超出图像边界
  215. top = max(0, top - margin)
  216. bottom = min(height, bottom + margin)
  217. left = max(0, left - margin)
  218. right = min(width, right + margin)
  219. # 裁剪人脸区域
  220. face_image = image[top:bottom, left:right]
  221. # 保存图像
  222. cv2.imwrite(output_path, face_image)
  223. logger.info(f"人脸图像已保存到: {output_path}")
  224. return True
  225. except Exception as e:
  226. logger.error(f"保存人脸图像失败: {e}")
  227. return False
  228. def test_face_detector():
  229. """测试人脸检测器功能"""
  230. # 创建人脸检测器
  231. detector = FaceDetector()
  232. # 测试图像路径
  233. test_image_path = "test_image.jpg"
  234. # 检测人脸
  235. face_locations = detector.detect_faces(test_image_path)
  236. print(f"检测到 {len(face_locations)} 个人脸")
  237. # 提取人脸编码
  238. face_encodings = detector.encode_faces(test_image_path, face_locations)
  239. print(f"提取了 {len(face_encodings)} 个人脸特征")
  240. # 加载图像并绘制结果
  241. image = cv2.imread(test_image_path)
  242. result_image = detector.draw_results(image, face_locations, [(None, 0.5) for _ in face_locations])
  243. # 显示结果
  244. cv2.imshow("Face Detection Results", result_image)
  245. cv2.waitKey(0)
  246. cv2.destroyAllWindows()
  247. if __name__ == "__main__":
  248. test_face_detector()
复制代码

README.md

  1. # 校园人脸识别考勤系统
  2. 基于深度学习的校园人脸识别考勤系统,使用Python、Flask、OpenCV和face_recognition库开发。
  3. ## 功能特点
  4. - 用户管理:注册、登录、编辑和删除用户
  5. - 人脸识别:通过摄像头或上传图片进行人脸识别
  6. - 考勤管理:记录和查询考勤信息
  7. - 课程管理:创建课程和管理课程考勤
  8. - 权限控制:区分管理员和普通用户权限
  9. ## 技术栈
  10. - **后端**:Python、Flask
  11. - **前端**:HTML、CSS、JavaScript、Bootstrap 5
  12. - **数据库**:SQLite
  13. - **人脸识别**:face_recognition、OpenCV
  14. - **其他**:NumPy、Pickle
  15. ## 安装指南
  16. 1. 克隆仓库
  17. ```bash
  18. git clone https://github.com/yourusername/face-attendance-system.git
  19. cd face-attendance-system
复制代码
  1. 创建虚拟环境
  1. python -m venv venv
  2. source venv/bin/activate # Windows: venv\Scripts\activate
复制代码
  1. 安装依赖
  1. pip install -r requirements.txt
复制代码
  1. 初始化数据库
  1. python database/init_db.py
复制代码
  1. 运行应用
  1. python app.py
复制代码
  1. 访问应用
    在浏览器中访问 http://localhost:5000

系统要求

  • Python 3.7+
  • 摄像头(用于人脸识别)
  • 现代浏览器(Chrome、Firefox、Edge等)

默认管理员账户

  • 学号:admin
  • 密码:admin123

项目结构

  1. face_attendance_system/
  2. ├── app.py # 主应用入口
  3. ├── face_detection.py # 人脸检测和识别模块
  4. ├── requirements.txt # 项目依赖
  5. ├── README.md # 项目说明
  6. ├── database/ # 数据库相关
  7. │ ├── init_db.py # 数据库初始化
  8. │ ├── migrate.py # 数据库迁移
  9. │ └── models.py # 数据模型
  10. ├── static/ # 静态资源
  11. │ ├── css/ # CSS样式
  12. │ ├── js/ # JavaScript脚本
  13. │ └── uploads/ # 上传文件存储
  14. │ └── faces/ # 人脸图像存储
  15. └── templates/ # HTML模板
  16. ├── base.html # 基础模板
  17. ├── login.html # 登录页面
  18. ├── register.html # 注册页面
  19. ├── user_management.html # 用户管理页面
  20. ├── edit_user.html # 编辑用户页面
  21. ├── face_registration_admin.html # 管理员人脸注册页面
  22. ├── webcam_registration.html # 摄像头人脸注册页面
  23. └── face_recognition_attendance.html # 人脸识别考勤页面
复制代码

许可证

MIT License

  1. ### requirements.txt
  2. ```text/plain
  3. Flask==2.0.1
  4. Werkzeug==2.0.1
  5. Jinja2==3.0.1
  6. itsdangerous==2.0.1
  7. MarkupSafe==2.0.1
  8. numpy==1.21.0
  9. opencv-python==4.5.3.56
  10. face-recognition==1.3.0
  11. face-recognition-models==0.3.0
  12. dlib==19.22.1
  13. Pillow==8.3.1
复制代码

database\db_setup.py

  1. import sqlite3
  2. import os
  3. # Database directory
  4. DB_DIR = os.path.dirname(os.path.abspath(__file__))
  5. DB_PATH = os.path.join(DB_DIR, 'attendance.db')
  6. def init_database():
  7. """Initialize the database with necessary tables"""
  8. conn = sqlite3.connect(DB_PATH)
  9. cursor = conn.cursor()
  10. # Create users table
  11. cursor.execute('''
  12. CREATE TABLE IF NOT EXISTS users (
  13. id INTEGER PRIMARY KEY AUTOINCREMENT,
  14. student_id TEXT UNIQUE NOT NULL,
  15. name TEXT NOT NULL,
  16. email TEXT UNIQUE,
  17. password TEXT NOT NULL,
  18. registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  19. )
  20. ''')
  21. # Create face_encodings table
  22. cursor.execute('''
  23. CREATE TABLE IF NOT EXISTS face_encodings (
  24. id INTEGER PRIMARY KEY AUTOINCREMENT,
  25. user_id INTEGER NOT NULL,
  26. encoding BLOB NOT NULL,
  27. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  28. FOREIGN KEY (user_id) REFERENCES users (id)
  29. )
  30. ''')
  31. # Create attendance table
  32. cursor.execute('''
  33. CREATE TABLE IF NOT EXISTS attendance (
  34. id INTEGER PRIMARY KEY AUTOINCREMENT,
  35. user_id INTEGER NOT NULL,
  36. check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  37. check_out_time TIMESTAMP,
  38. date TEXT,
  39. FOREIGN KEY (user_id) REFERENCES users (id)
  40. )
  41. ''')
  42. conn.commit()
  43. conn.close()
  44. print("Database initialized successfully!")
  45. if __name__ == "__main__":
  46. init_database()
复制代码

database\init_db.py

  1. import sqlite3
  2. import os
  3. # Database path
  4. DB_DIR = os.path.dirname(os.path.abspath(__file__))
  5. DB_PATH = os.path.join(DB_DIR, 'attendance.db')
  6. def init_database():
  7. """Initialize database with required tables"""
  8. print("Initializing database...")
  9. # Connect to database
  10. conn = sqlite3.connect(DB_PATH)
  11. cursor = conn.cursor()
  12. try:
  13. # Create users table
  14. cursor.execute('''
  15. CREATE TABLE IF NOT EXISTS users (
  16. id INTEGER PRIMARY KEY AUTOINCREMENT,
  17. student_id TEXT UNIQUE NOT NULL,
  18. name TEXT NOT NULL,
  19. email TEXT NOT NULL,
  20. password TEXT NOT NULL,
  21. registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  22. role TEXT DEFAULT 'student',
  23. is_active INTEGER DEFAULT 1
  24. )
  25. ''')
  26. # Create face_encodings table
  27. cursor.execute('''
  28. CREATE TABLE IF NOT EXISTS face_encodings (
  29. id INTEGER PRIMARY KEY AUTOINCREMENT,
  30. user_id INTEGER NOT NULL,
  31. encoding BLOB NOT NULL,
  32. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  33. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
  34. )
  35. ''')
  36. # Create attendance table
  37. cursor.execute('''
  38. CREATE TABLE IF NOT EXISTS attendance (
  39. id INTEGER PRIMARY KEY AUTOINCREMENT,
  40. user_id INTEGER NOT NULL,
  41. check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  42. check_out_time TIMESTAMP,
  43. status TEXT DEFAULT 'present',
  44. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
  45. )
  46. ''')
  47. # Create courses table
  48. cursor.execute('''
  49. CREATE TABLE IF NOT EXISTS courses (
  50. id INTEGER PRIMARY KEY AUTOINCREMENT,
  51. course_code TEXT UNIQUE NOT NULL,
  52. course_name TEXT NOT NULL,
  53. instructor TEXT NOT NULL,
  54. schedule TEXT,
  55. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  56. )
  57. ''')
  58. # Create course_enrollments table
  59. cursor.execute('''
  60. CREATE TABLE IF NOT EXISTS course_enrollments (
  61. id INTEGER PRIMARY KEY AUTOINCREMENT,
  62. course_id INTEGER NOT NULL,
  63. user_id INTEGER NOT NULL,
  64. enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  65. FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE,
  66. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
  67. UNIQUE(course_id, user_id)
  68. )
  69. ''')
  70. # Create course_attendance table
  71. cursor.execute('''
  72. CREATE TABLE IF NOT EXISTS course_attendance (
  73. id INTEGER PRIMARY KEY AUTOINCREMENT,
  74. course_id INTEGER NOT NULL,
  75. user_id INTEGER NOT NULL,
  76. attendance_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  77. status TEXT DEFAULT 'present',
  78. FOREIGN KEY (course_id) REFERENCES courses (id) ON DELETE CASCADE,
  79. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
  80. )
  81. ''')
  82. # Create admin user if not exists
  83. cursor.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
  84. if not cursor.fetchone():
  85. import hashlib
  86. admin_password = hashlib.sha256('admin123'.encode()).hexdigest()
  87. cursor.execute('''
  88. INSERT INTO users (student_id, name, email, password, role)
  89. VALUES (?, ?, ?, ?, ?)
  90. ''', ('admin', 'System Administrator', 'admin@example.com', admin_password, 'admin'))
  91. print("Created default admin user (student_id: admin, password: admin123)")
  92. conn.commit()
  93. print("Database initialized successfully.")
  94. except Exception as e:
  95. print(f"Error during initialization: {e}")
  96. conn.rollback()
  97. finally:
  98. conn.close()
  99. if __name__ == '__main__':
  100. init_database()
复制代码

database\migrate.py

  1. import sqlite3
  2. import os
  3. import sys
  4. # Database path
  5. DB_DIR = os.path.dirname(os.path.abspath(__file__))
  6. DB_PATH = os.path.join(DB_DIR, 'attendance.db')
  7. def check_column_exists(cursor, table_name, column_name):
  8. """Check if a column exists in a table"""
  9. cursor.execute(f"PRAGMA table_info({table_name})")
  10. columns = cursor.fetchall()
  11. for column in columns:
  12. if column[1] == column_name:
  13. return True
  14. return False
  15. def migrate_database():
  16. """Migrate database to latest schema"""
  17. print("Starting database migration...")
  18. # Connect to database
  19. conn = sqlite3.connect(DB_PATH)
  20. cursor = conn.cursor()
  21. try:
  22. # Check if database exists
  23. cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
  24. if not cursor.fetchone():
  25. print("Database not initialized. Please run init_db.py first.")
  26. conn.close()
  27. sys.exit(1)
  28. # Add role column to users table if it doesn't exist
  29. if not check_column_exists(cursor, 'users', 'role'):
  30. print("Adding 'role' column to users table...")
  31. cursor.execute("ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'student'")
  32. conn.commit()
  33. print("Added 'role' column to users table.")
  34. # Add is_active column to users table if it doesn't exist
  35. if not check_column_exists(cursor, 'users', 'is_active'):
  36. print("Adding 'is_active' column to users table...")
  37. cursor.execute("ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1")
  38. conn.commit()
  39. print("Added 'is_active' column to users table.")
  40. # Check if face_encodings table has the correct schema
  41. cursor.execute("PRAGMA table_info(face_encodings)")
  42. columns = cursor.fetchall()
  43. encoding_column_type = None
  44. for column in columns:
  45. if column[1] == 'encoding':
  46. encoding_column_type = column[2]
  47. break
  48. # If encoding column is not BLOB, we need to recreate the table
  49. if encoding_column_type != 'BLOB':
  50. print("Updating face_encodings table schema...")
  51. # Create a backup of the face_encodings table
  52. cursor.execute("CREATE TABLE IF NOT EXISTS face_encodings_backup AS SELECT * FROM face_encodings")
  53. # Drop the original table
  54. cursor.execute("DROP TABLE face_encodings")
  55. # Create the table with the correct schema
  56. cursor.execute('''
  57. CREATE TABLE face_encodings (
  58. id INTEGER PRIMARY KEY AUTOINCREMENT,
  59. user_id INTEGER NOT NULL,
  60. encoding BLOB NOT NULL,
  61. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  62. FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
  63. )
  64. ''')
  65. # Note: We can't restore the data because the encoding format has changed
  66. # from numpy array bytes to pickle serialized data
  67. print("Updated face_encodings table schema. Note: Previous face encodings have been backed up but not restored.")
  68. print("Users will need to re-register their faces.")
  69. print("Database migration completed successfully.")
  70. except Exception as e:
  71. print(f"Error during migration: {e}")
  72. conn.rollback()
  73. finally:
  74. conn.close()
  75. if __name__ == '__main__':
  76. migrate_database()
复制代码

database\models.py

  1. import sqlite3
  2. import os
  3. import numpy as np
  4. import hashlib
  5. import pickle
  6. from datetime import datetime
  7. # Database path
  8. DB_DIR = os.path.dirname(os.path.abspath(__file__))
  9. DB_PATH = os.path.join(DB_DIR, 'attendance.db')
  10. class User:
  11. """User model for handling user-related database operations"""
  12. @staticmethod
  13. def create_user(student_id, name, email, password):
  14. """Create a new user"""
  15. conn = sqlite3.connect(DB_PATH)
  16. cursor = conn.cursor()
  17. # Hash the password
  18. hashed_password = hashlib.sha256(password.encode()).hexdigest()
  19. try:
  20. cursor.execute('''
  21. INSERT INTO users (student_id, name, email, password)
  22. VALUES (?, ?, ?, ?)
  23. ''', (student_id, name, email, hashed_password))
  24. conn.commit()
  25. user_id = cursor.lastrowid
  26. conn.close()
  27. return user_id
  28. except sqlite3.IntegrityError:
  29. conn.close()
  30. return None
  31. @staticmethod
  32. def get_user_by_id(user_id):
  33. """Get user by ID"""
  34. conn = sqlite3.connect(DB_PATH)
  35. cursor = conn.cursor()
  36. cursor.execute('''
  37. SELECT id, student_id, name, email, registration_date, role, is_active
  38. FROM users
  39. WHERE id = ?
  40. ''', (user_id,))
  41. user = cursor.fetchone()
  42. conn.close()
  43. if user:
  44. return {
  45. 'id': user[0],
  46. 'student_id': user[1],
  47. 'name': user[2],
  48. 'email': user[3],
  49. 'registration_date': user[4],
  50. 'role': user[5] if len(user) > 5 else 'student',
  51. 'is_active': bool(user[6]) if len(user) > 6 else True
  52. }
  53. return None
  54. @staticmethod
  55. def get_user_by_student_id(student_id):
  56. """Get user by student ID"""
  57. conn = sqlite3.connect(DB_PATH)
  58. cursor = conn.cursor()
  59. cursor.execute('''
  60. SELECT id, student_id, name, email, registration_date, role, is_active
  61. FROM users
  62. WHERE student_id = ?
  63. ''', (student_id,))
  64. user = cursor.fetchone()
  65. conn.close()
  66. if user:
  67. return {
  68. 'id': user[0],
  69. 'student_id': user[1],
  70. 'name': user[2],
  71. 'email': user[3],
  72. 'registration_date': user[4],
  73. 'role': user[5] if len(user) > 5 else 'student',
  74. 'is_active': bool(user[6]) if len(user) > 6 else True
  75. }
  76. return None
  77. @staticmethod
  78. def authenticate(student_id, password):
  79. """Authenticate a user"""
  80. conn = sqlite3.connect(DB_PATH)
  81. cursor = conn.cursor()
  82. # Hash the password
  83. hashed_password = hashlib.sha256(password.encode()).hexdigest()
  84. cursor.execute('''
  85. SELECT id, student_id, name, email, registration_date, role, is_active
  86. FROM users
  87. WHERE student_id = ? AND password = ?
  88. ''', (student_id, hashed_password))
  89. user = cursor.fetchone()
  90. conn.close()
  91. if user:
  92. return {
  93. 'id': user[0],
  94. 'student_id': user[1],
  95. 'name': user[2],
  96. 'email': user[3],
  97. 'registration_date': user[4],
  98. 'role': user[5] if len(user) > 5 else 'student',
  99. 'is_active': bool(user[6]) if len(user) > 6 else True
  100. }
  101. return None
  102. @staticmethod
  103. def get_all_users(page=1, per_page=10):
  104. """Get all users"""
  105. conn = sqlite3.connect(DB_PATH)
  106. cursor = conn.cursor()
  107. offset = (page - 1) * per_page
  108. cursor.execute('''
  109. SELECT id, student_id, name, email, registration_date, role, is_active
  110. FROM users
  111. ORDER BY id DESC
  112. LIMIT ? OFFSET ?
  113. ''', (per_page, offset))
  114. users = cursor.fetchall()
  115. conn.close()
  116. result = []
  117. for user in users:
  118. result.append({
  119. 'id': user[0],
  120. 'student_id': user[1],
  121. 'name': user[2],
  122. 'email': user[3],
  123. 'registration_date': user[4],
  124. 'role': user[5] if len(user) > 5 else 'student',
  125. 'is_active': bool(user[6]) if len(user) > 6 else True
  126. })
  127. return result
  128. @staticmethod
  129. def count_all_users():
  130. """Count all users"""
  131. conn = sqlite3.connect(DB_PATH)
  132. cursor = conn.cursor()
  133. cursor.execute('''
  134. SELECT COUNT(*)
  135. FROM users
  136. ''')
  137. count = cursor.fetchone()[0]
  138. conn.close()
  139. return count
  140. @staticmethod
  141. def search_users(query, page=1, per_page=10):
  142. """Search users"""
  143. conn = sqlite3.connect(DB_PATH)
  144. cursor = conn.cursor()
  145. offset = (page - 1) * per_page
  146. search_query = f"%{query}%"
  147. cursor.execute('''
  148. SELECT id, student_id, name, email, registration_date, role, is_active
  149. FROM users
  150. WHERE student_id LIKE ? OR name LIKE ?
  151. ORDER BY id DESC
  152. LIMIT ? OFFSET ?
  153. ''', (search_query, search_query, per_page, offset))
  154. users = cursor.fetchall()
  155. conn.close()
  156. result = []
  157. for user in users:
  158. result.append({
  159. 'id': user[0],
  160. 'student_id': user[1],
  161. 'name': user[2],
  162. 'email': user[3],
  163. 'registration_date': user[4],
  164. 'role': user[5] if len(user) > 5 else 'student',
  165. 'is_active': bool(user[6]) if len(user) > 6 else True
  166. })
  167. return result
  168. @staticmethod
  169. def count_search_results(query):
  170. """Count search results"""
  171. conn = sqlite3.connect(DB_PATH)
  172. cursor = conn.cursor()
  173. search_query = f"%{query}%"
  174. cursor.execute('''
  175. SELECT COUNT(*)
  176. FROM users
  177. WHERE student_id LIKE ? OR name LIKE ?
  178. ''', (search_query, search_query))
  179. count = cursor.fetchone()[0]
  180. conn.close()
  181. return count
  182. @staticmethod
  183. def update_user(user_id, student_id, name, email, password=None, role='student', is_active=True):
  184. """Update user"""
  185. conn = sqlite3.connect(DB_PATH)
  186. cursor = conn.cursor()
  187. try:
  188. if password:
  189. hashed_password = hashlib.sha256(password.encode()).hexdigest()
  190. cursor.execute('''
  191. UPDATE users
  192. SET student_id = ?, name = ?, email = ?, password = ?, role = ?, is_active = ?
  193. WHERE id = ?
  194. ''', (student_id, name, email, hashed_password, role, is_active, user_id))
  195. else:
  196. cursor.execute('''
  197. UPDATE users
  198. SET student_id = ?, name = ?, email = ?, role = ?, is_active = ?
  199. WHERE id = ?
  200. ''', (student_id, name, email, role, is_active, user_id))
  201. conn.commit()
  202. return True
  203. except Exception as e:
  204. print(f"Error updating user: {e}")
  205. return False
  206. @staticmethod
  207. def delete_user(user_id):
  208. """Delete user"""
  209. conn = sqlite3.connect(DB_PATH)
  210. cursor = conn.cursor()
  211. try:
  212. # Delete user's face encodings
  213. cursor.execute('''
  214. DELETE FROM face_encodings
  215. WHERE user_id = ?
  216. ''', (user_id,))
  217. # Delete user's attendance records
  218. cursor.execute('''
  219. DELETE FROM attendance
  220. WHERE user_id = ?
  221. ''', (user_id,))
  222. # Delete user
  223. cursor.execute('''
  224. DELETE FROM users
  225. WHERE id = ?
  226. ''', (user_id,))
  227. conn.commit()
  228. return True
  229. except Exception as e:
  230. print(f"Error deleting user: {e}")
  231. return False
  232. class FaceEncoding:
  233. """Face encoding model for handling face-related database operations"""
  234. @staticmethod
  235. def save_face_encoding(user_id, face_encoding):
  236. """Save a face encoding for a user"""
  237. conn = sqlite3.connect(DB_PATH)
  238. cursor = conn.cursor()
  239. # Convert numpy array to bytes for storage
  240. encoding_bytes = pickle.dumps(face_encoding)
  241. cursor.execute('''
  242. INSERT INTO face_encodings (user_id, encoding)
  243. VALUES (?, ?)
  244. ''', (user_id, encoding_bytes))
  245. conn.commit()
  246. encoding_id = cursor.lastrowid
  247. conn.close()
  248. return encoding_id
  249. @staticmethod
  250. def get_face_encodings_by_user_id(user_id):
  251. """Get face encodings for a specific user"""
  252. conn = sqlite3.connect(DB_PATH)
  253. cursor = conn.cursor()
  254. cursor.execute('''
  255. SELECT id, user_id, encoding
  256. FROM face_encodings
  257. WHERE user_id = ?
  258. ''', (user_id,))
  259. encodings = cursor.fetchall()
  260. conn.close()
  261. result = []
  262. for encoding in encodings:
  263. # Convert bytes back to numpy array
  264. face_encoding = pickle.loads(encoding[2])
  265. result.append({
  266. 'id': encoding[0],
  267. 'user_id': encoding[1],
  268. 'encoding': face_encoding
  269. })
  270. return result
  271. @staticmethod
  272. def get_all_face_encodings():
  273. """Get all face encodings with user information"""
  274. conn = sqlite3.connect(DB_PATH)
  275. cursor = conn.cursor()
  276. cursor.execute('''
  277. SELECT f.id, f.user_id, f.encoding, u.student_id, u.name
  278. FROM face_encodings f
  279. JOIN users u ON f.user_id = u.id
  280. ''')
  281. encodings = cursor.fetchall()
  282. conn.close()
  283. result = []
  284. for encoding in encodings:
  285. # Convert bytes back to numpy array
  286. face_encoding = pickle.loads(encoding[2])
  287. result.append({
  288. 'id': encoding[0],
  289. 'user_id': encoding[1],
  290. 'encoding': face_encoding,
  291. 'student_id': encoding[3],
  292. 'name': encoding[4]
  293. })
  294. return result
  295. @staticmethod
  296. def delete_face_encodings_by_user_id(user_id):
  297. """Delete face encodings for a specific user"""
  298. conn = sqlite3.connect(DB_PATH)
  299. cursor = conn.cursor()
  300. try:
  301. cursor.execute('''
  302. DELETE FROM face_encodings
  303. WHERE user_id = ?
  304. ''', (user_id,))
  305. conn.commit()
  306. return True
  307. except Exception as e:
  308. print(f"Error deleting face encodings: {e}")
  309. return False
  310. class Attendance:
  311. """Attendance model for handling attendance-related database operations"""
  312. @staticmethod
  313. def record_check_in(user_id):
  314. """Record attendance check-in"""
  315. conn = sqlite3.connect(DB_PATH)
  316. cursor = conn.cursor()
  317. today = datetime.now().strftime('%Y-%m-%d')
  318. # Check if user already checked in today
  319. cursor.execute('''
  320. SELECT id FROM attendance
  321. WHERE user_id = ? AND date = ? AND check_out_time IS NULL
  322. ''', (user_id, today))
  323. existing = cursor.fetchone()
  324. if existing:
  325. conn.close()
  326. return False
  327. cursor.execute('''
  328. INSERT INTO attendance (user_id, date)
  329. VALUES (?, ?)
  330. ''', (user_id, today))
  331. conn.commit()
  332. attendance_id = cursor.lastrowid
  333. conn.close()
  334. return attendance_id
  335. @staticmethod
  336. def record_check_out(user_id):
  337. """Record attendance check-out"""
  338. conn = sqlite3.connect(DB_PATH)
  339. cursor = conn.cursor()
  340. today = datetime.now().strftime('%Y-%m-%d')
  341. now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  342. cursor.execute('''
  343. UPDATE attendance
  344. SET check_out_time = ?
  345. WHERE user_id = ? AND date = ? AND check_out_time IS NULL
  346. ''', (now, user_id, today))
  347. affected = cursor.rowcount
  348. conn.commit()
  349. conn.close()
  350. return affected > 0
  351. @staticmethod
  352. def get_attendance_by_date(date):
  353. """Get attendance records for a specific date"""
  354. conn = sqlite3.connect(DB_PATH)
  355. cursor = conn.cursor()
  356. cursor.execute('''
  357. SELECT a.id, a.user_id, u.student_id, u.name, a.check_in_time, a.check_out_time
  358. FROM attendance a
  359. JOIN users u ON a.user_id = u.id
  360. WHERE a.date = ?
  361. ORDER BY a.check_in_time DESC
  362. ''', (date,))
  363. records = cursor.fetchall()
  364. conn.close()
  365. result = []
  366. for record in records:
  367. result.append({
  368. 'id': record[0],
  369. 'user_id': record[1],
  370. 'student_id': record[2],
  371. 'name': record[3],
  372. 'check_in_time': record[4],
  373. 'check_out_time': record[5]
  374. })
  375. return result
  376. @staticmethod
  377. def get_attendance_by_user(user_id):
  378. """Get attendance records for a specific user"""
  379. conn = sqlite3.connect(DB_PATH)
  380. cursor = conn.cursor()
  381. cursor.execute('''
  382. SELECT id, date, check_in_time, check_out_time
  383. FROM attendance
  384. WHERE user_id = ?
  385. ORDER BY date DESC, check_in_time DESC
  386. ''', (user_id,))
  387. records = cursor.fetchall()
  388. conn.close()
  389. result = []
  390. for record in records:
  391. result.append({
  392. 'id': record[0],
  393. 'date': record[1],
  394. 'check_in_time': record[2],
  395. 'check_out_time': record[3]
  396. })
  397. return result
  398. @staticmethod
  399. def get_today_attendance(user_id):
  400. """Get user's attendance for today"""
  401. conn = sqlite3.connect(DB_PATH)
  402. cursor = conn.cursor()
  403. # Get today's date (format: YYYY-MM-DD)
  404. today = datetime.now().strftime('%Y-%m-%d')
  405. cursor.execute('''
  406. SELECT id, user_id, check_in_time, check_out_time, status
  407. FROM attendance
  408. WHERE user_id = ? AND date(check_in_time) = ?
  409. ''', (user_id, today))
  410. attendance = cursor.fetchone()
  411. conn.close()
  412. if attendance:
  413. return {
  414. 'id': attendance[0],
  415. 'user_id': attendance[1],
  416. 'check_in_time': attendance[2],
  417. 'check_out_time': attendance[3],
  418. 'status': attendance[4]
  419. }
  420. return None
  421. @staticmethod
  422. def get_recent_attendance(limit=10):
  423. """Get recent attendance records"""
  424. conn = sqlite3.connect(DB_PATH)
  425. cursor = conn.cursor()
  426. cursor.execute('''
  427. SELECT a.id, a.user_id, a.check_in_time, a.status, u.student_id, u.name
  428. FROM attendance a
  429. JOIN users u ON a.user_id = u.id
  430. ORDER BY a.check_in_time DESC
  431. LIMIT ?
  432. ''', (limit,))
  433. attendances = cursor.fetchall()
  434. conn.close()
  435. result = []
  436. for attendance in attendances:
  437. result.append({
  438. 'id': attendance[0],
  439. 'user_id': attendance[1],
  440. 'check_in_time': attendance[2],
  441. 'status': attendance[3],
  442. 'student_id': attendance[4],
  443. 'name': attendance[5]
  444. })
  445. return result
复制代码

static\css\style.css

  1. /* 全局样式 */
  2. body {
  3. background-color: #f8f9fa;
  4. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  5. }
  6. /* 导航栏样式 */
  7. .navbar {
  8. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  9. }
  10. .navbar-brand {
  11. font-weight: 600;
  12. }
  13. /* 卡片样式 */
  14. .card {
  15. border: none;
  16. border-radius: 10px;
  17. overflow: hidden;
  18. margin-bottom: 20px;
  19. transition: transform 0.3s, box-shadow 0.3s;
  20. }
  21. .card:hover {
  22. transform: translateY(-5px);
  23. box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
  24. }
  25. .card-header {
  26. font-weight: 600;
  27. border-bottom: none;
  28. }
  29. /* 按钮样式 */
  30. .btn {
  31. border-radius: 5px;
  32. font-weight: 500;
  33. padding: 8px 16px;
  34. transition: all 0.3s;
  35. }
  36. .btn-primary {
  37. background-color: #4e73df;
  38. border-color: #4e73df;
  39. }
  40. .btn-primary:hover {
  41. background-color: #2e59d9;
  42. border-color: #2e59d9;
  43. }
  44. .btn-success {
  45. background-color: #1cc88a;
  46. border-color: #1cc88a;
  47. }
  48. .btn-success:hover {
  49. background-color: #17a673;
  50. border-color: #17a673;
  51. }
  52. .btn-info {
  53. background-color: #36b9cc;
  54. border-color: #36b9cc;
  55. }
  56. .btn-info:hover {
  57. background-color: #2c9faf;
  58. border-color: #2c9faf;
  59. }
  60. /* 表单样式 */
  61. .form-control {
  62. border-radius: 5px;
  63. padding: 10px 15px;
  64. border: 1px solid #d1d3e2;
  65. }
  66. .form-control:focus {
  67. border-color: #4e73df;
  68. box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.25);
  69. }
  70. .input-group-text {
  71. background-color: #f8f9fc;
  72. border: 1px solid #d1d3e2;
  73. }
  74. /* 摄像头容器 */
  75. #camera-container, #captured-container {
  76. position: relative;
  77. width: 100%;
  78. max-width: 640px;
  79. margin: 0 auto;
  80. border-radius: 10px;
  81. overflow: hidden;
  82. }
  83. #webcam, #captured-image {
  84. width: 100%;
  85. height: auto;
  86. border-radius: 10px;
  87. }
  88. /* 考勤信息样式 */
  89. #attendance-info, #recognition-result {
  90. transition: all 0.3s ease;
  91. }
  92. /* 动画效果 */
  93. .fade-in {
  94. animation: fadeIn 0.5s;
  95. }
  96. @keyframes fadeIn {
  97. from { opacity: 0; }
  98. to { opacity: 1; }
  99. }
  100. /* 响应式调整 */
  101. @media (max-width: 768px) {
  102. .card-body {
  103. padding: 1rem;
  104. }
  105. .btn {
  106. padding: 6px 12px;
  107. }
  108. }
  109. /* 页脚样式 */
  110. footer {
  111. margin-top: 3rem;
  112. padding: 1.5rem 0;
  113. color: #6c757d;
  114. border-top: 1px solid #e3e6f0;
  115. }
复制代码

static\js\main.js

  1. // 全局工具函数
  2. // 格式化日期时间
  3. function formatDateTime(dateString) {
  4. const date = new Date(dateString);
  5. return date.toLocaleString();
  6. }
  7. // 格式化日期
  8. function formatDate(dateString) {
  9. const date = new Date(dateString);
  10. return date.toLocaleDateString();
  11. }
  12. // 格式化时间
  13. function formatTime(dateString) {
  14. const date = new Date(dateString);
  15. return date.toLocaleTimeString();
  16. }
  17. // 显示加载中状态
  18. function showLoading(element, message = '加载中...') {
  19. element.innerHTML = `
  20. <div class="text-center py-4">
  21. <div class="spinner-border text-primary" role="status">
  22. <span class="visually-hidden">Loading...</span>
  23. </div>
  24. <p class="mt-2">${message}</p>
  25. </div>
  26. `;
  27. }
  28. // 显示错误消息
  29. function showError(element, message) {
  30. element.innerHTML = `
  31. <div class="alert alert-danger" role="alert">
  32. <i class="fas fa-exclamation-circle me-2"></i>${message}
  33. </div>
  34. `;
  35. }
  36. // 显示成功消息
  37. function showSuccess(element, message) {
  38. element.innerHTML = `
  39. <div class="alert alert-success" role="alert">
  40. <i class="fas fa-check-circle me-2"></i>${message}
  41. </div>
  42. `;
  43. }
  44. // 显示警告消息
  45. function showWarning(element, message) {
  46. element.innerHTML = `
  47. <div class="alert alert-warning" role="alert">
  48. <i class="fas fa-exclamation-triangle me-2"></i>${message}
  49. </div>
  50. `;
  51. }
  52. // 显示信息消息
  53. function showInfo(element, message) {
  54. element.innerHTML = `
  55. <div class="alert alert-info" role="alert">
  56. <i class="fas fa-info-circle me-2"></i>${message}
  57. </div>
  58. `;
  59. }
  60. // 复制文本到剪贴板
  61. function copyToClipboard(text) {
  62. const textarea = document.createElement('textarea');
  63. textarea.value = text;
  64. document.body.appendChild(textarea);
  65. textarea.select();
  66. document.execCommand('copy');
  67. document.body.removeChild(textarea);
  68. }
  69. // 防抖函数
  70. function debounce(func, wait) {
  71. let timeout;
  72. return function(...args) {
  73. const context = this;
  74. clearTimeout(timeout);
  75. timeout = setTimeout(() => func.apply(context, args), wait);
  76. };
  77. }
  78. // 节流函数
  79. function throttle(func, limit) {
  80. let inThrottle;
  81. return function(...args) {
  82. const context = this;
  83. if (!inThrottle) {
  84. func.apply(context, args);
  85. inThrottle = true;
  86. setTimeout(() => inThrottle = false, limit);
  87. }
  88. };
  89. }
  90. // 文档就绪事件
  91. document.addEventListener('DOMContentLoaded', function() {
  92. // 初始化工具提示
  93. const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
  94. tooltipTriggerList.map(function(tooltipTriggerEl) {
  95. return new bootstrap.Tooltip(tooltipTriggerEl);
  96. });
  97. // 初始化弹出框
  98. const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
  99. popoverTriggerList.map(function(popoverTriggerEl) {
  100. return new bootstrap.Popover(popoverTriggerEl);
  101. });
  102. // 处理闪现消息自动消失
  103. const flashMessages = document.querySelectorAll('.alert-dismissible');
  104. flashMessages.forEach(function(message) {
  105. setTimeout(function() {
  106. const alert = bootstrap.Alert.getInstance(message);
  107. if (alert) {
  108. alert.close();
  109. } else {
  110. message.classList.add('fade');
  111. setTimeout(() => message.remove(), 500);
  112. }
  113. }, 5000);
  114. });
  115. // 处理表单验证
  116. const forms = document.querySelectorAll('.needs-validation');
  117. Array.from(forms).forEach(function(form) {
  118. form.addEventListener('submit', function(event) {
  119. if (!form.checkValidity()) {
  120. event.preventDefault();
  121. event.stopPropagation();
  122. }
  123. form.classList.add('was-validated');
  124. }, false);
  125. });
  126. // 处理返回顶部按钮
  127. const backToTopButton = document.getElementById('back-to-top');
  128. if (backToTopButton) {
  129. window.addEventListener('scroll', function() {
  130. if (window.pageYOffset > 300) {
  131. backToTopButton.classList.add('show');
  132. } else {
  133. backToTopButton.classList.remove('show');
  134. }
  135. });
  136. backToTopButton.addEventListener('click', function() {
  137. window.scrollTo({
  138. top: 0,
  139. behavior: 'smooth'
  140. });
  141. });
  142. }
  143. // 处理侧边栏切换
  144. const sidebarToggle = document.getElementById('sidebar-toggle');
  145. if (sidebarToggle) {
  146. sidebarToggle.addEventListener('click', function() {
  147. document.body.classList.toggle('sidebar-collapsed');
  148. localStorage.setItem('sidebar-collapsed', document.body.classList.contains('sidebar-collapsed'));
  149. });
  150. // 从本地存储恢复侧边栏状态
  151. if (localStorage.getItem('sidebar-collapsed') === 'true') {
  152. document.body.classList.add('sidebar-collapsed');
  153. }
  154. }
  155. // 处理暗黑模式切换
  156. const darkModeToggle = document.getElementById('dark-mode-toggle');
  157. if (darkModeToggle) {
  158. darkModeToggle.addEventListener('click', function() {
  159. document.body.classList.toggle('dark-mode');
  160. localStorage.setItem('dark-mode', document.body.classList.contains('dark-mode'));
  161. });
  162. // 从本地存储恢复暗黑模式状态
  163. if (localStorage.getItem('dark-mode') === 'true') {
  164. document.body.classList.add('dark-mode');
  165. }
  166. }
  167. });
复制代码

templates\attendance.html

  1. {% extends 'base.html' %}
  2. {% block title %}考勤记录 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="card shadow">
  5. <div class="card-header bg-primary text-white">
  6. <h4 class="mb-0"><i class="fas fa-clipboard-list me-2"></i>考勤记录</h4>
  7. </div>
  8. <div class="card-body">
  9. <div class="row mb-4">
  10. <div class="col-md-6">
  11. <form method="GET" action="{{ url_for('attendance') }}" class="d-flex">
  12. <input type="date" class="form-control me-2" name="date" value="{{ selected_date }}" required="">
  13. <button type="submit" class="btn btn-primary">
  14. <i class="fas fa-search me-1"></i>查询
  15. </button>
  16. </form>
  17. </div>
  18. <div class="col-md-6 text-md-end mt-3 mt-md-0">
  19. <a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-success">
  20. <i class="fas fa-camera me-1"></i>人脸识别考勤
  21. </a>
  22. </div>
  23. </div>
  24. {% if attendance_records %}
  25. <div class="table-responsive">
  26. {% for record in attendance_records %}
  27. {% endfor %}
  28. <table class="table table-hover table-striped">
  29. <thead class="table-light">
  30. <tr>
  31. <th>学号</th>
  32. <th>姓名</th>
  33. <th>签到时间</th>
  34. <th>签退时间</th>
  35. <th>状态</th>
  36. <th>时长</th>
  37. </tr>
  38. </thead>
  39. <tbody><tr>
  40. <td>{{ record.student_id }}</td>
  41. <td>{{ record.name }}</td>
  42. <td>{{ record.check_in_time }}</td>
  43. <td>{{ record.check_out_time if record.check_out_time else '未签退' }}</td>
  44. <td>
  45. {% if record.check_out_time %}
  46. <span class="badge bg-success">已完成</span>
  47. {% else %}
  48. <span class="badge bg-warning">进行中</span>
  49. {% endif %}
  50. </td>
  51. <td>
  52. {% if record.check_out_time %}
  53. {% set check_in = record.check_in_time.split(' ')[1] %}
  54. {% set check_out = record.check_out_time.split(' ')[1] %}
  55. {% set hours = (check_out.split(':')[0]|int - check_in.split(':')[0]|int) %}
  56. {% set minutes = (check_out.split(':')[1]|int - check_in.split(':')[1]|int) %}
  57. {% if minutes < 0 %}
  58. {% set hours = hours - 1 %}
  59. {% set minutes = minutes + 60 %}
  60. {% endif %}
  61. {{ hours }}小时{{ minutes }}分钟
  62. {% else %}
  63. -
  64. {% endif %}
  65. </td>
  66. </tr></tbody>
  67. </table>
  68. </div>
  69. <div class="row mt-4">
  70. <div class="col-md-6">
  71. <div class="card">
  72. <div class="card-header bg-light">
  73. <h5 class="mb-0">考勤统计</h5>
  74. </div>
  75. <div class="card-body">
  76. <div class="row text-center">
  77. <div class="col-4">
  78. <div class="border-end">
  79. <h3 class="text-primary">{{ attendance_records|length }}</h3>
  80. <p class="text-muted">总人数</p>
  81. </div>
  82. </div>
  83. <div class="col-4">
  84. <div class="border-end">
  85. <h3 class="text-success">
  86. {% set completed = 0 %}
  87. {% for record in attendance_records %}
  88. {% if record.check_out_time %}
  89. {% set completed = completed + 1 %}
  90. {% endif %}
  91. {% endfor %}
  92. {{ completed }}
  93. </h3>
  94. <p class="text-muted">已完成</p>
  95. </div>
  96. </div>
  97. <div class="col-4">
  98. <h3 class="text-warning">
  99. {% set in_progress = 0 %}
  100. {% for record in attendance_records %}
  101. {% if not record.check_out_time %}
  102. {% set in_progress = in_progress + 1 %}
  103. {% endif %}
  104. {% endfor %}
  105. {{ in_progress }}
  106. </h3>
  107. <p class="text-muted">进行中</p>
  108. </div>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. <div class="col-md-6 mt-3 mt-md-0">
  114. <div class="card">
  115. <div class="card-header bg-light">
  116. <h5 class="mb-0">图表统计</h5>
  117. </div>
  118. <div class="card-body">
  119. <canvas id="attendanceChart" width="100%" height="200"></canvas>
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. {% else %}
  125. <div class="alert alert-info">
  126. <i class="fas fa-info-circle me-2"></i>{{ selected_date }} 没有考勤记录
  127. </div>
  128. {% endif %}
  129. </div>
  130. <div class="card-footer">
  131. <div class="row">
  132. <div class="col-md-6">
  133. <button class="btn btn-outline-primary" onclick="window.print()">
  134. <i class="fas fa-print me-1"></i>打印记录
  135. </button>
  136. </div>
  137. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  138. <a href="#" class="btn btn-outline-success" id="exportBtn">
  139. <i class="fas fa-file-excel me-1"></i>导出Excel
  140. </a>
  141. </div>
  142. </div>
  143. </div>
  144. </div>
  145. {% endblock %}
  146. {% block extra_js %}
  147. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  148. <script>
  149. // 考勤统计图表
  150. {% if attendance_records %}
  151. const ctx = document.getElementById('attendanceChart').getContext('2d');
  152. // 计算已完成和进行中的数量
  153. let completed = 0;
  154. let inProgress = 0;
  155. {% for record in attendance_records %}
  156. {% if record.check_out_time %}
  157. completed++;
  158. {% else %}
  159. inProgress++;
  160. {% endif %}
  161. {% endfor %}
  162. const attendanceChart = new Chart(ctx, {
  163. type: 'pie',
  164. data: {
  165. labels: ['已完成', '进行中'],
  166. datasets: [{
  167. data: [completed, inProgress],
  168. backgroundColor: [
  169. 'rgba(40, 167, 69, 0.7)',
  170. 'rgba(255, 193, 7, 0.7)'
  171. ],
  172. borderColor: [
  173. 'rgba(40, 167, 69, 1)',
  174. 'rgba(255, 193, 7, 1)'
  175. ],
  176. borderWidth: 1
  177. }]
  178. },
  179. options: {
  180. responsive: true,
  181. maintainAspectRatio: false,
  182. plugins: {
  183. legend: {
  184. position: 'bottom'
  185. }
  186. }
  187. }
  188. });
  189. {% endif %}
  190. // 导出Excel功能
  191. document.getElementById('exportBtn').addEventListener('click', function(e) {
  192. e.preventDefault();
  193. alert('导出功能将在完整版中提供');
  194. });
  195. </script>
  196. {% endblock %}
复制代码

templates\base.html

  1. <meta charset="UTF-8">
  2. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  3. <title>{% block title %}校园人脸识别考勤系统{% endblock %}</title>
  4. <!-- Bootstrap CSS -->
  5. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
  6. <!-- Font Awesome -->
  7. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
  8. <!-- Custom CSS -->
  9. <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  10. {% block extra_css %}{% endblock %}
  11. <!-- Navigation -->
  12. <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
  13. <div class="container">
  14. <a class="navbar-brand" href="{{ url_for('index') }}">
  15. <i class="fas fa-user-check me-2"></i>校园人脸识别考勤系统
  16. </a>
  17. <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
  18. <span class="navbar-toggler-icon"></span>
  19. </button>
  20. <div class="collapse navbar-collapse" id="navbarNav">
  21. <ul class="navbar-nav ms-auto">
  22. <li class="nav-item">
  23. <a class="nav-link" href="{{ url_for('index') }}">首页</a>
  24. </li>
  25. {% if session.get('user_id') %}
  26. <li class="nav-item">
  27. <a class="nav-link" href="{{ url_for('dashboard') }}">控制面板</a>
  28. </li>
  29. <li class="nav-item">
  30. <a class="nav-link" href="{{ url_for('face_recognition_attendance') }}">人脸识别考勤</a>
  31. </li>
  32. <li class="nav-item">
  33. <a class="nav-link" href="{{ url_for('attendance') }}">考勤记录</a>
  34. </li>
  35. <li class="nav-item dropdown">
  36. <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
  37. <i class="fas fa-user-circle me-1"></i>{{ session.get('name') }}
  38. </a>
  39. <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
  40. <li><a class="dropdown-item" href="{{ url_for('dashboard') }}">个人信息</a></li>
  41. <li><a class="dropdown-item" href="{{ url_for('face_registration') }}">人脸注册</a></li>
  42. <li>
  43. <hr class="dropdown-divider">
  44. </li>
  45. <li><a class="dropdown-item" href="{{ url_for('logout') }}">退出登录</a></li>
  46. </ul>
  47. </li>
  48. {% else %}
  49. <li class="nav-item">
  50. <a class="nav-link" href="{{ url_for('login') }}">登录</a>
  51. </li>
  52. <li class="nav-item">
  53. <a class="nav-link" href="{{ url_for('register') }}">注册</a>
  54. </li>
  55. {% endif %}
  56. </ul>
  57. </div>
  58. </div>
  59. </nav>
  60. <!-- Flash Messages -->
  61. <div class="container mt-3">
  62. {% with messages = get_flashed_messages(with_categories=true) %}
  63. {% if messages %}
  64. {% for category, message in messages %}
  65. <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
  66. {{ message }}
  67. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
  68. </div>
  69. {% endfor %}
  70. {% endif %}
  71. {% endwith %}
  72. </div>
  73. <!-- Main Content -->
  74. <main class="container my-4">
  75. {% block content %}{% endblock %}
  76. </main>
  77. <!-- Footer -->
  78. <footer class="bg-light py-4 mt-5">
  79. <div class="container text-center">
  80. <p class="mb-0">© {{ now.year }} 校园人脸识别考勤系统 | 基于深度学习的智能考勤解决方案</p>
  81. </div>
  82. </footer>
  83. <!-- Bootstrap JS Bundle with Popper -->
  84. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
  85. <!-- jQuery -->
  86. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  87. <!-- Custom JS -->
  88. <script src="{{ url_for('static', filename='js/main.js') }}"></script>
  89. {% block extra_js %}{% endblock %}
复制代码

templates\dashboard.html

  1. {% extends 'base.html' %}
  2. {% block title %}控制面板 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row">
  5. <div class="col-md-4">
  6. <div class="card shadow mb-4">
  7. <div class="card-header bg-primary text-white">
  8. <h5 class="mb-0"><i class="fas fa-user me-2"></i>个人信息</h5>
  9. </div>
  10. <div class="card-body">
  11. <div class="text-center mb-3">
  12. {% if has_face_data %}
  13. <div class="avatar-container mb-3">
  14. <i class="fas fa-user-circle fa-6x text-primary"></i>
  15. <span class="badge bg-success position-absolute bottom-0 end-0">
  16. <i class="fas fa-check"></i>
  17. </span>
  18. </div>
  19. <p class="text-success"><i class="fas fa-check-circle me-1"></i>人脸数据已注册</p>
  20. {% else %}
  21. <div class="avatar-container mb-3">
  22. <i class="fas fa-user-circle fa-6x text-secondary"></i>
  23. <span class="badge bg-warning position-absolute bottom-0 end-0">
  24. <i class="fas fa-exclamation"></i>
  25. </span>
  26. </div>
  27. <p class="text-warning"><i class="fas fa-exclamation-circle me-1"></i>尚未注册人脸数据</p>
  28. <a href="{{ url_for('face_registration') }}" class="btn btn-primary btn-sm">
  29. <i class="fas fa-camera me-1"></i>立即注册
  30. </a>
  31. {% endif %}
  32. </div>
  33. <table class="table">
  34. <tbody>
  35. <tr>
  36. <th scope="row"><i class="fas fa-id-card me-2"></i>学号</th>
  37. <td>{{ user.student_id }}</td>
  38. </tr>
  39. <tr>
  40. <th scope="row"><i class="fas fa-user me-2"></i>姓名</th>
  41. <td>{{ user.name }}</td>
  42. </tr>
  43. <tr>
  44. <th scope="row"><i class="fas fa-envelope me-2"></i>邮箱</th>
  45. <td>{{ user.email }}</td>
  46. </tr>
  47. <tr>
  48. <th scope="row"><i class="fas fa-calendar-alt me-2"></i>注册日期</th>
  49. <td>{{ user.registration_date }}</td>
  50. </tr>
  51. </tbody>
  52. </table>
  53. </div>
  54. </div>
  55. <div class="card shadow mb-4">
  56. <div class="card-header bg-info text-white">
  57. <h5 class="mb-0"><i class="fas fa-clock me-2"></i>快速考勤</h5>
  58. </div>
  59. <div class="card-body text-center">
  60. <div class="row">
  61. <div class="col-6">
  62. <button id="check-in-btn" class="btn btn-success btn-lg w-100 mb-2">
  63. <i class="fas fa-sign-in-alt me-2"></i>签到
  64. </button>
  65. </div>
  66. <div class="col-6">
  67. <button id="check-out-btn" class="btn btn-danger btn-lg w-100 mb-2">
  68. <i class="fas fa-sign-out-alt me-2"></i>签退
  69. </button>
  70. </div>
  71. </div>
  72. <div id="attendance-status" class="mt-2"></div>
  73. <div class="mt-3">
  74. <a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-primary w-100">
  75. <i class="fas fa-camera me-2"></i>人脸识别考勤
  76. </a>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. <div class="col-md-8">
  82. <div class="card shadow mb-4">
  83. <div class="card-header bg-primary text-white">
  84. <h5 class="mb-0"><i class="fas fa-history me-2"></i>考勤记录</h5>
  85. </div>
  86. <div class="card-body">
  87. {% if attendance_records %}
  88. <div class="table-responsive">
  89. {% for record in attendance_records %}
  90. {% endfor %}
  91. <table class="table table-hover">
  92. <thead>
  93. <tr>
  94. <th>日期</th>
  95. <th>签到时间</th>
  96. <th>签退时间</th>
  97. <th>状态</th>
  98. </tr>
  99. </thead>
  100. <tbody><tr>
  101. <td>{{ record.date }}</td>
  102. <td>{{ record.check_in_time }}</td>
  103. <td>{{ record.check_out_time if record.check_out_time else '未签退' }}</td>
  104. <td>
  105. {% if record.check_out_time %}
  106. <span class="badge bg-success">已完成</span>
  107. {% else %}
  108. <span class="badge bg-warning">进行中</span>
  109. {% endif %}
  110. </td>
  111. </tr></tbody>
  112. </table>
  113. </div>
  114. {% else %}
  115. <div class="alert alert-info">
  116. <i class="fas fa-info-circle me-2"></i>暂无考勤记录
  117. </div>
  118. {% endif %}
  119. </div>
  120. <div class="card-footer text-end">
  121. <a href="{{ url_for('attendance') }}" class="btn btn-outline-primary btn-sm">
  122. <i class="fas fa-list me-1"></i>查看全部记录
  123. </a>
  124. </div>
  125. </div>
  126. <div class="row">
  127. <div class="col-md-6">
  128. <div class="card shadow mb-4">
  129. <div class="card-header bg-success text-white">
  130. <h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>本月统计</h5>
  131. </div>
  132. <div class="card-body">
  133. <canvas id="monthlyChart" width="100%" height="200"></canvas>
  134. </div>
  135. </div>
  136. </div>
  137. <div class="col-md-6">
  138. <div class="card shadow mb-4">
  139. <div class="card-header bg-warning text-white">
  140. <h5 class="mb-0"><i class="fas fa-bell me-2"></i>通知</h5>
  141. </div>
  142. <div class="card-body">
  143. <div class="list-group">
  144. <a href="#" class="list-group-item list-group-item-action">
  145. <div class="d-flex w-100 justify-content-between">
  146. <h6 class="mb-1">系统更新通知</h6>
  147. <small>3天前</small>
  148. </div>
  149. <p class="mb-1">系统已更新到最新版本,新增人脸识别算法...</p>
  150. </a>
  151. <a href="#" class="list-group-item list-group-item-action">
  152. <div class="d-flex w-100 justify-content-between">
  153. <h6 class="mb-1">考勤规则变更</h6>
  154. <small>1周前</small>
  155. </div>
  156. <p class="mb-1">根据学校规定,考勤时间调整为8:30-17:30...</p>
  157. </a>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. </div>
  165. {% endblock %}
  166. {% block extra_css %}
  167. <style>
  168. .avatar-container {
  169. position: relative;
  170. display: inline-block;
  171. }
  172. .avatar-container .badge {
  173. width: 25px;
  174. height: 25px;
  175. border-radius: 50%;
  176. display: flex;
  177. align-items: center;
  178. justify-content: center;
  179. }
  180. </style>
  181. {% endblock %}
  182. {% block extra_js %}
  183. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  184. <script>
  185. // 考勤按钮功能
  186. document.getElementById('check-in-btn').addEventListener('click', function() {
  187. const statusDiv = document.getElementById('attendance-status');
  188. statusDiv.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> 处理中...';
  189. fetch('{{ url_for("process_check_in") }}', {
  190. method: 'POST',
  191. headers: {
  192. 'Content-Type': 'application/json',
  193. }
  194. })
  195. .then(response => response.json())
  196. .then(data => {
  197. if (data.success) {
  198. statusDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
  199. setTimeout(() => {
  200. window.location.reload();
  201. }, 2000);
  202. } else {
  203. statusDiv.innerHTML = '<div class="alert alert-warning">' + data.message + '</div>';
  204. }
  205. })
  206. .catch(error => {
  207. console.error('Error:', error);
  208. statusDiv.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
  209. });
  210. });
  211. document.getElementById('check-out-btn').addEventListener('click', function() {
  212. const statusDiv = document.getElementById('attendance-status');
  213. statusDiv.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> 处理中...';
  214. fetch('{{ url_for("check_out") }}', {
  215. method: 'POST',
  216. headers: {
  217. 'Content-Type': 'application/json',
  218. }
  219. })
  220. .then(response => response.json())
  221. .then(data => {
  222. if (data.success) {
  223. statusDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
  224. setTimeout(() => {
  225. window.location.reload();
  226. }, 2000);
  227. } else {
  228. statusDiv.innerHTML = '<div class="alert alert-warning">' + data.message + '</div>';
  229. }
  230. })
  231. .catch(error => {
  232. console.error('Error:', error);
  233. statusDiv.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
  234. });
  235. });
  236. // 月度统计图表
  237. const ctx = document.getElementById('monthlyChart').getContext('2d');
  238. const monthlyChart = new Chart(ctx, {
  239. type: 'bar',
  240. data: {
  241. labels: ['1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日'],
  242. datasets: [{
  243. label: '考勤时长(小时)',
  244. data: [8, 8.5, 7.5, 8, 8, 0, 0, 8.5, 8, 7],
  245. backgroundColor: 'rgba(75, 192, 192, 0.2)',
  246. borderColor: 'rgba(75, 192, 192, 1)',
  247. borderWidth: 1
  248. }]
  249. },
  250. options: {
  251. scales: {
  252. y: {
  253. beginAtZero: true,
  254. max: 10
  255. }
  256. },
  257. plugins: {
  258. legend: {
  259. display: false
  260. }
  261. },
  262. maintainAspectRatio: false
  263. }
  264. });
  265. </script>
  266. {% endblock %}
复制代码

templates\edit_user.html

  1. {% extends 'base.html' %}
  2. {% block title %}编辑用户 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-8">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-user-edit me-2"></i>编辑用户</h4>
  9. </div>
  10. <div class="card-body">
  11. <form method="POST" action="{{ url_for('edit_user', user_id=user.id) }}">
  12. <div class="row">
  13. <div class="col-md-6 mb-3">
  14. <label for="student_id" class="form-label">学号 <span class="text-danger">*</span></label>
  15. <div class="input-group">
  16. <span class="input-group-text"><i class="fas fa-id-card"></i></span>
  17. <input type="text" class="form-control" id="student_id" name="student_id" value="{{ user.student_id }}" required="">
  18. </div>
  19. </div>
  20. <div class="col-md-6 mb-3">
  21. <label for="name" class="form-label">姓名 <span class="text-danger">*</span></label>
  22. <div class="input-group">
  23. <span class="input-group-text"><i class="fas fa-user"></i></span>
  24. <input type="text" class="form-control" id="name" name="name" value="{{ user.name }}" required="">
  25. </div>
  26. </div>
  27. </div>
  28. <div class="mb-3">
  29. <label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
  30. <div class="input-group">
  31. <span class="input-group-text"><i class="fas fa-envelope"></i></span>
  32. <input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required="">
  33. </div>
  34. </div>
  35. <div class="mb-3">
  36. <label for="password" class="form-label">重置密码 <small class="text-muted">(留空表示不修改)</small></label>
  37. <div class="input-group">
  38. <span class="input-group-text"><i class="fas fa-lock"></i></span>
  39. <input type="password" class="form-control" id="password" name="password">
  40. </div>
  41. <div class="form-text">如需重置密码,请在此输入新密码</div>
  42. </div>
  43. <div class="mb-3">
  44. <label for="role" class="form-label">用户角色</label>
  45. <div class="input-group">
  46. <span class="input-group-text"><i class="fas fa-user-tag"></i></span>
  47. <select class="form-select" id="role" name="role">
  48. <option value="student" {%="" if="" user.role="=" 'student'="" %}selected{%="" endif="" %}="">学生</option>
  49. <option value="teacher" {%="" if="" user.role="=" 'teacher'="" %}selected{%="" endif="" %}="">教师</option>
  50. <option value="admin" {%="" if="" user.role="=" 'admin'="" %}selected{%="" endif="" %}="">管理员</option>
  51. </select>
  52. </div>
  53. </div>
  54. <div class="mb-3">
  55. <div class="form-check form-switch">
  56. <input class="form-check-input" type="checkbox" id="is_active" name="is_active" {%="" if="" user.is_active="" %}checked{%="" endif="" %}="">
  57. <label class="form-check-label" for="is_active">账号状态(启用/禁用)</label>
  58. </div>
  59. </div>
  60. <div class="d-grid gap-2">
  61. <button type="submit" class="btn btn-primary">保存修改</button>
  62. </div>
  63. </form>
  64. </div>
  65. <div class="card-footer">
  66. <div class="row">
  67. <div class="col-md-6">
  68. <a href="{{ url_for('user_management') }}" class="btn btn-outline-secondary">
  69. <i class="fas fa-arrow-left me-1"></i>返回用户列表
  70. </a>
  71. </div>
  72. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  73. {% if user.has_face_data %}
  74. <button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#resetFaceModal">
  75. <i class="fas fa-trash-alt me-1"></i>重置人脸数据
  76. </button>
  77. {% else %}
  78. <a href="{{ url_for('face_registration_admin', user_id=user.id) }}" class="btn btn-outline-success">
  79. <i class="fas fa-camera me-1"></i>注册人脸数据
  80. </a>
  81. {% endif %}
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. <!-- Reset Face Data Modal -->
  89. <div class="modal fade" id="resetFaceModal" tabindex="-1" aria-labelledby="resetFaceModalLabel" aria-hidden="true">
  90. <div class="modal-dialog">
  91. <div class="modal-content">
  92. <div class="modal-header">
  93. <h5 class="modal-title" id="resetFaceModalLabel">确认重置人脸数据</h5>
  94. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  95. </div>
  96. <div class="modal-body">
  97. <p>确定要重置用户 <strong>{{ user.name }}</strong> 的人脸数据吗?</p>
  98. <p class="text-danger">此操作不可逆,用户将需要重新注册人脸数据才能使用人脸识别功能。</p>
  99. </div>
  100. <div class="modal-footer">
  101. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
  102. <form action="{{ url_for('reset_face_data', user_id=user.id) }}" method="POST" style="display: inline;">
  103. <button type="submit" class="btn btn-danger">确认重置</button>
  104. </form>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. {% endblock %}
复制代码

templates\face_recognition_attendance.html

  1. {% extends 'base.html' %}
  2. {% block title %}人脸识别考勤 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-8">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-camera me-2"></i>人脸识别考勤</h4>
  9. </div>
  10. <div class="card-body">
  11. <div class="text-center mb-4">
  12. <h5 class="mb-3">请面向摄像头,系统将自动识别您的身份</h5>
  13. <div class="alert alert-info">
  14. <i class="fas fa-info-circle me-2"></i>请确保光线充足,面部无遮挡
  15. </div>
  16. </div>
  17. <div class="row">
  18. <div class="col-md-8 mx-auto">
  19. <div id="camera-container" class="position-relative">
  20. <video id="webcam" autoplay="" playsinline="" width="100%" class="rounded border"></video>
  21. <div id="face-overlay" class="position-absolute top-0 start-0 w-100 h-100"></div>
  22. <canvas id="canvas" class="d-none"></canvas>
  23. </div>
  24. <div id="recognition-status" class="text-center mt-3">
  25. <div class="alert alert-secondary">
  26. <i class="fas fa-spinner fa-spin me-2"></i>准备中...
  27. </div>
  28. </div>
  29. <div id="recognition-result" class="text-center mt-3 d-none">
  30. <div class="card">
  31. <div class="card-body">
  32. <h5 id="result-name" class="card-title mb-2"></h5>
  33. <p id="result-id" class="card-text text-muted"></p>
  34. <p id="result-time" class="card-text"></p>
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. </div>
  40. <div class="row mt-4">
  41. <div class="col-md-8 mx-auto">
  42. <div class="d-grid gap-2">
  43. <button id="start-camera" class="btn btn-primary">
  44. <i class="fas fa-video me-2"></i>启动摄像头
  45. </button>
  46. <button id="capture-photo" class="btn btn-success d-none">
  47. <i class="fas fa-camera me-2"></i>拍摄并识别
  48. </button>
  49. <button id="retry-button" class="btn btn-secondary d-none">
  50. <i class="fas fa-redo me-2"></i>重新识别
  51. </button>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="card-footer">
  57. <div class="row">
  58. <div class="col-md-6">
  59. <a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">
  60. <i class="fas fa-arrow-left me-1"></i>返回控制面板
  61. </a>
  62. </div>
  63. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  64. <a href="{{ url_for('check_in') }}" class="btn btn-outline-primary">
  65. <i class="fas fa-clipboard-check me-1"></i>手动考勤
  66. </a>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. {% endblock %}
  74. {% block extra_css %}
  75. <style>
  76. #camera-container {
  77. max-width: 640px;
  78. margin: 0 auto;
  79. border-radius: 0.25rem;
  80. overflow: hidden;
  81. }
  82. #face-overlay {
  83. pointer-events: none;
  84. }
  85. .face-box {
  86. position: absolute;
  87. border: 2px solid #28a745;
  88. border-radius: 4px;
  89. }
  90. .face-label {
  91. position: absolute;
  92. background-color: rgba(40, 167, 69, 0.8);
  93. color: white;
  94. padding: 2px 6px;
  95. border-radius: 2px;
  96. font-size: 12px;
  97. top: -20px;
  98. left: 0;
  99. }
  100. .unknown-face {
  101. border-color: #dc3545;
  102. }
  103. .unknown-face .face-label {
  104. background-color: rgba(220, 53, 69, 0.8);
  105. }
  106. .processing-indicator {
  107. position: absolute;
  108. top: 50%;
  109. left: 50%;
  110. transform: translate(-50%, -50%);
  111. background-color: rgba(0, 0, 0, 0.7);
  112. color: white;
  113. padding: 10px 20px;
  114. border-radius: 4px;
  115. font-size: 14px;
  116. }
  117. @keyframes pulse {
  118. 0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); }
  119. 70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
  120. 100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
  121. }
  122. .pulse {
  123. animation: pulse 1.5s infinite;
  124. }
  125. </style>
  126. {% endblock %}
  127. {% block extra_js %}
  128. <script>
  129. const startCameraBtn = document.getElementById('start-camera');
  130. const capturePhotoBtn = document.getElementById('capture-photo');
  131. const retryButton = document.getElementById('retry-button');
  132. const webcamVideo = document.getElementById('webcam');
  133. const canvas = document.getElementById('canvas');
  134. const faceOverlay = document.getElementById('face-overlay');
  135. const recognitionStatus = document.getElementById('recognition-status');
  136. const recognitionResult = document.getElementById('recognition-result');
  137. const resultName = document.getElementById('result-name');
  138. const resultId = document.getElementById('result-id');
  139. const resultTime = document.getElementById('result-time');
  140. let stream = null;
  141. let isProcessing = false;
  142. // 启动摄像头
  143. startCameraBtn.addEventListener('click', async function() {
  144. try {
  145. stream = await navigator.mediaDevices.getUserMedia({
  146. video: {
  147. width: { ideal: 640 },
  148. height: { ideal: 480 },
  149. facingMode: 'user'
  150. }
  151. });
  152. webcamVideo.srcObject = stream;
  153. startCameraBtn.classList.add('d-none');
  154. capturePhotoBtn.classList.remove('d-none');
  155. recognitionStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>摄像头已启动,请面向摄像头</div>';
  156. // 添加脉冲效果
  157. webcamVideo.classList.add('pulse');
  158. } catch (err) {
  159. console.error('摄像头访问失败:', err);
  160. recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>无法访问摄像头: ' + err.message + '</div>';
  161. }
  162. });
  163. // 拍摄照片并识别
  164. capturePhotoBtn.addEventListener('click', function() {
  165. if (isProcessing) return;
  166. isProcessing = true;
  167. // 显示处理中状态
  168. faceOverlay.innerHTML = '<div class="processing-indicator"><i class="fas fa-spinner fa-spin me-2"></i>正在识别...</div>';
  169. recognitionStatus.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin me-2"></i>正在处理,请稍候...</div>';
  170. // 拍摄照片
  171. canvas.width = webcamVideo.videoWidth;
  172. canvas.height = webcamVideo.videoHeight;
  173. const ctx = canvas.getContext('2d');
  174. ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
  175. // 获取图像数据
  176. const imageData = canvas.toDataURL('image/jpeg');
  177. // 发送到服务器进行人脸识别
  178. fetch('{{ url_for("process_face_attendance") }}', {
  179. method: 'POST',
  180. headers: {
  181. 'Content-Type': 'application/x-www-form-urlencoded',
  182. },
  183. body: 'image_data=' + encodeURIComponent(imageData)
  184. })
  185. .then(response => response.json())
  186. .then(data => {
  187. isProcessing = false;
  188. faceOverlay.innerHTML = '';
  189. if (data.success) {
  190. // 识别成功
  191. recognitionStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>' + data.message + '</div>';
  192. // 显示结果
  193. resultName.textContent = data.user.name;
  194. resultId.textContent = '学号: ' + data.user.student_id;
  195. resultTime.textContent = '考勤时间: ' + new Date().toLocaleString();
  196. recognitionResult.classList.remove('d-none');
  197. // 更新按钮状态
  198. capturePhotoBtn.classList.add('d-none');
  199. retryButton.classList.remove('d-none');
  200. // 绘制人脸框
  201. drawFaceBox(true, data.user.name);
  202. // 移除脉冲效果
  203. webcamVideo.classList.remove('pulse');
  204. } else {
  205. // 识别失败
  206. recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>' + data.message + '</div>';
  207. // 绘制未知人脸框
  208. drawFaceBox(false);
  209. }
  210. })
  211. .catch(error => {
  212. console.error('Error:', error);
  213. isProcessing = false;
  214. faceOverlay.innerHTML = '';
  215. recognitionStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>服务器错误,请稍后重试</div>';
  216. });
  217. });
  218. // 重新识别
  219. retryButton.addEventListener('click', function() {
  220. recognitionResult.classList.add('d-none');
  221. capturePhotoBtn.classList.remove('d-none');
  222. retryButton.classList.add('d-none');
  223. faceOverlay.innerHTML = '';
  224. recognitionStatus.innerHTML = '<div class="alert alert-secondary"><i class="fas fa-info-circle me-2"></i>请面向摄像头,准备重新识别</div>';
  225. // 添加脉冲效果
  226. webcamVideo.classList.add('pulse');
  227. });
  228. // 绘制人脸框
  229. function drawFaceBox(isRecognized, name) {
  230. // 模拟人脸位置
  231. const videoWidth = webcamVideo.videoWidth;
  232. const videoHeight = webcamVideo.videoHeight;
  233. const scale = webcamVideo.offsetWidth / videoWidth;
  234. // 人脸框位置(居中)
  235. const faceWidth = videoWidth * 0.4;
  236. const faceHeight = videoHeight * 0.5;
  237. const faceLeft = (videoWidth - faceWidth) / 2;
  238. const faceTop = (videoHeight - faceHeight) / 2;
  239. // 创建人脸框元素
  240. const faceBox = document.createElement('div');
  241. faceBox.className = 'face-box' + (isRecognized ? '' : ' unknown-face');
  242. faceBox.style.left = (faceLeft * scale) + 'px';
  243. faceBox.style.top = (faceTop * scale) + 'px';
  244. faceBox.style.width = (faceWidth * scale) + 'px';
  245. faceBox.style.height = (faceHeight * scale) + 'px';
  246. // 添加标签
  247. const faceLabel = document.createElement('div');
  248. faceLabel.className = 'face-label';
  249. faceLabel.textContent = isRecognized ? name : '未识别';
  250. faceBox.appendChild(faceLabel);
  251. faceOverlay.appendChild(faceBox);
  252. }
  253. // 页面卸载时停止摄像头
  254. window.addEventListener('beforeunload', function() {
  255. if (stream) {
  256. stream.getTracks().forEach(track => track.stop());
  257. }
  258. });
  259. </script>
  260. {% endblock %}
复制代码

templates\face_registration.html

  1. {% extends 'base.html' %}
  2. {% block title %}人脸注册 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-10">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-camera me-2"></i>人脸注册</h4>
  9. </div>
  10. <div class="card-body">
  11. <div class="row">
  12. <div class="col-md-6">
  13. <div class="card mb-4">
  14. <div class="card-header bg-light">
  15. <h5 class="mb-0">上传照片</h5>
  16. </div>
  17. <div class="card-body">
  18. <form method="POST" action="{{ url_for('face_registration') }}" enctype="multipart/form-data">
  19. <div class="mb-3">
  20. <label for="face_image" class="form-label">选择照片</label>
  21. <input class="form-control" type="file" id="face_image" name="face_image" accept="image/jpeg,image/png,image/jpg" required="">
  22. <div class="form-text">请上传清晰的正面照片,确保光线充足,面部无遮挡</div>
  23. </div>
  24. <div class="mb-3">
  25. <div id="image-preview" class="text-center d-none">
  26. <img id="preview-img" src="#" alt="预览图" class="img-fluid rounded mb-2" style="max-height: 300px;">
  27. <button type="button" id="clear-preview" class="btn btn-sm btn-outline-danger">
  28. <i class="fas fa-times"></i> 清除
  29. </button>
  30. </div>
  31. </div>
  32. <div class="d-grid">
  33. <button type="submit" class="btn btn-primary">
  34. <i class="fas fa-upload me-2"></i>上传并注册
  35. </button>
  36. </div>
  37. </form>
  38. </div>
  39. </div>
  40. </div>
  41. <div class="col-md-6">
  42. <div class="card">
  43. <div class="card-header bg-light">
  44. <h5 class="mb-0">使用摄像头</h5>
  45. </div>
  46. <div class="card-body">
  47. <div class="text-center mb-3">
  48. <div id="camera-container">
  49. <video id="webcam" autoplay="" playsinline="" width="100%" class="rounded"></video>
  50. <canvas id="canvas" class="d-none"></canvas>
  51. </div>
  52. <div id="captured-container" class="d-none">
  53. <img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded mb-2">
  54. </div>
  55. </div>
  56. <div class="d-grid gap-2">
  57. <button id="start-camera" class="btn btn-info">
  58. <i class="fas fa-video me-2"></i>打开摄像头
  59. </button>
  60. <button id="capture-photo" class="btn btn-primary d-none">
  61. <i class="fas fa-camera me-2"></i>拍摄照片
  62. </button>
  63. <button id="retake-photo" class="btn btn-outline-secondary d-none">
  64. <i class="fas fa-redo me-2"></i>重新拍摄
  65. </button>
  66. <button id="save-photo" class="btn btn-success d-none">
  67. <i class="fas fa-save me-2"></i>保存并注册
  68. </button>
  69. </div>
  70. <div id="webcam-status" class="mt-2 text-center"></div>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. <div class="card-footer">
  77. <div class="alert alert-info mb-0">
  78. <h5><i class="fas fa-info-circle me-2"></i>人脸注册说明</h5>
  79. <ul>
  80. <li>请确保面部清晰可见,无遮挡物(如口罩、墨镜等)</li>
  81. <li>保持自然表情,正面面对摄像头或照片中心</li>
  82. <li>避免强烈的侧光或背光,确保光线均匀</li>
  83. <li>注册成功后,您可以使用人脸识别功能进行考勤</li>
  84. <li>如遇注册失败,请尝试调整光线或姿势后重新尝试</li>
  85. </ul>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. {% endblock %}
  92. {% block extra_js %}
  93. <script>
  94. // 照片上传预览
  95. document.getElementById('face_image').addEventListener('change', function(e) {
  96. const file = e.target.files[0];
  97. if (file) {
  98. const reader = new FileReader();
  99. reader.onload = function(event) {
  100. const previewImg = document.getElementById('preview-img');
  101. previewImg.src = event.target.result;
  102. document.getElementById('image-preview').classList.remove('d-none');
  103. };
  104. reader.readAsDataURL(file);
  105. }
  106. });
  107. document.getElementById('clear-preview').addEventListener('click', function() {
  108. document.getElementById('face_image').value = '';
  109. document.getElementById('image-preview').classList.add('d-none');
  110. });
  111. // 摄像头功能
  112. const startCameraBtn = document.getElementById('start-camera');
  113. const capturePhotoBtn = document.getElementById('capture-photo');
  114. const retakePhotoBtn = document.getElementById('retake-photo');
  115. const savePhotoBtn = document.getElementById('save-photo');
  116. const webcamVideo = document.getElementById('webcam');
  117. const canvas = document.getElementById('canvas');
  118. const capturedImage = document.getElementById('captured-image');
  119. const webcamContainer = document.getElementById('camera-container');
  120. const capturedContainer = document.getElementById('captured-container');
  121. const webcamStatus = document.getElementById('webcam-status');
  122. let stream = null;
  123. // 启动摄像头
  124. startCameraBtn.addEventListener('click', async function() {
  125. try {
  126. stream = await navigator.mediaDevices.getUserMedia({
  127. video: {
  128. width: { ideal: 640 },
  129. height: { ideal: 480 },
  130. facingMode: 'user'
  131. }
  132. });
  133. webcamVideo.srcObject = stream;
  134. startCameraBtn.classList.add('d-none');
  135. capturePhotoBtn.classList.remove('d-none');
  136. webcamStatus.innerHTML = '<span class="text-success">摄像头已启动</span>';
  137. } catch (err) {
  138. console.error('摄像头访问失败:', err);
  139. webcamStatus.innerHTML = '<span class="text-danger">无法访问摄像头: ' + err.message + '</span>';
  140. }
  141. });
  142. // 拍摄照片
  143. capturePhotoBtn.addEventListener('click', function() {
  144. canvas.width = webcamVideo.videoWidth;
  145. canvas.height = webcamVideo.videoHeight;
  146. const ctx = canvas.getContext('2d');
  147. ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
  148. capturedImage.src = canvas.toDataURL('image/jpeg');
  149. webcamContainer.classList.add('d-none');
  150. capturedContainer.classList.remove('d-none');
  151. capturePhotoBtn.classList.add('d-none');
  152. retakePhotoBtn.classList.remove('d-none');
  153. savePhotoBtn.classList.remove('d-none');
  154. });
  155. // 重新拍摄
  156. retakePhotoBtn.addEventListener('click', function() {
  157. webcamContainer.classList.remove('d-none');
  158. capturedContainer.classList.add('d-none');
  159. capturePhotoBtn.classList.remove('d-none');
  160. retakePhotoBtn.classList.add('d-none');
  161. savePhotoBtn.classList.add('d-none');
  162. });
  163. // 保存照片并注册
  164. savePhotoBtn.addEventListener('click', function() {
  165. const imageData = capturedImage.src;
  166. // 显示加载状态
  167. savePhotoBtn.disabled = true;
  168. savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
  169. // 发送到服务器
  170. fetch('{{ url_for("webcam_registration") }}', {
  171. method: 'POST',
  172. headers: {
  173. 'Content-Type': 'application/x-www-form-urlencoded',
  174. },
  175. body: 'image_data=' + encodeURIComponent(imageData)
  176. })
  177. .then(response => response.json())
  178. .then(data => {
  179. if (data.success) {
  180. // 注册成功
  181. webcamStatus.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
  182. // 停止摄像头
  183. if (stream) {
  184. stream.getTracks().forEach(track => track.stop());
  185. }
  186. // 3秒后跳转到控制面板
  187. setTimeout(() => {
  188. window.location.href = '{{ url_for("dashboard") }}';
  189. }, 3000);
  190. } else {
  191. // 注册失败
  192. webcamStatus.innerHTML = '<div class="alert alert-danger">' + data.message + '</div>';
  193. savePhotoBtn.disabled = false;
  194. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  195. // 重置为拍摄状态
  196. setTimeout(() => {
  197. retakePhotoBtn.click();
  198. }, 2000);
  199. }
  200. })
  201. .catch(error => {
  202. console.error('Error:', error);
  203. webcamStatus.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
  204. savePhotoBtn.disabled = false;
  205. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  206. });
  207. });
  208. // 页面卸载时停止摄像头
  209. window.addEventListener('beforeunload', function() {
  210. if (stream) {
  211. stream.getTracks().forEach(track => track.stop());
  212. }
  213. });
  214. </script>
  215. {% endblock %}
复制代码

templates\face_registration_admin.html

  1. {% extends 'base.html' %}
  2. {% block title %}管理员人脸注册 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-8">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-camera me-2"></i>为用户注册人脸数据</h4>
  9. </div>
  10. <div class="card-body">
  11. <div class="alert alert-info mb-4">
  12. <h5 class="mb-2"><i class="fas fa-info-circle me-2"></i>用户信息</h5>
  13. <div class="row">
  14. <div class="col-md-6">
  15. <p><strong>学号:</strong> {{ user.student_id }}</p>
  16. <p><strong>姓名:</strong> {{ user.name }}</p>
  17. </div>
  18. <div class="col-md-6">
  19. <p><strong>邮箱:</strong> {{ user.email }}</p>
  20. <p><strong>注册日期:</strong> {{ user.registration_date }}</p>
  21. </div>
  22. </div>
  23. </div>
  24. <div class="row">
  25. <div class="col-md-6">
  26. <div class="card mb-4">
  27. <div class="card-header bg-light">
  28. <h5 class="mb-0">上传照片</h5>
  29. </div>
  30. <div class="card-body">
  31. <form method="POST" action="{{ url_for('face_registration_admin', user_id=user.id) }}" enctype="multipart/form-data">
  32. <div class="mb-3">
  33. <label for="face_image" class="form-label">选择照片</label>
  34. <input class="form-control" type="file" id="face_image" name="face_image" accept="image/jpeg,image/png,image/jpg" required="">
  35. <div class="form-text">请上传清晰的正面照片,确保光线充足,面部无遮挡</div>
  36. </div>
  37. <div class="mb-3">
  38. <div id="image-preview" class="text-center d-none">
  39. <img id="preview-img" src="#" alt="预览图" class="img-fluid rounded mb-2" style="max-height: 300px;">
  40. <button type="button" id="clear-preview" class="btn btn-sm btn-outline-danger">
  41. <i class="fas fa-times"></i> 清除
  42. </button>
  43. </div>
  44. </div>
  45. <div class="d-grid">
  46. <button type="submit" class="btn btn-primary">
  47. <i class="fas fa-upload me-2"></i>上传并注册
  48. </button>
  49. </div>
  50. </form>
  51. </div>
  52. </div>
  53. </div>
  54. <div class="col-md-6">
  55. <div class="card">
  56. <div class="card-header bg-light">
  57. <h5 class="mb-0">使用摄像头</h5>
  58. </div>
  59. <div class="card-body">
  60. <div class="text-center mb-3">
  61. <div id="camera-container">
  62. <video id="webcam" autoplay="" playsinline="" width="100%" class="rounded"></video>
  63. <canvas id="canvas" class="d-none"></canvas>
  64. </div>
  65. <div id="captured-container" class="d-none">
  66. <img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded mb-2">
  67. </div>
  68. </div>
  69. <div class="d-grid gap-2">
  70. <button id="start-camera" class="btn btn-info">
  71. <i class="fas fa-video me-2"></i>打开摄像头
  72. </button>
  73. <button id="capture-photo" class="btn btn-primary d-none">
  74. <i class="fas fa-camera me-2"></i>拍摄照片
  75. </button>
  76. <button id="retake-photo" class="btn btn-outline-secondary d-none">
  77. <i class="fas fa-redo me-2"></i>重新拍摄
  78. </button>
  79. <button id="save-photo" class="btn btn-success d-none">
  80. <i class="fas fa-save me-2"></i>保存并注册
  81. </button>
  82. </div>
  83. <div id="webcam-status" class="mt-2 text-center"></div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. <div class="card-footer">
  90. <div class="row">
  91. <div class="col-md-6">
  92. <a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-outline-secondary">
  93. <i class="fas fa-arrow-left me-1"></i>返回用户编辑
  94. </a>
  95. </div>
  96. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  97. <a href="{{ url_for('user_management') }}" class="btn btn-outline-primary">
  98. <i class="fas fa-users me-1"></i>返回用户列表
  99. </a>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. {% endblock %}
  107. {% block extra_js %}
  108. <script>
  109. // 照片上传预览
  110. document.getElementById('face_image').addEventListener('change', function(e) {
  111. const file = e.target.files[0];
  112. if (file) {
  113. const reader = new FileReader();
  114. reader.onload = function(event) {
  115. const previewImg = document.getElementById('preview-img');
  116. previewImg.src = event.target.result;
  117. document.getElementById('image-preview').classList.remove('d-none');
  118. };
  119. reader.readAsDataURL(file);
  120. }
  121. });
  122. document.getElementById('clear-preview').addEventListener('click', function() {
  123. document.getElementById('face_image').value = '';
  124. document.getElementById('image-preview').classList.add('d-none');
  125. });
  126. // 摄像头功能
  127. const startCameraBtn = document.getElementById('start-camera');
  128. const capturePhotoBtn = document.getElementById('capture-photo');
  129. const retakePhotoBtn = document.getElementById('retake-photo');
  130. const savePhotoBtn = document.getElementById('save-photo');
  131. const webcamVideo = document.getElementById('webcam');
  132. const canvas = document.getElementById('canvas');
  133. const capturedImage = document.getElementById('captured-image');
  134. const webcamContainer = document.getElementById('camera-container');
  135. const capturedContainer = document.getElementById('captured-container');
  136. const webcamStatus = document.getElementById('webcam-status');
  137. let stream = null;
  138. // 启动摄像头
  139. startCameraBtn.addEventListener('click', async function() {
  140. try {
  141. stream = await navigator.mediaDevices.getUserMedia({
  142. video: {
  143. width: { ideal: 640 },
  144. height: { ideal: 480 },
  145. facingMode: 'user'
  146. }
  147. });
  148. webcamVideo.srcObject = stream;
  149. startCameraBtn.classList.add('d-none');
  150. capturePhotoBtn.classList.remove('d-none');
  151. webcamStatus.innerHTML = '<span class="text-success">摄像头已启动</span>';
  152. } catch (err) {
  153. console.error('摄像头访问失败:', err);
  154. webcamStatus.innerHTML = '<span class="text-danger">无法访问摄像头: ' + err.message + '</span>';
  155. }
  156. });
  157. // 拍摄照片
  158. capturePhotoBtn.addEventListener('click', function() {
  159. canvas.width = webcamVideo.videoWidth;
  160. canvas.height = webcamVideo.videoHeight;
  161. const ctx = canvas.getContext('2d');
  162. ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
  163. capturedImage.src = canvas.toDataURL('image/jpeg');
  164. webcamContainer.classList.add('d-none');
  165. capturedContainer.classList.remove('d-none');
  166. capturePhotoBtn.classList.add('d-none');
  167. retakePhotoBtn.classList.remove('d-none');
  168. savePhotoBtn.classList.remove('d-none');
  169. });
  170. // 重新拍摄
  171. retakePhotoBtn.addEventListener('click', function() {
  172. webcamContainer.classList.remove('d-none');
  173. capturedContainer.classList.add('d-none');
  174. capturePhotoBtn.classList.remove('d-none');
  175. retakePhotoBtn.classList.add('d-none');
  176. savePhotoBtn.classList.add('d-none');
  177. });
  178. // 保存照片并注册
  179. savePhotoBtn.addEventListener('click', function() {
  180. const imageData = capturedImage.src;
  181. // 显示加载状态
  182. savePhotoBtn.disabled = true;
  183. savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
  184. // 发送到服务器
  185. fetch('{{ url_for("webcam_registration") }}', {
  186. method: 'POST',
  187. headers: {
  188. 'Content-Type': 'application/x-www-form-urlencoded',
  189. },
  190. body: 'image_data=' + encodeURIComponent(imageData) + '&user_id={{ user.id }}'
  191. })
  192. .then(response => response.json())
  193. .then(data => {
  194. if (data.success) {
  195. // 注册成功
  196. webcamStatus.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
  197. // 停止摄像头
  198. if (stream) {
  199. stream.getTracks().forEach(track => track.stop());
  200. }
  201. // 3秒后跳转到用户编辑页面
  202. setTimeout(() => {
  203. window.location.href = '{{ url_for("edit_user", user_id=user.id) }}';
  204. }, 3000);
  205. } else {
  206. // 注册失败
  207. webcamStatus.innerHTML = '<div class="alert alert-danger">' + data.message + '</div>';
  208. savePhotoBtn.disabled = false;
  209. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  210. // 重置为拍摄状态
  211. setTimeout(() => {
  212. retakePhotoBtn.click();
  213. }, 2000);
  214. }
  215. })
  216. .catch(error => {
  217. console.error('Error:', error);
  218. webcamStatus.innerHTML = '<div class="alert alert-danger">服务器错误,请稍后重试</div>';
  219. savePhotoBtn.disabled = false;
  220. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  221. });
  222. });
  223. // 页面卸载时停止摄像头
  224. window.addEventListener('beforeunload', function() {
  225. if (stream) {
  226. stream.getTracks().forEach(track => track.stop());
  227. }
  228. });
  229. </script>
  230. {% endblock %}
复制代码

templates\index.html

  1. {% extends 'base.html' %}
  2. {% block title %}首页 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row align-items-center">
  5. <div class="col-lg-6">
  6. <h1 class="display-4 fw-bold mb-4">智能校园考勤系统</h1>
  7. <p class="lead mb-4">基于深度学习的人脸识别技术,为校园考勤带来全新体验。告别传统签到方式,实现快速、准确、高效的智能考勤管理。</p>
  8. <div class="d-grid gap-2 d-md-flex justify-content-md-start mb-4">
  9. {% if session.get('user_id') %}
  10. <a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-primary btn-lg px-4 me-md-2">开始考勤</a>
  11. <a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary btn-lg px-4">控制面板</a>
  12. {% else %}
  13. <a href="{{ url_for('login') }}" class="btn btn-primary btn-lg px-4 me-md-2">登录系统</a>
  14. <a href="{{ url_for('register') }}" class="btn btn-outline-secondary btn-lg px-4">注册账号</a>
  15. {% endif %}
  16. </div>
  17. </div>
  18. <div class="col-lg-6">
  19. <img src="https://source.unsplash.com/random/600x400/?face,technology" class="img-fluid rounded shadow" alt="人脸识别技术">
  20. </div>
  21. </div>
  22. <div class="row mt-5 pt-5">
  23. <div class="col-12 text-center">
  24. <h2 class="mb-4">系统特点</h2>
  25. </div>
  26. </div>
  27. <div class="row g-4 py-3">
  28. <div class="col-md-4">
  29. <div class="card h-100 shadow-sm">
  30. <div class="card-body text-center">
  31. <i class="fas fa-bolt text-primary fa-3x mb-3"></i>
  32. <h3 class="card-title">快速识别</h3>
  33. <p class="card-text">采用先进的深度学习算法,实现毫秒级人脸识别,大幅提高考勤效率。</p>
  34. </div>
  35. </div>
  36. </div>
  37. <div class="col-md-4">
  38. <div class="card h-100 shadow-sm">
  39. <div class="card-body text-center">
  40. <i class="fas fa-shield-alt text-primary fa-3x mb-3"></i>
  41. <h3 class="card-title">安全可靠</h3>
  42. <p class="card-text">人脸特征加密存储,确保用户隐私安全,防止冒名顶替,提高考勤准确性。</p>
  43. </div>
  44. </div>
  45. </div>
  46. <div class="col-md-4">
  47. <div class="card h-100 shadow-sm">
  48. <div class="card-body text-center">
  49. <i class="fas fa-chart-line text-primary fa-3x mb-3"></i>
  50. <h3 class="card-title">数据分析</h3>
  51. <p class="card-text">自动生成考勤统计报表,提供直观的数据可视化,辅助教学管理决策。</p>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="row mt-5 pt-3">
  57. <div class="col-12 text-center">
  58. <h2 class="mb-4">使用流程</h2>
  59. </div>
  60. </div>
  61. <div class="row">
  62. <div class="col-12">
  63. <div class="steps">
  64. <div class="step-item">
  65. <div class="step-number">1</div>
  66. <div class="step-content">
  67. <h4>注册账号</h4>
  68. <p>创建个人账号,填写基本信息</p>
  69. </div>
  70. </div>
  71. <div class="step-item">
  72. <div class="step-number">2</div>
  73. <div class="step-content">
  74. <h4>人脸录入</h4>
  75. <p>上传照片或使用摄像头采集人脸数据</p>
  76. </div>
  77. </div>
  78. <div class="step-item">
  79. <div class="step-number">3</div>
  80. <div class="step-content">
  81. <h4>日常考勤</h4>
  82. <p>通过人脸识别快速完成签到签退</p>
  83. </div>
  84. </div>
  85. <div class="step-item">
  86. <div class="step-number">4</div>
  87. <div class="step-content">
  88. <h4>查看记录</h4>
  89. <p>随时查看个人考勤记录和统计数据</p>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. {% endblock %}
  96. {% block extra_css %}
  97. <style>
  98. .steps {
  99. display: flex;
  100. justify-content: space-between;
  101. margin: 2rem 0;
  102. position: relative;
  103. }
  104. .steps:before {
  105. content: '';
  106. position: absolute;
  107. top: 30px;
  108. left: 0;
  109. right: 0;
  110. height: 2px;
  111. background: #e9ecef;
  112. z-index: -1;
  113. }
  114. .step-item {
  115. text-align: center;
  116. flex: 1;
  117. position: relative;
  118. }
  119. .step-number {
  120. width: 60px;
  121. height: 60px;
  122. border-radius: 50%;
  123. background: #0d6efd;
  124. color: white;
  125. font-size: 1.5rem;
  126. font-weight: bold;
  127. display: flex;
  128. align-items: center;
  129. justify-content: center;
  130. margin: 0 auto 1rem;
  131. }
  132. .step-content h4 {
  133. margin-bottom: 0.5rem;
  134. }
  135. .step-content p {
  136. color: #6c757d;
  137. }
  138. </style>
  139. {% endblock %}
复制代码

templates\login.html

  1. {% extends 'base.html' %}
  2. {% block title %}登录 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-6">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>用户登录</h4>
  9. </div>
  10. <div class="card-body">
  11. <form method="POST" action="{{ url_for('login') }}">
  12. <div class="mb-3">
  13. <label for="student_id" class="form-label">学号</label>
  14. <div class="input-group">
  15. <span class="input-group-text"><i class="fas fa-id-card"></i></span>
  16. <input type="text" class="form-control" id="student_id" name="student_id" required="" autofocus="">
  17. </div>
  18. </div>
  19. <div class="mb-3">
  20. <label for="password" class="form-label">密码</label>
  21. <div class="input-group">
  22. <span class="input-group-text"><i class="fas fa-lock"></i></span>
  23. <input type="password" class="form-control" id="password" name="password" required="">
  24. </div>
  25. </div>
  26. <div class="d-grid gap-2">
  27. <button type="submit" class="btn btn-primary">登录</button>
  28. </div>
  29. </form>
  30. </div>
  31. <div class="card-footer text-center">
  32. <p class="mb-0">还没有账号? <a href="{{ url_for('register') }}">立即注册</a></p>
  33. </div>
  34. </div>
  35. <div class="card mt-4 shadow">
  36. <div class="card-header bg-info text-white">
  37. <h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>人脸识别登录</h5>
  38. </div>
  39. <div class="card-body text-center">
  40. <p>您也可以使用人脸识别功能直接考勤</p>
  41. <a href="{{ url_for('face_recognition_attendance') }}" class="btn btn-info">
  42. <i class="fas fa-camera me-2"></i>人脸识别考勤
  43. </a>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. {% endblock %}
复制代码

templates\register.html

  1. {% extends 'base.html' %}
  2. {% block title %}注册 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-8">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-user-plus me-2"></i>用户注册</h4>
  9. </div>
  10. <div class="card-body">
  11. <form method="POST" action="{{ url_for('register') }}">
  12. <div class="row">
  13. <div class="col-md-6 mb-3">
  14. <label for="student_id" class="form-label">学号 <span class="text-danger">*</span></label>
  15. <div class="input-group">
  16. <span class="input-group-text"><i class="fas fa-id-card"></i></span>
  17. <input type="text" class="form-control" id="student_id" name="student_id" required="" autofocus="">
  18. </div>
  19. <div class="form-text">请输入您的学号,将作为登录账号使用</div>
  20. </div>
  21. <div class="col-md-6 mb-3">
  22. <label for="name" class="form-label">姓名 <span class="text-danger">*</span></label>
  23. <div class="input-group">
  24. <span class="input-group-text"><i class="fas fa-user"></i></span>
  25. <input type="text" class="form-control" id="name" name="name" required="">
  26. </div>
  27. </div>
  28. </div>
  29. <div class="mb-3">
  30. <label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
  31. <div class="input-group">
  32. <span class="input-group-text"><i class="fas fa-envelope"></i></span>
  33. <input type="email" class="form-control" id="email" name="email" required="">
  34. </div>
  35. <div class="form-text">请输入有效的电子邮箱,用于接收系统通知</div>
  36. </div>
  37. <div class="row">
  38. <div class="col-md-6 mb-3">
  39. <label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
  40. <div class="input-group">
  41. <span class="input-group-text"><i class="fas fa-lock"></i></span>
  42. <input type="password" class="form-control" id="password" name="password" required="">
  43. </div>
  44. <div class="form-text">密码长度至少为6位,包含字母和数字</div>
  45. </div>
  46. <div class="col-md-6 mb-3">
  47. <label for="confirm_password" class="form-label">确认密码 <span class="text-danger">*</span></label>
  48. <div class="input-group">
  49. <span class="input-group-text"><i class="fas fa-lock"></i></span>
  50. <input type="password" class="form-control" id="confirm_password" name="confirm_password" required="">
  51. </div>
  52. <div class="form-text">请再次输入密码进行确认</div>
  53. </div>
  54. </div>
  55. <div class="mb-3 form-check">
  56. <input type="checkbox" class="form-check-input" id="terms" required="">
  57. <label class="form-check-label" for="terms">我已阅读并同意 <a href="#" data-bs-toggle="modal" data-bs-target="#termsModal">用户协议</a> 和 <a href="#" data-bs-toggle="modal" data-bs-target="#privacyModal">隐私政策</a></label>
  58. </div>
  59. <div class="d-grid gap-2">
  60. <button type="submit" class="btn btn-primary btn-lg">注册账号</button>
  61. </div>
  62. </form>
  63. </div>
  64. <div class="card-footer text-center">
  65. <p class="mb-0">已有账号? <a href="{{ url_for('login') }}">立即登录</a></p>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. <!-- Terms Modal -->
  71. <div class="modal fade" id="termsModal" tabindex="-1" aria-labelledby="termsModalLabel" aria-hidden="true">
  72. <div class="modal-dialog modal-lg">
  73. <div class="modal-content">
  74. <div class="modal-header">
  75. <h5 class="modal-title" id="termsModalLabel">用户协议</h5>
  76. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  77. </div>
  78. <div class="modal-body">
  79. <h5>校园人脸识别考勤系统用户协议</h5>
  80. <p>欢迎使用校园人脸识别考勤系统。请仔细阅读以下条款,注册即表示您同意接受本协议的所有条款。</p>
  81. <h6>1. 服务说明</h6>
  82. <p>校园人脸识别考勤系统(以下简称"本系统")是一款基于深度学习的人脸识别考勤系统,为用户提供自动化考勤服务。</p>
  83. <h6>2. 用户注册与账号安全</h6>
  84. <p>2.1 用户在注册时需要提供真实、准确、完整的个人资料。<br>
  85. 2.2 用户应妥善保管账号和密码,因账号和密码泄露导致的一切损失由用户自行承担。<br>
  86. 2.3 用户注册成功后,需要上传本人的人脸数据用于识别。</p>
  87. <h6>3. 用户行为规范</h6>
  88. <p>3.1 用户不得利用本系统进行任何违法或不当的活动。<br>
  89. 3.2 用户不得尝试破解、篡改或干扰本系统的正常运行。<br>
  90. 3.3 用户不得上传非本人的人脸数据,或尝试冒充他人进行考勤。</p>
  91. <h6>4. 隐私保护</h6>
  92. <p>4.1 本系统重视用户隐私保护,收集的个人信息和人脸数据仅用于考勤目的。<br>
  93. 4.2 未经用户同意,本系统不会向第三方披露用户个人信息。<br>
  94. 4.3 详细隐私政策请参阅《隐私政策》。</p>
  95. <h6>5. 免责声明</h6>
  96. <p>5.1 本系统不保证服务不会中断,对系统的及时性、安全性、准确性也不作保证。<br>
  97. 5.2 因网络状况、通讯线路、第三方网站或管理部门的要求等任何原因而导致的服务中断或其他缺陷,本系统不承担任何责任。</p>
  98. <h6>6. 协议修改</h6>
  99. <p>本系统有权在必要时修改本协议条款,修改后的协议一旦公布即代替原协议。用户可在本系统查阅最新版协议条款。</p>
  100. <h6>7. 适用法律</h6>
  101. <p>本协议的订立、执行和解释及争议的解决均应适用中国法律。</p>
  102. </div>
  103. <div class="modal-footer">
  104. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. <!-- Privacy Modal -->
  110. <div class="modal fade" id="privacyModal" tabindex="-1" aria-labelledby="privacyModalLabel" aria-hidden="true">
  111. <div class="modal-dialog modal-lg">
  112. <div class="modal-content">
  113. <div class="modal-header">
  114. <h5 class="modal-title" id="privacyModalLabel">隐私政策</h5>
  115. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  116. </div>
  117. <div class="modal-body">
  118. <h5>校园人脸识别考勤系统隐私政策</h5>
  119. <p>本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。请在使用本系统前仔细阅读本政策。</p>
  120. <h6>1. 信息收集</h6>
  121. <p>1.1 基本信息:我们收集您的学号、姓名、电子邮箱等基本信息。<br>
  122. 1.2 人脸数据:我们收集您的人脸图像并提取特征向量用于身份识别。<br>
  123. 1.3 考勤记录:我们记录您的考勤时间和考勤状态。</p>
  124. <h6>2. 信息使用</h6>
  125. <p>2.1 您的个人信息和人脸数据仅用于身份验证和考勤记录目的。<br>
  126. 2.2 我们不会将您的个人信息用于与考勤无关的其他目的。<br>
  127. 2.3 未经您的明确许可,我们不会向任何第三方提供您的个人信息。</p>
  128. <h6>3. 信息存储与保护</h6>
  129. <p>3.1 您的人脸特征数据以加密形式存储在我们的数据库中。<br>
  130. 3.2 我们采取适当的技术和组织措施来保护您的个人信息不被未经授权的访问、使用或泄露。<br>
  131. 3.3 我们定期审查我们的信息收集、存储和处理实践,以防止未经授权的访问和使用。</p>
  132. <h6>4. 信息保留</h6>
  133. <p>4.1 我们仅在必要的时间内保留您的个人信息,以实现本政策中所述的目的。<br>
  134. 4.2 当您不再使用本系统时,您可以要求我们删除您的个人信息和人脸数据。</p>
  135. <h6>5. 您的权利</h6>
  136. <p>5.1 您有权访问、更正或删除您的个人信息。<br>
  137. 5.2 您有权随时撤回您对收集和使用您个人信息的同意。<br>
  138. 5.3 如需行使上述权利,请联系系统管理员。</p>
  139. <h6>6. 政策更新</h6>
  140. <p>我们可能会不时更新本隐私政策。任何重大变更都会通过电子邮件或系统通知的形式通知您。</p>
  141. <h6>7. 联系我们</h6>
  142. <p>如果您对本隐私政策有任何疑问或建议,请联系系统管理员。</p>
  143. </div>
  144. <div class="modal-footer">
  145. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. {% endblock %}
  151. {% block extra_js %}
  152. <script>
  153. // 密码一致性验证
  154. document.getElementById('confirm_password').addEventListener('input', function() {
  155. const password = document.getElementById('password').value;
  156. const confirmPassword = this.value;
  157. if (password !== confirmPassword) {
  158. this.setCustomValidity('两次输入的密码不一致');
  159. } else {
  160. this.setCustomValidity('');
  161. }
  162. });
  163. // 密码强度验证
  164. document.getElementById('password').addEventListener('input', function() {
  165. const password = this.value;
  166. const hasLetter = /[a-zA-Z]/.test(password);
  167. const hasNumber = /[0-9]/.test(password);
  168. const isLongEnough = password.length >= 6;
  169. if (!hasLetter || !hasNumber || !isLongEnough) {
  170. this.setCustomValidity('密码必须至少包含6个字符,包括字母和数字');
  171. } else {
  172. this.setCustomValidity('');
  173. }
  174. });
  175. </script>
  176. {% endblock %}
复制代码

templates\user_management.html

  1. {% extends 'base.html' %}
  2. {% block title %}用户管理 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="card shadow">
  5. <div class="card-header bg-primary text-white">
  6. <h4 class="mb-0"><i class="fas fa-users-cog me-2"></i>用户管理</h4>
  7. </div>
  8. <div class="card-body">
  9. <div class="row mb-4">
  10. <div class="col-md-6">
  11. <form method="GET" action="{{ url_for('user_management') }}" class="d-flex">
  12. <input type="text" class="form-control me-2" name="search" placeholder="搜索学号或姓名" value="{{ search_query }}">
  13. <button type="submit" class="btn btn-primary">
  14. <i class="fas fa-search me-1"></i>搜索
  15. </button>
  16. </form>
  17. </div>
  18. <div class="col-md-6 text-md-end mt-3 mt-md-0">
  19. <a href="{{ url_for('register') }}" class="btn btn-success">
  20. <i class="fas fa-user-plus me-1"></i>添加用户
  21. </a>
  22. </div>
  23. </div>
  24. {% if users %}
  25. <div class="table-responsive">
  26. {% for user in users %}
  27. {% endfor %}
  28. <table class="table table-hover table-striped">
  29. <thead class="table-light">
  30. <tr>
  31. <th>学号</th>
  32. <th>姓名</th>
  33. <th>邮箱</th>
  34. <th>注册日期</th>
  35. <th>人脸数据</th>
  36. <th>操作</th>
  37. </tr>
  38. </thead>
  39. <tbody><tr>
  40. <td>{{ user.student_id }}</td>
  41. <td>{{ user.name }}</td>
  42. <td>{{ user.email }}</td>
  43. <td>{{ user.registration_date }}</td>
  44. <td>
  45. {% if user.has_face_data %}
  46. <span class="badge bg-success">已注册</span>
  47. {% else %}
  48. <span class="badge bg-warning">未注册</span>
  49. {% endif %}
  50. </td>
  51. <td>
  52. <div class="btn-group btn-group-sm">
  53. <a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-outline-primary">
  54. <i class="fas fa-edit"></i>
  55. </a>
  56. <button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ user.id }}">
  57. <i class="fas fa-trash-alt"></i>
  58. </button>
  59. {% if not user.has_face_data %}
  60. <a href="{{ url_for('face_registration_admin', user_id=user.id) }}" class="btn btn-outline-success">
  61. <i class="fas fa-camera"></i>
  62. </a>
  63. {% endif %}
  64. </div>
  65. <!-- Delete Modal -->
  66. <div class="modal fade" id="deleteModal{{ user.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ user.id }}" aria-hidden="true">
  67. <div class="modal-dialog">
  68. <div class="modal-content">
  69. <div class="modal-header">
  70. <h5 class="modal-title" id="deleteModalLabel{{ user.id }}">确认删除</h5>
  71. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  72. </div>
  73. <div class="modal-body">
  74. <p>确定要删除用户 <strong>{{ user.name }}</strong> ({{ user.student_id }}) 吗?</p>
  75. <p class="text-danger">此操作不可逆,用户的所有数据(包括考勤记录和人脸数据)将被永久删除。</p>
  76. </div>
  77. <div class="modal-footer">
  78. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
  79. <form action="{{ url_for('delete_user', user_id=user.id) }}" method="POST" style="display: inline;">
  80. <button type="submit" class="btn btn-danger">确认删除</button>
  81. </form>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </td>
  87. </tr></tbody>
  88. </table>
  89. </div>
  90. <!-- Pagination -->
  91. {% if total_pages > 1 %}
  92. <nav aria-label="Page navigation">
  93. <ul class="pagination justify-content-center">
  94. <li class="page-item {{ 'disabled' if current_page == 1 else '' }}">
  95. <a class="page-link" href="{{ url_for('user_management', page=current_page-1, search=search_query) }}" aria-label="Previous">
  96. <span aria-hidden="true">«</span>
  97. </a>
  98. </li>
  99. {% for i in range(1, total_pages + 1) %}
  100. <li class="page-item {{ 'active' if i == current_page else '' }}">
  101. <a class="page-link" href="{{ url_for('user_management', page=i, search=search_query) }}">{{ i }}</a>
  102. </li>
  103. {% endfor %}
  104. <li class="page-item {{ 'disabled' if current_page == total_pages else '' }}">
  105. <a class="page-link" href="{{ url_for('user_management', page=current_page+1, search=search_query) }}" aria-label="Next">
  106. <span aria-hidden="true">»</span>
  107. </a>
  108. </li>
  109. </ul>
  110. </nav>
  111. {% endif %}
  112. {% else %}
  113. <div class="alert alert-info">
  114. <i class="fas fa-info-circle me-2"></i>没有找到用户记录
  115. </div>
  116. {% endif %}
  117. </div>
  118. <div class="card-footer">
  119. <div class="row">
  120. <div class="col-md-6">
  121. <button class="btn btn-outline-primary" onclick="window.print()">
  122. <i class="fas fa-print me-1"></i>打印用户列表
  123. </button>
  124. </div>
  125. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  126. <a href="#" class="btn btn-outline-success" id="exportBtn">
  127. <i class="fas fa-file-excel me-1"></i>导出Excel
  128. </a>
  129. </div>
  130. </div>
  131. </div>
  132. </div>
  133. {% endblock %}
  134. {% block extra_js %}
  135. <script>
  136. // 导出Excel功能
  137. document.getElementById('exportBtn').addEventListener('click', function(e) {
  138. e.preventDefault();
  139. alert('导出功能将在完整版中提供');
  140. });
  141. </script>
  142. {% endblock %}
复制代码

templates\webcam_registration.html

  1. {% extends 'base.html' %}
  2. {% block title %}摄像头人脸注册 - 校园人脸识别考勤系统{% endblock %}
  3. {% block content %}
  4. <div class="row justify-content-center">
  5. <div class="col-md-8">
  6. <div class="card shadow">
  7. <div class="card-header bg-primary text-white">
  8. <h4 class="mb-0"><i class="fas fa-camera me-2"></i>摄像头人脸注册</h4>
  9. </div>
  10. <div class="card-body">
  11. <div class="text-center mb-4">
  12. <h5 class="mb-3">请面向摄像头,确保光线充足,面部清晰可见</h5>
  13. <div class="alert alert-info">
  14. <i class="fas fa-info-circle me-2"></i>请保持自然表情,正面面对摄像头
  15. </div>
  16. </div>
  17. <div class="row">
  18. <div class="col-md-8 mx-auto">
  19. <div id="camera-container" class="position-relative">
  20. <video id="webcam" autoplay="" playsinline="" width="100%" class="rounded border"></video>
  21. <div id="face-overlay" class="position-absolute top-0 start-0 w-100 h-100"></div>
  22. <canvas id="canvas" class="d-none"></canvas>
  23. </div>
  24. <div id="captured-container" class="d-none text-center mt-3">
  25. <img id="captured-image" src="#" alt="已拍摄照片" class="img-fluid rounded border" style="max-height: 300px;">
  26. </div>
  27. <div id="registration-status" class="text-center mt-3">
  28. <div class="alert alert-secondary">
  29. <i class="fas fa-info-circle me-2"></i>请点击下方按钮启动摄像头
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. <div class="row mt-4">
  35. <div class="col-md-8 mx-auto">
  36. <div class="d-grid gap-2">
  37. <button id="start-camera" class="btn btn-primary">
  38. <i class="fas fa-video me-2"></i>启动摄像头
  39. </button>
  40. <button id="capture-photo" class="btn btn-success d-none">
  41. <i class="fas fa-camera me-2"></i>拍摄照片
  42. </button>
  43. <button id="retake-photo" class="btn btn-outline-secondary d-none">
  44. <i class="fas fa-redo me-2"></i>重新拍摄
  45. </button>
  46. <button id="save-photo" class="btn btn-primary d-none">
  47. <i class="fas fa-save me-2"></i>保存并注册
  48. </button>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="card-footer">
  54. <div class="row">
  55. <div class="col-md-6">
  56. <a href="{{ url_for('face_registration') }}" class="btn btn-outline-secondary">
  57. <i class="fas fa-arrow-left me-1"></i>返回上传方式
  58. </a>
  59. </div>
  60. <div class="col-md-6 text-md-end mt-2 mt-md-0">
  61. <a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">
  62. <i class="fas fa-home me-1"></i>返回控制面板
  63. </a>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. {% endblock %}
  71. {% block extra_css %}
  72. <style>
  73. #camera-container {
  74. max-width: 640px;
  75. margin: 0 auto;
  76. border-radius: 0.25rem;
  77. overflow: hidden;
  78. }
  79. #face-overlay {
  80. pointer-events: none;
  81. }
  82. .face-box {
  83. position: absolute;
  84. border: 2px solid #28a745;
  85. border-radius: 4px;
  86. }
  87. .face-label {
  88. position: absolute;
  89. background-color: rgba(40, 167, 69, 0.8);
  90. color: white;
  91. padding: 2px 6px;
  92. border-radius: 2px;
  93. font-size: 12px;
  94. top: -20px;
  95. left: 0;
  96. }
  97. .processing-indicator {
  98. position: absolute;
  99. top: 50%;
  100. left: 50%;
  101. transform: translate(-50%, -50%);
  102. background-color: rgba(0, 0, 0, 0.7);
  103. color: white;
  104. padding: 10px 20px;
  105. border-radius: 4px;
  106. font-size: 14px;
  107. }
  108. @keyframes pulse {
  109. 0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7); }
  110. 70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
  111. 100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
  112. }
  113. .pulse {
  114. animation: pulse 1.5s infinite;
  115. }
  116. </style>
  117. {% endblock %}
  118. {% block extra_js %}
  119. <script>
  120. const startCameraBtn = document.getElementById('start-camera');
  121. const capturePhotoBtn = document.getElementById('capture-photo');
  122. const retakePhotoBtn = document.getElementById('retake-photo');
  123. const savePhotoBtn = document.getElementById('save-photo');
  124. const webcamVideo = document.getElementById('webcam');
  125. const canvas = document.getElementById('canvas');
  126. const capturedImage = document.getElementById('captured-image');
  127. const cameraContainer = document.getElementById('camera-container');
  128. const capturedContainer = document.getElementById('captured-container');
  129. const faceOverlay = document.getElementById('face-overlay');
  130. const registrationStatus = document.getElementById('registration-status');
  131. let stream = null;
  132. // 启动摄像头
  133. startCameraBtn.addEventListener('click', async function() {
  134. try {
  135. stream = await navigator.mediaDevices.getUserMedia({
  136. video: {
  137. width: { ideal: 640 },
  138. height: { ideal: 480 },
  139. facingMode: 'user'
  140. }
  141. });
  142. webcamVideo.srcObject = stream;
  143. startCameraBtn.classList.add('d-none');
  144. capturePhotoBtn.classList.remove('d-none');
  145. registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>摄像头已启动,请面向摄像头</div>';
  146. // 添加脉冲效果
  147. webcamVideo.classList.add('pulse');
  148. // 检测人脸
  149. detectFace();
  150. } catch (err) {
  151. console.error('摄像头访问失败:', err);
  152. registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>无法访问摄像头: ' + err.message + '</div>';
  153. }
  154. });
  155. // 模拟人脸检测
  156. function detectFace() {
  157. // 这里仅作为UI示例,实际人脸检测应在服务器端进行
  158. setTimeout(() => {
  159. if (stream && stream.active) {
  160. const videoWidth = webcamVideo.videoWidth;
  161. const videoHeight = webcamVideo.videoHeight;
  162. const scale = webcamVideo.offsetWidth / videoWidth;
  163. // 人脸框位置(居中)
  164. const faceWidth = videoWidth * 0.4;
  165. const faceHeight = videoHeight * 0.5;
  166. const faceLeft = (videoWidth - faceWidth) / 2;
  167. const faceTop = (videoHeight - faceHeight) / 2;
  168. // 创建人脸框元素
  169. const faceBox = document.createElement('div');
  170. faceBox.className = 'face-box';
  171. faceBox.style.left = (faceLeft * scale) + 'px';
  172. faceBox.style.top = (faceTop * scale) + 'px';
  173. faceBox.style.width = (faceWidth * scale) + 'px';
  174. faceBox.style.height = (faceHeight * scale) + 'px';
  175. faceOverlay.innerHTML = '';
  176. faceOverlay.appendChild(faceBox);
  177. registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>检测到人脸,可以进行拍摄</div>';
  178. }
  179. }, 1500);
  180. }
  181. // 拍摄照片
  182. capturePhotoBtn.addEventListener('click', function() {
  183. canvas.width = webcamVideo.videoWidth;
  184. canvas.height = webcamVideo.videoHeight;
  185. const ctx = canvas.getContext('2d');
  186. ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
  187. capturedImage.src = canvas.toDataURL('image/jpeg');
  188. cameraContainer.classList.add('d-none');
  189. capturedContainer.classList.remove('d-none');
  190. capturePhotoBtn.classList.add('d-none');
  191. retakePhotoBtn.classList.remove('d-none');
  192. savePhotoBtn.classList.remove('d-none');
  193. registrationStatus.innerHTML = '<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>请确认照片清晰可见,如不满意可重新拍摄</div>';
  194. });
  195. // 重新拍摄
  196. retakePhotoBtn.addEventListener('click', function() {
  197. cameraContainer.classList.remove('d-none');
  198. capturedContainer.classList.add('d-none');
  199. capturePhotoBtn.classList.remove('d-none');
  200. retakePhotoBtn.classList.add('d-none');
  201. savePhotoBtn.classList.add('d-none');
  202. faceOverlay.innerHTML = '';
  203. registrationStatus.innerHTML = '<div class="alert alert-secondary"><i class="fas fa-info-circle me-2"></i>请重新面向摄像头</div>';
  204. // 重新检测人脸
  205. detectFace();
  206. });
  207. // 保存照片并注册
  208. savePhotoBtn.addEventListener('click', function() {
  209. const imageData = capturedImage.src;
  210. // 显示加载状态
  211. savePhotoBtn.disabled = true;
  212. savePhotoBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 处理中...';
  213. // 发送到服务器
  214. fetch('{{ url_for("webcam_registration") }}', {
  215. method: 'POST',
  216. headers: {
  217. 'Content-Type': 'application/x-www-form-urlencoded',
  218. },
  219. body: 'image_data=' + encodeURIComponent(imageData)
  220. })
  221. .then(response => response.json())
  222. .then(data => {
  223. if (data.success) {
  224. // 注册成功
  225. registrationStatus.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>' + data.message + '</div>';
  226. // 停止摄像头
  227. if (stream) {
  228. stream.getTracks().forEach(track => track.stop());
  229. }
  230. // 禁用所有按钮
  231. retakePhotoBtn.disabled = true;
  232. savePhotoBtn.disabled = true;
  233. // 3秒后跳转到控制面板
  234. setTimeout(() => {
  235. window.location.href = '{{ url_for("dashboard") }}';
  236. }, 3000);
  237. } else {
  238. // 注册失败
  239. registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>' + data.message + '</div>';
  240. savePhotoBtn.disabled = false;
  241. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  242. // 重置为拍摄状态
  243. setTimeout(() => {
  244. retakePhotoBtn.click();
  245. }, 2000);
  246. }
  247. })
  248. .catch(error => {
  249. console.error('Error:', error);
  250. registrationStatus.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>服务器错误,请稍后重试</div>';
  251. savePhotoBtn.disabled = false;
  252. savePhotoBtn.innerHTML = '<i class="fas fa-save me-2"></i>保存并注册';
  253. });
  254. });
  255. // 页面卸载时停止摄像头
  256. window.addEventListener('beforeunload', function() {
  257. if (stream) {
  258. stream.getTracks().forEach(track => track.stop());
  259. }
  260. });
  261. </script>
  262. {% endblock %}
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

荣誉红客

关注
  • 4008
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行