MogFace人脸检测模型数据库集成案例:构建人脸信息管理系统
MogFace人脸检测模型数据库集成案例:构建人脸信息管理系统
最近在帮一个朋友的公司做个小项目,他们需要管理大量的人脸图片数据,比如员工打卡照片、访客登记照片这些。传统做法就是人工整理文件夹,找起来麻烦不说,统计个数据还得一张张数,效率实在太低。
我就想,能不能用AI自动识别人脸,然后把识别结果存到数据库里,做个简单的管理系统?这样既能自动统计人脸数量,又能快速查询历史记录。正好之前用过MogFace这个人脸检测模型,效果不错,就决定用它来试试。
今天就跟大家分享一下这个项目的完整实现过程。你会发现,把AI模型和传统数据库结合起来,其实没想象中那么复杂,而且能解决很多实际的管理问题。
1. 系统整体设计思路
先说说这个系统要干什么。简单来说,就是用户通过网页上传一张图片,系统自动检测图片里有几张人脸,然后把图片信息、检测时间、人脸数量这些数据存到数据库里。之后可以在网页上查看所有记录,还能按时间、按人脸数量做统计。
听起来是不是挺实用的?很多场景都能用上,比如小区门禁系统记录访客、公司统计会议出席人数、甚至学校统计活动参与人数等等。
整个系统我分成了三个部分:
- 前端:一个简单的网页,用来上传图片和展示结果
- 后端:处理图片、调用MogFace模型、操作数据库
- 数据库:MySQL,存储所有检测记录
技术选型上,我用了Flask做后端框架,因为它轻量简单,适合这种小项目。前端就用基本的HTML+CSS+JavaScript,没用什么复杂框架。数据库当然是MySQL,毕竟大家都熟悉,社区资源也多。
2. 环境准备与快速部署
2.1 安装MySQL数据库
如果你还没装MySQL,这里简单说一下安装步骤。我用的是Ubuntu系统,其他系统也差不多。
# 更新包列表 sudo apt update # 安装MySQL服务器 sudo apt install mysql-server # 启动MySQL服务 sudo systemctl start mysql # 设置开机自启 sudo systemctl enable mysql # 运行安全脚本(设置root密码等) sudo mysql_secure_installation安装过程中,会提示你设置root密码,记得选个复杂点的。安全脚本还会问你要不要移除匿名用户、禁止root远程登录这些,建议都选“是”,安全第一。
安装完成后,登录MySQL创建我们的数据库:
# 登录MySQL sudo mysql -u root -p # 创建数据库 CREATE DATABASE face_management; # 创建用户并授权 CREATE USER 'face_user'@'localhost' IDENTIFIED BY 'your_password_here'; GRANT ALL PRIVILEGES ON face_management.* TO 'face_user'@'localhost'; FLUSH PRIVILEGES; # 退出 EXIT;2.2 安装Python依赖
接下来准备Python环境。建议用虚拟环境,避免包冲突。
# 创建项目目录 mkdir face-management-system cd face-management-system # 创建虚拟环境 python3 -m venv venv # 激活虚拟环境 source venv/bin/activate # Linux/Mac # 或者 venv\Scripts\activate # Windows # 安装依赖 pip install flask flask-cors mysql-connector-python opencv-python pillow numpy torch torchvision这里安装了几个关键包:
flask:Web框架mysql-connector-python:连接MySQLopencv-python:处理图片torch:PyTorch,运行MogFace模型需要
2.3 下载MogFace模型
MogFace是个轻量级的人脸检测模型,效果不错,速度也快。我们可以从GitHub上找到预训练模型。
import torch import urllib.request import os # 创建模型目录 os.makedirs('models', exist_ok=True) # MogFace模型下载地址(示例,实际地址可能需要更新) model_url = "https://github.com/xxx/mogface/raw/main/weights/mogface.pth" model_path = "models/mogface.pth" # 下载模型 if not os.path.exists(model_path): print("正在下载MogFace模型...") urllib.request.urlretrieve(model_url, model_path) print("模型下载完成")如果下载地址失效,可以去MogFace的GitHub仓库找最新的模型文件。
3. 数据库设计与实现
3.1 设计数据表结构
我们要存哪些信息呢?我想了想,至少需要这些字段:
- 图片信息:图片保存的路径、上传时间
- 检测结果:检测到的人脸数量、检测耗时
- 人脸详情:每个人脸的位置(边界框坐标)、置信度
- 系统信息:记录ID、检测状态
基于这些需求,我设计了两张表:
-- 切换到我们的数据库 USE face_management; -- 创建检测记录表 CREATE TABLE detection_records ( id INT AUTO_INCREMENT PRIMARY KEY, image_path VARCHAR(500) NOT NULL, upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, detection_time TIMESTAMP NULL, face_count INT DEFAULT 0, detection_duration FLOAT NULL, status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 创建人脸详情表 CREATE TABLE face_details ( id INT AUTO_INCREMENT PRIMARY KEY, record_id INT NOT NULL, face_index INT NOT NULL, x_min INT NOT NULL, y_min INT NOT NULL, x_max INT NOT NULL, y_max INT NOT NULL, confidence FLOAT NOT NULL, FOREIGN KEY (record_id) REFERENCES detection_records(id) ON DELETE CASCADE ); -- 创建索引,加快查询速度 CREATE INDEX idx_upload_time ON detection_records(upload_time); CREATE INDEX idx_face_count ON detection_records(face_count); CREATE INDEX idx_record_id ON face_details(record_id);为什么分两张表?因为一张图片可能检测到多个人脸,如果都塞在一行里,查询和统计都不方便。分开存储更符合数据库设计规范。
3.2 数据库连接工具类
为了方便操作数据库,我写了个简单的工具类:
import mysql.connector from mysql.connector import Error import json from datetime import datetime class DatabaseManager: def __init__(self): self.config = { 'host': 'localhost', 'database': 'face_management', 'user': 'face_user', 'password': 'your_password_here', 'charset': 'utf8mb4' } self.connection = None def connect(self): """连接数据库""" try: self.connection = mysql.connector.connect(**self.config) return True except Error as e: print(f"数据库连接失败: {e}") return False def disconnect(self): """断开数据库连接""" if self.connection and self.connection.is_connected(): self.connection.close() def create_detection_record(self, image_path): """创建检测记录""" if not self.connect(): return None try: cursor = self.connection.cursor() query = """ INSERT INTO detection_records (image_path, status) VALUES (%s, 'pending') """ cursor.execute(query, (image_path,)) self.connection.commit() record_id = cursor.lastrowid cursor.close() return record_id except Error as e: print(f"创建记录失败: {e}") return None finally: self.disconnect() def update_detection_result(self, record_id, face_count, detection_duration, face_details): """更新检测结果""" if not self.connect(): return False try: cursor = self.connection.cursor() # 更新主记录 update_query = """ UPDATE detection_records SET face_count = %s, detection_duration = %s, detection_time = NOW(), status = 'completed' WHERE id = %s """ cursor.execute(update_query, (face_count, detection_duration, record_id)) # 插入人脸详情 if face_details: detail_query = """ INSERT INTO face_details (record_id, face_index, x_min, y_min, x_max, y_max, confidence) VALUES (%s, %s, %s, %s, %s, %s, %s) """ detail_data = [] for i, face in enumerate(face_details): detail_data.append(( record_id, i, int(face['bbox'][0]), int(face['bbox'][1]), int(face['bbox'][2]), int(face['bbox'][3]), float(face['confidence']) )) cursor.executemany(detail_query, detail_data) self.connection.commit() cursor.close() return True except Error as e: print(f"更新结果失败: {e}") return False finally: self.disconnect() def get_all_records(self, page=1, page_size=20): """获取所有记录(分页)""" if not self.connect(): return [] try: cursor = self.connection.cursor(dictionary=True) offset = (page - 1) * page_size query = """ SELECT id, image_path, upload_time, detection_time, face_count, detection_duration, status FROM detection_records ORDER BY upload_time DESC LIMIT %s OFFSET %s """ cursor.execute(query, (page_size, offset)) records = cursor.fetchall() # 转换时间格式 for record in records: if record['upload_time']: record['upload_time'] = record['upload_time'].strftime('%Y-%m-%d %H:%M:%S') if record['detection_time']: record['detection_time'] = record['detection_time'].strftime('%Y-%m-%d %H:%M:%S') cursor.close() return records except Error as e: print(f"查询记录失败: {e}") return [] finally: self.disconnect() def get_statistics(self, start_date=None, end_date=None): """获取统计信息""" if not self.connect(): return {} try: cursor = self.connection.cursor(dictionary=True) stats = {} # 总记录数 cursor.execute("SELECT COUNT(*) as total FROM detection_records") stats['total_records'] = cursor.fetchone()['total'] # 总人脸数 cursor.execute("SELECT SUM(face_count) as total_faces FROM detection_records WHERE status='completed'") result = cursor.fetchone() stats['total_faces'] = result['total_faces'] or 0 # 今日记录 cursor.execute(""" SELECT COUNT(*) as today_count FROM detection_records WHERE DATE(upload_time) = CURDATE() """) stats['today_records'] = cursor.fetchone()['today_count'] # 人脸数量分布 cursor.execute(""" SELECT face_count, COUNT(*) as count FROM detection_records WHERE status='completed' GROUP BY face_count ORDER BY face_count """) stats['face_distribution'] = cursor.fetchall() # 每日统计 cursor.execute(""" SELECT DATE(upload_time) as date, COUNT(*) as record_count, SUM(face_count) as face_count FROM detection_records WHERE status='completed' GROUP BY DATE(upload_time) ORDER BY date DESC LIMIT 30 """) stats['daily_stats'] = cursor.fetchall() cursor.close() return stats except Error as e: print(f"获取统计失败: {e}") return {} finally: self.disconnect()这个工具类封装了常用的数据库操作,后面写业务逻辑的时候直接调用就行,代码会整洁很多。
4. MogFace人脸检测服务
4.1 模型加载与初始化
MogFace模型的使用不算复杂,主要是加载模型和写检测逻辑。
import torch import torchvision.transforms as transforms from PIL import Image import cv2 import numpy as np import time class MogFaceDetector: def __init__(self, model_path='models/mogface.pth'): self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"使用设备: {self.device}") # 加载模型 self.model = self.load_model(model_path) self.model.to(self.device) self.model.eval() # 设置为评估模式 # 图像预处理 self.transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def load_model(self, model_path): """加载MogFace模型""" try: # 这里需要根据MogFace的实际模型结构来定义 # 由于MogFace的具体实现可能变化,这里用伪代码表示 model = torch.load(model_path, map_location=self.device) return model except Exception as e: print(f"加载模型失败: {e}") # 如果加载失败,可以尝试其他方式 raise e def detect_faces(self, image_path, confidence_threshold=0.5): """检测图片中的人脸""" start_time = time.time() try: # 读取图片 image = cv2.imread(image_path) if image is None: raise ValueError(f"无法读取图片: {image_path}") # 转换颜色空间 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) original_height, original_width = image.shape[:2] # 预处理图片 pil_image = Image.fromarray(image_rgb) input_tensor = self.transform(pil_image).unsqueeze(0).to(self.device) # 执行检测 with torch.no_grad(): predictions = self.model(input_tensor) # 处理检测结果 faces = [] for pred in predictions[0]: # 假设predictions的结构 if len(pred) >= 5: # 至少包含bbox和confidence x_min, y_min, x_max, y_max, confidence = pred[:5] if confidence >= confidence_threshold: # 还原到原始图片尺寸 x_min = int(x_min * original_width) y_min = int(y_min * original_height) x_max = int(x_max * original_width) y_max = int(y_max * original_height) faces.append({ 'bbox': [x_min, y_min, x_max, y_max], 'confidence': float(confidence) }) # 按置信度排序 faces.sort(key=lambda x: x['confidence'], reverse=True) detection_time = time.time() - start_time return { 'success': True, 'face_count': len(faces), 'faces': faces, 'detection_time': detection_time, 'image_size': {'width': original_width, 'height': original_height} } except Exception as e: print(f"人脸检测失败: {e}") return { 'success': False, 'error': str(e), 'face_count': 0, 'faces': [], 'detection_time': 0 } def draw_faces_on_image(self, image_path, faces, output_path=None): """在图片上绘制检测到的人脸框""" image = cv2.imread(image_path) if image is None: return None for face in faces: bbox = face['bbox'] confidence = face['confidence'] # 绘制矩形框 cv2.rectangle(image, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 255, 0), 2) # 添加置信度标签 label = f"Face: {confidence:.2f}" cv2.putText(image, label, (bbox[0], bbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) if output_path: cv2.imwrite(output_path, image) return image4.2 图片处理与存储
用户上传的图片需要妥善保存,我设计了一个简单的文件管理模块:
import os import uuid from datetime import datetime from werkzeug.utils import secure_filename class ImageManager: def __init__(self, upload_folder='uploads', result_folder='results'): self.upload_folder = upload_folder self.result_folder = result_folder # 创建必要的目录 os.makedirs(upload_folder, exist_ok=True) os.makedirs(result_folder, exist_ok=True) os.makedirs(os.path.join(result_folder, 'annotated'), exist_ok=True) def save_uploaded_image(self, file): """保存上传的图片""" if not file or file.filename == '': return None # 生成唯一文件名 original_filename = secure_filename(file.filename) file_ext = os.path.splitext(original_filename)[1] unique_filename = f"{uuid.uuid4().hex}{file_ext}" # 按日期组织目录 today = datetime.now().strftime('%Y%m%d') date_folder = os.path.join(self.upload_folder, today) os.makedirs(date_folder, exist_ok=True) # 保存文件 file_path = os.path.join(date_folder, unique_filename) file.save(file_path) return { 'original_name': original_filename, 'saved_path': file_path, 'relative_path': os.path.join(today, unique_filename) } def save_annotated_image(self, image, record_id): """保存带标注的图片""" today = datetime.now().strftime('%Y%m%d') date_folder = os.path.join(self.result_folder, 'annotated', today) os.makedirs(date_folder, exist_ok=True) output_path = os.path.join(date_folder, f"{record_id}_annotated.jpg") cv2.imwrite(output_path, image) return output_path def get_image_url(self, file_path): """获取图片的访问URL""" if not file_path or not os.path.exists(file_path): return None # 这里返回相对路径,前端通过Flask的静态文件路由访问 return f"/static/{file_path}"5. Web应用开发
5.1 Flask后端API
现在把各个模块组合起来,写Flask后端:
from flask import Flask, request, jsonify, render_template, send_from_directory from flask_cors import CORS import os import json app = Flask(__name__) CORS(app) # 允许跨域请求 # 初始化各个模块 image_manager = ImageManager() detector = MogFaceDetector() db_manager = DatabaseManager() # 配置 app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB限制 app.config['UPLOAD_FOLDER'] = 'uploads' @app.route('/') def index(): """首页""" return render_template('index.html') @app.route('/api/upload', methods=['POST']) def upload_image(): """上传图片API""" try: if 'image' not in request.files: return jsonify({'error': '没有选择图片'}), 400 file = request.files['image'] # 保存图片 save_result = image_manager.save_uploaded_image(file) if not save_result: return jsonify({'error': '保存图片失败'}), 500 # 创建数据库记录 record_id = db_manager.create_detection_record(save_result['saved_path']) if not record_id: return jsonify({'error': '创建记录失败'}), 500 # 异步处理人脸检测(实际项目中可以用Celery等任务队列) # 这里为了简单,直接同步处理 detection_result = detector.detect_faces(save_result['saved_path']) if detection_result['success']: # 绘制标注图片 annotated_image = detector.draw_faces_on_image( save_result['saved_path'], detection_result['faces'] ) # 保存标注图片 if annotated_image is not None: annotated_path = image_manager.save_annotated_image( annotated_image, record_id ) # 更新数据库 db_manager.update_detection_result( record_id, detection_result['face_count'], detection_result['detection_time'], detection_result['faces'] ) return jsonify({ 'success': True, 'record_id': record_id, 'face_count': detection_result['face_count'], 'detection_time': round(detection_result['detection_time'], 3), 'image_url': image_manager.get_image_url(save_result['saved_path']), 'annotated_url': image_manager.get_image_url(annotated_path) if annotated_path else None }) else: # 检测失败,更新状态 db_manager.update_detection_result( record_id, 0, 0, [] ) return jsonify({ 'success': False, 'error': detection_result['error'] }), 500 except Exception as e: print(f"上传处理失败: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/records', methods=['GET']) def get_records(): """获取检测记录""" try: page = request.args.get('page', 1, type=int) page_size = request.args.get('page_size', 20, type=int) records = db_manager.get_all_records(page, page_size) # 为每条记录添加图片URL for record in records: if record['image_path']: record['image_url'] = image_manager.get_image_url(record['image_path']) return jsonify({ 'success': True, 'records': records, 'page': page, 'page_size': page_size }) except Exception as e: print(f"获取记录失败: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/statistics', methods=['GET']) def get_statistics(): """获取统计信息""" try: stats = db_manager.get_statistics() return jsonify({ 'success': True, 'statistics': stats }) except Exception as e: print(f"获取统计失败: {e}") return jsonify({'error': str(e)}), 500 @app.route('/static/<path:filename>') def serve_static(filename): """提供静态文件访问""" # 根据文件路径判断在哪个目录 if filename.startswith('uploads/'): directory = '.' elif filename.startswith('results/'): directory = '.' else: directory = 'static' return send_from_directory(directory, filename) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)5.2 简单的前端界面
前端不需要太复杂,能上传图片、展示结果就行:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>人脸信息管理系统</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 10px; margin-bottom: 2rem; text-align: center; } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } .subtitle { font-size: 1.1rem; opacity: 0.9; } .main-content { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem; } @media (max-width: 768px) { .main-content { grid-template-columns: 1fr; } } .upload-section, .results-section { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .upload-area { border: 2px dashed #ddd; border-radius: 8px; padding: 2rem; text-align: center; margin: 1rem 0; cursor: pointer; transition: all 0.3s; } .upload-area:hover { border-color: #667eea; background: #f8f9ff; } .upload-area.dragover { border-color: #667eea; background: #f0f2ff; } .upload-icon { font-size: 3rem; color: #667eea; margin-bottom: 1rem; } .file-input { display: none; } .preview-container { margin-top: 1rem; text-align: center; } .image-preview { max-width: 100%; max-height: 300px; border-radius: 4px; margin-bottom: 1rem; } .detect-btn { background: #667eea; color: white; border: none; padding: 0.8rem 2rem; border-radius: 4px; font-size: 1rem; cursor: pointer; width: 100%; margin-top: 1rem; transition: background 0.3s; } .detect-btn:hover { background: #5a67d8; } .detect-btn:disabled { background: #ccc; cursor: not-allowed; } .result-item { background: #f8f9fa; padding: 1rem; border-radius: 6px; margin-bottom: 1rem; border-left: 4px solid #667eea; } .result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .face-count { background: #667eea; color: white; padding: 0.2rem 0.8rem; border-radius: 12px; font-size: 0.9rem; } .stats-section { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 2rem; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; } .stat-card { background: #f8f9fa; padding: 1rem; border-radius: 6px; text-align: center; } .stat-value { font-size: 2rem; font-weight: bold; color: #667eea; margin: 0.5rem 0; } .stat-label { color: #666; font-size: 0.9rem; } .loading { display: none; text-align: center; padding: 1rem; } .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #667eea; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error { color: #e53e3e; background: #fed7d7; padding: 0.8rem; border-radius: 4px; margin: 1rem 0; } .success { color: #38a169; background: #c6f6d5; padding: 0.8rem; border-radius: 4px; margin: 1rem 0; } </style> </head> <body> <div class="container"> <header> <h1>人脸信息管理系统</h1> <p class="subtitle">上传图片,自动检测人脸并存储到数据库</p> </header> <div class="stats-section"> <h2>系统统计</h2> <div class="stats-grid" id="statsGrid"> <!-- 统计信息将通过JavaScript动态加载 --> </div> </div> <div class="main-content"> <div class="upload-section"> <h2>上传图片</h2> <p>支持JPG、PNG格式,最大16MB</p> <div class="upload-area" id="uploadArea"> <div class="upload-icon">📁</div> <p>点击选择或拖拽图片到此处</p> <p style="color: #666; font-size: 0.9rem; margin-top: 0.5rem;"> 支持.jpg, .jpeg, .png格式 </p> </div> <input type="file" id="fileInput" class="file-input" accept="image/*"> <div class="preview-container" id="previewContainer" style="display: none;"> <img id="imagePreview" class="image-preview" src="" alt="预览"> <p id="fileName"></p> </div> <button id="detectBtn" class="detect-btn" disabled>开始检测人脸</button> <div class="loading" id="loading"> <div class="spinner"></div> <p>正在检测人脸,请稍候...</p> </div> <div id="resultMessage"></div> </div> <div class="results-section"> <h2>最近检测记录</h2> <div id="recentResults"> <!-- 最近记录将通过JavaScript动态加载 --> </div> </div> </div> </div> <script> // DOM元素 const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); const previewContainer = document.getElementById('previewContainer'); const imagePreview = document.getElementById('imagePreview'); const fileName = document.getElementById('fileName'); const detectBtn = document.getElementById('detectBtn'); const loading = document.getElementById('loading'); const resultMessage = document.getElementById('resultMessage'); const recentResults = document.getElementById('recentResults'); const statsGrid = document.getElementById('statsGrid'); let selectedFile = null; // 初始化 document.addEventListener('DOMContentLoaded', function() { loadStatistics(); loadRecentRecords(); // 设置上传区域事件 uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); if (e.dataTransfer.files.length) { handleFileSelect(e.dataTransfer.files[0]); } }); // 文件选择事件 fileInput.addEventListener('change', (e) => { if (e.target.files.length) { handleFileSelect(e.target.files[0]); } }); // 检测按钮事件 detectBtn.addEventListener('click', uploadAndDetect); }); // 处理文件选择 function handleFileSelect(file) { if (!file.type.match('image.*')) { showError('请选择图片文件(JPG、PNG格式)'); return; } if (file.size > 16 * 1024 * 1024) { showError('图片大小不能超过16MB'); return; } selectedFile = file; // 显示预览 const reader = new FileReader(); reader.onload = function(e) { imagePreview.src = e.target.result; previewContainer.style.display = 'block'; fileName.textContent = file.name; }; reader.readAsDataURL(file); // 启用检测按钮 detectBtn.disabled = false; resultMessage.innerHTML = ''; } // 上传并检测 async function uploadAndDetect() { if (!selectedFile) return; const formData = new FormData(); formData.append('image', selectedFile); // 显示加载中 loading.style.display = 'block'; detectBtn.disabled = true; resultMessage.innerHTML = ''; try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { showSuccess(`检测完成!发现 ${result.face_count} 张人脸,耗时 ${result.detection_time} 秒`); // 显示标注图片 if (result.annotated_url) { imagePreview.src = result.annotated_url; } // 刷新统计和记录 loadStatistics(); loadRecentRecords(); // 重置表单 setTimeout(() => { selectedFile = null; previewContainer.style.display = 'none'; detectBtn.disabled = true; fileInput.value = ''; }, 3000); } else { showError(`检测失败:${result.error}`); } } catch (error) { showError(`上传失败:${error.message}`); } finally { loading.style.display = 'none'; } } // 加载统计信息 async function loadStatistics() { try { const response = await fetch('/api/statistics'); const result = await response.json(); if (result.success) { const stats = result.statistics; statsGrid.innerHTML = ` <div class="stat-card"> <div class="stat-label">总检测记录</div> <div class="stat-value">${stats.total_records}</div> </div> <div class="stat-card"> <div class="stat-label">总人脸数量</div> <div class="stat-value">${stats.total_faces}</div> </div> <div class="stat-card"> <div class="stat-label">今日检测</div> <div class="stat-value">${stats.today_records}</div> </div> `; } } catch (error) { console.error('加载统计失败:', error); } } // 加载最近记录 async function loadRecentRecords() { try { const response = await fetch('/api/records?page_size=5'); const result = await response.json(); if (result.success && result.records.length > 0) { let html = ''; result.records.forEach(record => { const time = record.upload_time || '未知时间'; const faceCount = record.face_count || 0; html += ` <div class="result-item"> <div class="result-header"> <span>${time}</span> <span class="face-count">${faceCount} 张人脸</span> </div> ${record.image_url ? ` <img src="${record.image_url}" style="max-width: 100%; border-radius: 4px; margin-top: 0.5rem;" alt="检测图片"> ` : ''} </div> `; }); recentResults.innerHTML = html; } else { recentResults.innerHTML = '<p style="text-align: center; color: #666;">暂无检测记录</p>'; } } catch (error) { console.error('加载记录失败:', error); recentResults.innerHTML = '<p style="text-align: center; color: #e53e3e;">加载失败</p>'; } } // 显示错误消息 function showError(message) { resultMessage.innerHTML = `<div class="error">${message}</div>`; } // 显示成功消息 function showSuccess(message) { resultMessage.innerHTML = `<div class="success">${message}</div>`; } </script> </body> </html>6. 实际应用与扩展建议
这个系统虽然简单,但已经具备了基本的人脸信息管理功能。在实际使用中,我发现有几个地方可以进一步优化:
性能优化方面,如果图片数量很多,同步处理可能会让用户等太久。可以考虑用消息队列(比如RabbitMQ或Redis)来做异步处理,用户上传图片后立即返回,后台慢慢处理,处理完了再通知用户。
功能扩展方面,可以加入人脸识别功能,不只是检测有没有人脸,还能识别是谁。这样就能做考勤系统了,员工刷脸打卡,系统自动记录。还可以加个报警功能,检测到陌生人脸就发通知。
数据安全方面,现在图片是直接存文件系统的,可以考虑加密存储,或者存到对象存储服务里。数据库连接信息也应该放在环境变量里,别硬编码在代码里。
用户体验方面,可以加个批量上传功能,一次传多张图片。再加个导出功能,能把检测记录导出成Excel,方便做报表。
部署的时候,可以用Docker把整个应用打包,包括MySQL数据库、Flask应用都放容器里,这样部署起来方便,迁移也容易。
7. 总结
整个项目做下来,感觉把AI模型和传统数据库结合起来,确实能解决不少实际问题。MogFace的检测效果不错,速度也快,配合MySQL存储数据,查询统计都很方便。
实际用的时候,部署确实简单,基本上跟着步骤走就行。效果对大多数场景来说够用了,检测准确率挺高的。如果你刚接触这种AI+数据库的项目,建议先从简单的功能开始,跑通了再慢慢加新功能。
数据库设计这块,开始可能觉得有点复杂,但分清楚哪些数据该放哪张表后面就顺了。Web界面虽然简单,但该有的功能都有,上传、检测、查看记录都行。
这种项目最实用的地方就是能自动化处理重复工作。原来要人工一张张看图片、数人脸、记到Excel里,现在传上去自动就搞定了,还能随时查历史记录、做统计,省时省力。
如果你想自己试试,可以从最基础的版本开始,先实现上传和检测,存到数据库里。然后再慢慢加其他功能,比如批量处理、导出报表这些。遇到问题多查查文档,现在开源社区资源很丰富,大部分问题都能找到解决方案。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
