feat(photo): 优化照片分类和查询功能
- 重构了照片分类逻辑,提高了分类效率和准确性 - 新增照片查询功能,支持按姓名和日期筛选 - 优化了用户管理界面,增加了用户头像显示 -调整了照片上传和分类的前端展示效果
This commit is contained in:
parent
ff7b934797
commit
7507d4c14a
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="migrated" value="true" />
|
||||||
|
<option name="pristineConfig" value="false" />
|
||||||
|
<option name="userId" value="21c1c7ee:193388d497c:-7ff9" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -4,4 +4,7 @@
|
||||||
<option name="sdkName" value="Python 3.10 (photo)" />
|
<option name="sdkName" value="Python 3.10 (photo)" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (photo)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (photo)" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PyCharmProfessionalAdvertiser">
|
||||||
|
<option name="shown" value="true" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -4,7 +4,7 @@
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="jdk" jdkName="Python 3.10 (photo)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
221
README.md
221
README.md
|
@ -1,125 +1,140 @@
|
||||||
下面是一个示例 README 文档,你可以将其保存为 `README.md` 文件,并根据实际需求进行调整:
|
# 照片管理系统项目文档
|
||||||
|
|
||||||
---
|
## 项目概述
|
||||||
|
本项目是一个基于 Flask 框架的照片管理系统,主要功能包括:
|
||||||
|
1. **人脸管理**:增删改查用户的人脸数据。
|
||||||
|
2. **照片上传与分类**:用户可以上传照片,系统会根据 EXIF 时间信息和数据库中的人脸数据进行分类存储。
|
||||||
|
3. **照片查询**:用户可以根据姓名和日期查找相应的照片。
|
||||||
|
|
||||||
# Photo Project
|
## 技术栈
|
||||||
|
- **后端**:Flask(Python)
|
||||||
|
- **数据库**:SQLAlchemy(SQLite / MySQL)
|
||||||
|
- **前端**:HTML + CSS + Jinja2
|
||||||
|
- **存储**:文件系统存储上传的照片
|
||||||
|
|
||||||
本项目是一个基于 Flask 的人脸识别应用,用于处理用户头像上传和照片匹配。系统通过提取上传图片中最大的人脸区域生成面部特征码,并将用户信息和照片信息存储到 SQLite 数据库中,支持查询用户照片。
|
## 数据库设计
|
||||||
|
### 1. `users` 表(用户信息表)
|
||||||
|
| 字段名 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| id | 主键 | 自动生成 |
|
||||||
|
| username | 字符串 | 用户名,唯一且非空 |
|
||||||
|
| encoding | 二进制数据 | 人脸识别编码,用于匹配 |
|
||||||
|
| photo | 字符串 | 用户头像照片路径 |
|
||||||
|
|
||||||
## 项目功能
|
### 2. `photos` 表(照片信息表)
|
||||||
|
| 字段名 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| id | 主键 | 自动生成 |
|
||||||
|
| user_id | 外键 | 关联到 `users` 表的 `id` |
|
||||||
|
| created_at | 时间戳 | 照片拍摄时间(来自 EXIF 信息) |
|
||||||
|
| classification | 布尔值 | 照片是否分类(默认 `False`,分类成功后 `True`) |
|
||||||
|
| classification_confidence | 浮点数 | 照片分类置信度(默认 `0`) |
|
||||||
|
| classification_image_path | 字符串 | 分类后的照片存储路径 |
|
||||||
|
|
||||||
- **用户管理**
|
## 主要功能实现
|
||||||
- 上传用户头像,自动提取最大人脸并生成面部特征码。
|
### 1. 用户管理
|
||||||
- 编辑和删除用户信息。
|
- **添加用户**:用户可上传照片,系统提取人脸编码并存入数据库。
|
||||||
|
- **查看用户列表**:显示所有用户及其头像。
|
||||||
|
- **删除用户**:从数据库中删除用户信息及相关照片。
|
||||||
|
|
||||||
- **照片上传**
|
### 2. 照片上传与分类
|
||||||
- 支持批量照片上传。
|
- **上传照片**:用户上传照片,系统解析 EXIF 数据确定拍摄时间。
|
||||||
- 自动检测每张照片中最大的那张人脸,提取面部特征码,与已注册用户进行比对。
|
- **人脸匹配分类**:系统根据数据库中的人脸编码进行匹配,分类并存储到相应目录。
|
||||||
- 匹配成功后将照片移动到用户目录,并记录拍摄时间。
|
- **存储路径规则**:
|
||||||
|
```
|
||||||
|
static/uploads/users/{用户名}/{日期(年-月-日)}/照片文件名
|
||||||
|
```
|
||||||
|
|
||||||
- **查询功能**
|
### 3. 照片查询
|
||||||
- 根据用户名和拍摄日期查询用户照片。
|
- **基于姓名和日期查询**:用户输入姓名和日期,系统返回符合条件的照片。
|
||||||
|
- **展示分类结果**:前端页面显示照片缩略图,并提供原始照片下载链接。
|
||||||
|
|
||||||
- **其他功能**
|
## 关键代码
|
||||||
- 根据图片 EXIF 信息获取拍摄时间。
|
### 1. 照片上传(`upload` 端点)
|
||||||
- 自动校正图片方向。
|
```python
|
||||||
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
|
def upload():
|
||||||
|
if request.method == 'POST':
|
||||||
|
photos = request.files.getlist('photos')
|
||||||
|
classify_photos(photos) # 进行分类处理
|
||||||
|
|
||||||
## 项目结构
|
# 获取最新照片并关联用户信息
|
||||||
|
recent_photos = Photo.query.order_by(Photo.created_at.desc()).limit(len(photos)).all()
|
||||||
|
photos_with_users = []
|
||||||
|
for photo in recent_photos:
|
||||||
|
user = User.query.get(photo.user_id)
|
||||||
|
photos_with_users.append({
|
||||||
|
"filename": photo.classification_image_path,
|
||||||
|
"username": user.username if user else "未知",
|
||||||
|
"created_at": photo.created_at
|
||||||
|
})
|
||||||
|
|
||||||
```
|
return render_template('upload.html', photos=photos_with_users)
|
||||||
photo_project/ # 项目根目录
|
|
||||||
├── .venv/ # 虚拟环境目录(可选)
|
return render_template('upload.html', photos=[])
|
||||||
├── app.py # Flask 主应用程序,包含所有业务逻辑和路由
|
|
||||||
├── fuctions.py # 功能函数
|
|
||||||
├── moodle.py # 数据库模型定义(User、Photo)
|
|
||||||
├── requirements.txt # 项目依赖包列表
|
|
||||||
├── README.md # 项目说明文档
|
|
||||||
├── templates/ # HTML 模板文件
|
|
||||||
│ ├── index.html # 首页模板
|
|
||||||
│ ├── user_management.html # 用户管理页面模板
|
|
||||||
│ ├── upload.html # 照片上传及结果展示模板
|
|
||||||
│ └── search.html # 查询页面模板
|
|
||||||
└── static/ # 静态文件目录
|
|
||||||
└── uploads/ # 图片上传目录
|
|
||||||
├── users/ # 用户头像和照片存储目录(按用户/日期组织)
|
|
||||||
└── temp/ # 临时目录,用于存储上传处理中的图片,分类完成的照片会删除
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装与配置
|
### 2. 照片查询(`search` 端点)
|
||||||
|
```python
|
||||||
|
@app.route('/search', methods=['POST'])
|
||||||
|
def search():
|
||||||
|
name = request.form['name']
|
||||||
|
date = request.form['date']
|
||||||
|
|
||||||
1. **克隆项目**
|
user = User.query.filter_by(username=name).first()
|
||||||
|
if not user:
|
||||||
|
return render_template('search.html', photos=[], message="用户不存在")
|
||||||
|
|
||||||
```bash
|
photos = Photo.query.filter_by(user_id=user.id).filter(Photo.created_at.like(f"{date}%")).all()
|
||||||
git clone <repository_url>
|
return render_template('search.html', photos=photos)
|
||||||
cd photo_project
|
```
|
||||||
```
|
|
||||||
|
|
||||||
2. **创建虚拟环境并安装依赖**
|
## 前端页面示例
|
||||||
|
### 1. 照片查询页面(`search.html`)
|
||||||
|
```html
|
||||||
|
<form action="{{ url_for('search') }}" method="POST">
|
||||||
|
<label for="name">姓名:</label>
|
||||||
|
<input type="text" id="name" name="name" placeholder="请输入用户名" required>
|
||||||
|
<label for="date">日期:</label>
|
||||||
|
<input type="date" id="date" name="date" required>
|
||||||
|
<button type="submit">查询照片</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
## 目录结构
|
||||||
python -m venv .venv
|
```
|
||||||
source .venv/bin/activate # Linux/macOS
|
project_root/
|
||||||
.venv\Scripts\activate # Windows
|
│── app.py # Flask 入口文件
|
||||||
|
│── functions.py # 处理照片分类等功能
|
||||||
|
│── models.py # 数据库模型
|
||||||
|
│── templates/
|
||||||
|
│ ├── base.html # 公共模板
|
||||||
|
│ ├── upload.html # 上传页面
|
||||||
|
│ ├── search.html # 查询页面
|
||||||
|
│── static/
|
||||||
|
│ ├── uploads/ # 存储照片
|
||||||
|
│ ├── styles.css # 样式表
|
||||||
|
│── .venv/ # 虚拟环境
|
||||||
|
│── requirements.txt # 依赖文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署方式
|
||||||
|
1. **安装依赖**
|
||||||
|
```sh
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
2. **运行 Flask 服务器**
|
||||||
3. **配置环境变量**
|
```sh
|
||||||
|
flask run
|
||||||
如果需要自定义密钥,可以设置环境变量 `SECRET_KEY`,例如:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export SECRET_KEY='your_secret_key'
|
|
||||||
```
|
```
|
||||||
|
3. **访问应用**
|
||||||
|
在浏览器中打开 `http://127.0.0.1:5000`
|
||||||
|
|
||||||
4. **初始化数据库**
|
## 未来优化方向
|
||||||
|
- **添加身份验证**:使用 Flask-Login 实现用户登录功能。
|
||||||
|
- **优化分类算法**:提高人脸识别和分类的准确率。
|
||||||
|
- **支持云存储**:将照片存储到云端,如 AWS S3 或阿里云 OSS。
|
||||||
|
|
||||||
运行项目时,系统会自动在应用上下文中创建数据库表。
|
---
|
||||||
|
本项目旨在提供一个高效、易用的照片管理系统,欢迎优化和扩展!
|
||||||
## 运行项目
|
|
||||||
|
|
||||||
在项目根目录下执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
项目将启动在 `http://127.0.0.1:5000/`,可在浏览器中访问。
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
- **首页**
|
|
||||||
访问根网页 `/`,进入首页,可选择进入用户管理、照片上传和查询页面。
|
|
||||||
|
|
||||||
- **用户管理**
|
|
||||||
访问 `/user_management` 页面,上传用户头像后,系统自动提取最大人脸并生成面部特征码保存到数据库。
|
|
||||||
|
|
||||||
- **照片上传**
|
|
||||||
访问 `/photo_upload` 页面,上传一张或多张照片,系统将对每张照片提取最大人脸,与数据库中用户进行比对,匹配成功后将照片移动至用户目录,并记录拍摄时间。
|
|
||||||
|
|
||||||
- **查询照片**
|
|
||||||
访问 `/search` 页面,根据输入的用户名和拍摄日期(格式:YYYY-MM-DD)查询用户当天的照片。
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 请确保 `templates` 文件夹中包含必要的 HTML 模板(如 `index.html`、`user_management.html`、`upload.html` 和 `search.html`)。
|
|
||||||
- 照片上传目录默认位于 `static/uploads` 下,建议定期检查和清理临时文件夹 `static/uploads/temp`。
|
|
||||||
- 本项目使用 SQLite 数据库,适合中小型应用,若用于生产环境,请考虑替换为更适合的数据库。
|
|
||||||
|
|
||||||
## 依赖
|
|
||||||
|
|
||||||
主要依赖包包括:
|
|
||||||
|
|
||||||
- Flask
|
|
||||||
- flask_session
|
|
||||||
- SQLAlchemy
|
|
||||||
- face_recognition
|
|
||||||
- OpenCV (opencv-python)
|
|
||||||
- Pillow
|
|
||||||
- numpy
|
|
||||||
|
|
||||||
详细依赖请参见 `requirements.txt`。
|
|
||||||
|
|
||||||
## 版权与许可证
|
|
||||||
|
|
||||||
本项目仅供学习和测试使用,具体版权和许可证信息请根据实际情况添加。
|
|
||||||
|
|
||||||
|
|
40
app.py
40
app.py
|
@ -1,30 +1,29 @@
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||||
from functions import add_user, delete_user, edit_user, get_user, classify_photos, search_photos
|
from functions import *
|
||||||
from models import db, User, Photo
|
from models import db, User, Photo
|
||||||
import os
|
import os
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
# 初始化Flask应用
|
# 初始化Flask应用
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = 'your_secret_key'
|
app.secret_key = 'your_secret_key'
|
||||||
|
|
||||||
# 设置数据库配置
|
# 设置数据库
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' # 修改为你的数据库URI
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' # 修改为你的数据库URI
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁用修改追踪
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
# 初始化SQLAlchemy
|
# 初始化数据库
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
# 首页路由
|
# 首页
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
# 用户管理页面
|
# 用户管理
|
||||||
@app.route('/user_management')
|
@app.route('/user_management')
|
||||||
def user_management():
|
def user_management():
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
|
@ -35,7 +34,6 @@ def get_encoding(user_id):
|
||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
return jsonify({"encoding": user.encoding if user and user.encoding else "无数据"})
|
return jsonify({"encoding": user.encoding if user and user.encoding else "无数据"})
|
||||||
|
|
||||||
|
|
||||||
# 添加用户
|
# 添加用户
|
||||||
@app.route('/add_user', methods=['POST'])
|
@app.route('/add_user', methods=['POST'])
|
||||||
def add_new_user():
|
def add_new_user():
|
||||||
|
@ -70,14 +68,30 @@ def edit_existing_user(user_id):
|
||||||
return redirect(url_for('user_management'))
|
return redirect(url_for('user_management'))
|
||||||
return render_template('edit_user.html', user=user)
|
return render_template('edit_user.html', user=user)
|
||||||
|
|
||||||
# 照片上传页面
|
# 照片上传
|
||||||
@app.route('/upload', methods=['GET', 'POST'])
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
def upload():
|
def upload():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
photos = request.files.getlist('photos')
|
photos = request.files.getlist('photos') # 获取上传的照片
|
||||||
classify_photos(photos) # 调用分类处理函数
|
classify_photos(photos) # 进行照片分类处理
|
||||||
return render_template('upload.html', photos=photos)
|
|
||||||
return render_template('upload.html')
|
# 获取数据库中最新的照片记录,按照创建时间排序
|
||||||
|
recent_photos = Photo.query.order_by(Photo.created_at.desc()).limit(len(photos)).all()
|
||||||
|
|
||||||
|
# 组装数据:获取照片对应的用户信息
|
||||||
|
photos_with_users = []
|
||||||
|
for photo in recent_photos:
|
||||||
|
user = User.query.get(photo.user_id) # 获取用户信息
|
||||||
|
photos_with_users.append({
|
||||||
|
"filename": photo.classification_image_path, # 分类后的照片路径
|
||||||
|
"username": user.username if user else "未知", # 用户名
|
||||||
|
"created_at": photo.created_at # 照片创建时间
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('upload.html', photos=photos_with_users)
|
||||||
|
|
||||||
|
return render_template('upload.html', photos=[])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 照片搜索
|
# 照片搜索
|
||||||
|
|
363
functions.py
363
functions.py
|
@ -1,243 +1,188 @@
|
||||||
import piexif
|
|
||||||
from PIL import Image
|
|
||||||
import face_recognition
|
|
||||||
from datetime import datetime
|
|
||||||
from models import db, User, Photo
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import face_recognition
|
||||||
|
import piexif
|
||||||
|
from datetime import datetime
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from PIL import Image
|
||||||
|
from models import db, User, Photo
|
||||||
|
|
||||||
def process_face_encoding(photo_path):
|
# 目录路径
|
||||||
"""提取人脸特征编码并裁剪出最大的面部区域"""
|
UPLOAD_FOLDER = 'static/uploads'
|
||||||
# 加载图片
|
USER_FOLDER = os.path.join(UPLOAD_FOLDER, 'users')
|
||||||
|
PHOTO_FOLDER = os.path.join(UPLOAD_FOLDER, 'photos')
|
||||||
|
TEMP_FOLDER = os.path.join(UPLOAD_FOLDER, 'temp')
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
for folder in [USER_FOLDER, PHOTO_FOLDER, TEMP_FOLDER]:
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_face_encoding(photo_path):
|
||||||
|
"""提取人脸编码"""
|
||||||
image = face_recognition.load_image_file(photo_path)
|
image = face_recognition.load_image_file(photo_path)
|
||||||
|
encodings = face_recognition.face_encodings(image)
|
||||||
|
return encodings[0] if encodings else None
|
||||||
|
|
||||||
# 获取所有人脸的位置
|
|
||||||
face_locations = face_recognition.face_locations(image)
|
|
||||||
|
|
||||||
if not face_locations:
|
def save_with_exif(image_path, save_path):
|
||||||
return None # 没有检测到人脸
|
"""保存图片并保留 EXIF 信息"""
|
||||||
|
image = Image.open(image_path)
|
||||||
|
exif_data = image.getexif()
|
||||||
|
if exif_data:
|
||||||
|
exif_bytes = piexif.dump(dict(exif_data))
|
||||||
|
image.save(save_path, exif=exif_bytes)
|
||||||
|
else:
|
||||||
|
image.save(save_path)
|
||||||
|
return exif_data
|
||||||
|
|
||||||
# 找到最大的人脸
|
|
||||||
largest_face = max(face_locations, key=lambda x: (x[2] - x[0]) * (x[3] - x[1])) # 按照面积选取最大的人脸
|
|
||||||
|
|
||||||
# 获取该人脸的特征编码
|
def format_exif_date(exif_data):
|
||||||
face_encodings = face_recognition.face_encodings(image, [largest_face])
|
"""格式化 EXIF 时间为 YYYY-MM-DD"""
|
||||||
|
try:
|
||||||
if face_encodings:
|
return datetime.strptime(exif_data[306], "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d")
|
||||||
# 裁剪出最大的人脸部分
|
except (KeyError, ValueError, TypeError):
|
||||||
top, right, bottom, left = largest_face
|
return None # 处理无效时间
|
||||||
image_pil = Image.open(photo_path)
|
|
||||||
cropped_face = image_pil.crop((left, top, right, bottom))
|
|
||||||
|
|
||||||
# 保存裁剪后的图像(可选)
|
|
||||||
cropped_face_path = photo_path.replace(".jpg", "_cropped.jpg")
|
|
||||||
cropped_face.save(cropped_face_path)
|
|
||||||
|
|
||||||
return face_encodings[0], cropped_face_path # 返回面部编码和裁剪后的人脸图片路径
|
|
||||||
|
|
||||||
return None # 如果没有提取到人脸特征编码
|
|
||||||
|
|
||||||
|
|
||||||
def add_user(username, userphoto):
|
def add_user(username, userphoto):
|
||||||
"""添加用户并存储人脸特征"""
|
"""添加用户并提取人脸编码"""
|
||||||
if userphoto:
|
if not username or not userphoto:
|
||||||
filename = secure_filename(userphoto.filename)
|
|
||||||
user_dir = os.path.join("static/uploads/users",username)
|
|
||||||
user_dir =user_dir.replace("\\","/")
|
|
||||||
os.makedirs(user_dir, exist_ok=True)
|
|
||||||
photo_path = os.path.join(user_dir,"photo.jpg")
|
|
||||||
photo_path = photo_path.replace("\\","/")
|
|
||||||
|
|
||||||
|
|
||||||
# 保存头像
|
|
||||||
userphoto.save(photo_path)
|
|
||||||
|
|
||||||
# 处理人脸特征
|
|
||||||
face_encoding = process_face_encoding(photo_path)
|
|
||||||
if face_encoding is None:
|
|
||||||
print("未检测到人脸,无法添加用户")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 存储到数据库
|
|
||||||
new_user = User(username=username, encoding=face_encoding, photo=photo_path)
|
|
||||||
db.session.add(new_user)
|
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 删除用户
|
|
||||||
def delete_user(user_id):
|
|
||||||
try:
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
if user:
|
|
||||||
db.session.delete(user)
|
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 编辑用户
|
user_dir = os.path.join(USER_FOLDER, username)
|
||||||
def edit_user(user_id, username, userphoto):
|
|
||||||
try:
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
if user:
|
|
||||||
user.username = username
|
|
||||||
if userphoto:
|
|
||||||
photo_path = os.path.join('static', 'uploads', 'users', str(user.id))
|
|
||||||
os.makedirs(photo_path, exist_ok=True)
|
|
||||||
photo_file = os.path.join(photo_path, userphoto.filename)
|
|
||||||
userphoto.save(photo_file)
|
|
||||||
# 更新照片路径
|
|
||||||
photo = Photo.query.filter_by(user_id=user.id).first()
|
|
||||||
photo.photo_path = photo_file
|
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 获取用户
|
|
||||||
def get_user(user_id):
|
|
||||||
return User.query.get(user_id)
|
|
||||||
|
|
||||||
def save_temp_photo(photo):
|
|
||||||
"""保存临时照片"""
|
|
||||||
temp_dir = 'static/uploads/temp'
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
photo_path = os.path.join(temp_dir, photo.filename).replace("\\", "/")
|
|
||||||
photo.save(photo_path)
|
|
||||||
return photo_path
|
|
||||||
|
|
||||||
|
|
||||||
def get_largest_face(image_path):
|
|
||||||
"""检测并裁剪最大的人脸"""
|
|
||||||
image = face_recognition.load_image_file(image_path)
|
|
||||||
face_locations = face_recognition.face_locations(image)
|
|
||||||
|
|
||||||
if not face_locations:
|
|
||||||
return None # 没有检测到人脸
|
|
||||||
|
|
||||||
# 找到最大的人脸
|
|
||||||
largest_face = max(face_locations, key=lambda loc: (loc[2] - loc[0]) * (loc[3] - loc[1]))
|
|
||||||
top, right, bottom, left = largest_face
|
|
||||||
|
|
||||||
# 裁剪最大的人脸
|
|
||||||
with Image.open(image_path) as img:
|
|
||||||
face_img = img.crop((left, top, right, bottom))
|
|
||||||
face_crop_path = image_path.replace(".jpg", "_face.jpg") # 生成裁剪后的人脸图片路径
|
|
||||||
face_img.save(face_crop_path)
|
|
||||||
|
|
||||||
return face_crop_path
|
|
||||||
|
|
||||||
def match_face(face_path):
|
|
||||||
"""匹配人脸并返回匹配的用户"""
|
|
||||||
face_image = face_recognition.load_image_file(face_path)
|
|
||||||
face_encodings = face_recognition.face_encodings(face_image)
|
|
||||||
|
|
||||||
if not face_encodings:
|
|
||||||
return None # 未检测到人脸
|
|
||||||
|
|
||||||
face_encoding = face_encodings[0] # 取第一张脸
|
|
||||||
users = User.query.all()
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
if user.encoding:
|
|
||||||
user_encoding = np.array(user.encoding) # 从数据库中获取存储的编码
|
|
||||||
match = face_recognition.compare_faces([user_encoding], face_encoding, tolerance=0.35)
|
|
||||||
if match[0]:
|
|
||||||
return user # 匹配成功
|
|
||||||
return None # 未找到匹配
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_exif_time(photo_path):
|
|
||||||
"""提取 EXIF 时间"""
|
|
||||||
try:
|
|
||||||
with Image.open(photo_path) as img:
|
|
||||||
exif_data = img._getexif()
|
|
||||||
if exif_data:
|
|
||||||
timestamp = exif_data.get(36867, None) # 36867 为 EXIF 拍摄时间
|
|
||||||
return timestamp if timestamp else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"无法提取 EXIF 时间: {e}")
|
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
def save_classified_photo(photo, user, timestamp):
|
|
||||||
"""保存分类后的照片"""
|
|
||||||
user_dir = os.path.join('static/uploads/users', str(user.id), timestamp)
|
|
||||||
os.makedirs(user_dir, exist_ok=True)
|
os.makedirs(user_dir, exist_ok=True)
|
||||||
|
|
||||||
new_photo_path = os.path.join(user_dir, photo.filename)
|
photo_path = os.path.join(user_dir, "photo.jpg")
|
||||||
|
userphoto.save(photo_path)
|
||||||
|
|
||||||
try:
|
encoding = extract_face_encoding(photo_path)
|
||||||
with Image.open(photo) as img:
|
if encoding is None:
|
||||||
img.save(new_photo_path, quality=95)
|
return False
|
||||||
except Exception as e:
|
|
||||||
print(f"保存图片时出错: {e}")
|
|
||||||
|
|
||||||
# 保存到数据库
|
new_user = User(username=username, encoding=json.dumps(encoding.tolist()), photo=photo_path)
|
||||||
photo_record = Photo(user_id=user.id, created_at=timestamp, classification=True,
|
db.session.add(new_user)
|
||||||
classification_image_path=new_photo_path)
|
|
||||||
db.session.add(photo_record)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
print(f"照片 {photo.filename} 已保存至 {new_photo_path}")
|
|
||||||
|
def edit_user(user_id, new_name, new_photo):
|
||||||
|
"""编辑用户信息"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user.username = new_name
|
||||||
|
|
||||||
|
if new_photo:
|
||||||
|
user_dir = os.path.join(USER_FOLDER, new_name)
|
||||||
|
os.makedirs(user_dir, exist_ok=True)
|
||||||
|
|
||||||
|
photo_path = os.path.join(user_dir, secure_filename(new_photo.filename))
|
||||||
|
new_photo.save(photo_path)
|
||||||
|
|
||||||
|
encoding = extract_face_encoding(photo_path)
|
||||||
|
if encoding:
|
||||||
|
user.encoding = json.dumps(encoding.tolist())
|
||||||
|
user.photo = photo_path
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def delete_user(user_id):
|
||||||
|
"""删除用户及其所有相关文件"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_folder = os.path.join("static", "uploads", "users", user.username)
|
||||||
|
user_folder = user_folder.replace("\\", "/")
|
||||||
|
|
||||||
|
# 删除整个用户文件夹及其内容
|
||||||
|
if os.path.exists(user_folder):
|
||||||
|
shutil.rmtree(user_folder)
|
||||||
|
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def match_face(photo_path, threshold=0.30):
|
||||||
|
"""匹配数据库中的人脸,一旦找到合适的直接返回"""
|
||||||
|
known_users = User.query.all()
|
||||||
|
if not known_users:
|
||||||
|
return None
|
||||||
|
|
||||||
|
unknown_encoding = extract_face_encoding(photo_path)
|
||||||
|
if unknown_encoding is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for user in known_users:
|
||||||
|
if not user.encoding:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
stored_encoding = np.array(json.loads(user.encoding))
|
||||||
|
if stored_encoding.size == 0 or stored_encoding.ndim != 1:
|
||||||
|
continue
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue # 解析失败,跳过
|
||||||
|
|
||||||
|
if np.linalg.norm(stored_encoding - unknown_encoding) < threshold:
|
||||||
|
return user # 直接返回匹配的用户
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def classify_photos(photos):
|
def classify_photos(photos):
|
||||||
"""处理上传的照片"""
|
"""分类照片"""
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
temp_photo_path = save_temp_photo(photo)
|
filename = secure_filename(photo.filename)
|
||||||
cropped_face_path = get_largest_face(temp_photo_path)
|
temp_photo_path = os.path.join(TEMP_FOLDER, filename)
|
||||||
|
temp_photo_path = temp_photo_path.replace("\\", "/")
|
||||||
|
|
||||||
if not cropped_face_path:
|
exif_data = save_with_exif(photo, temp_photo_path)
|
||||||
print(f"没有检测到人脸:{photo.filename}")
|
date_str = format_exif_date(exif_data)
|
||||||
|
if not date_str:
|
||||||
|
os.remove(temp_photo_path) # 无效时间的照片直接删除
|
||||||
continue
|
continue
|
||||||
|
|
||||||
matched_user = match_face(cropped_face_path)
|
matched_user = match_face(temp_photo_path)
|
||||||
timestamp = get_exif_time(temp_photo_path)
|
|
||||||
|
|
||||||
if matched_user:
|
if matched_user:
|
||||||
save_classified_photo(photo, matched_user, timestamp)
|
user_folder = os.path.join(USER_FOLDER, matched_user.username, date_str)
|
||||||
|
os.makedirs(user_folder, exist_ok=True)
|
||||||
|
|
||||||
|
classified_photo_path = os.path.join(user_folder, filename)
|
||||||
|
classified_photo_path =classified_photo_path.replace("\\", "/")
|
||||||
|
os.rename(temp_photo_path, classified_photo_path)
|
||||||
|
|
||||||
|
new_photo = Photo(
|
||||||
|
user_id=matched_user.id,
|
||||||
|
created_at=date_str,
|
||||||
|
classification=True,
|
||||||
|
classification_image_path=classified_photo_path
|
||||||
|
)
|
||||||
|
db.session.add(new_photo)
|
||||||
else:
|
else:
|
||||||
print(f"没有找到匹配的人脸:{photo.filename}")
|
os.remove(temp_photo_path) # 没匹配到的照片删除
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
# 根据姓名和时间查找照片
|
def search_photos(username, date):
|
||||||
def search_photos(name, date):
|
"""搜索用户的照片"""
|
||||||
user = User.query.filter_by(username=name).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return []
|
||||||
photos = Photo.query.filter_by(user_id=user.id).all()
|
|
||||||
filtered_photos = []
|
|
||||||
for photo in photos:
|
|
||||||
# 过滤日期
|
|
||||||
exif_data = piexif.load(photo.photo_path)
|
|
||||||
date_taken = exif_data.get(piexif.ExifIFD.DateTimeOriginal, b"").decode("utf-8")
|
|
||||||
if date_taken and date in date_taken:
|
|
||||||
filtered_photos.append(photo)
|
|
||||||
return filtered_photos
|
|
||||||
|
|
||||||
def find_match(image_path, known_face_encodings):
|
query = Photo.query.filter_by(user_id=user.id)
|
||||||
# 加载图片
|
if date:
|
||||||
unknown_image = face_recognition.load_image_file(image_path)
|
query = query.filter(Photo.created_at.like(f"{date}%"))
|
||||||
|
|
||||||
# 获取图片中的人脸特征编码
|
return query.all()
|
||||||
unknown_face_encoding = face_recognition.face_encodings(unknown_image)
|
|
||||||
|
|
||||||
if unknown_face_encoding:
|
|
||||||
unknown_face_encoding = unknown_face_encoding[0]
|
|
||||||
# 设置一个匹配的阈值,例如0.6,值越低匹配越宽松
|
|
||||||
matches = face_recognition.compare_faces(known_face_encodings, unknown_face_encoding, tolerance=0.6)
|
|
||||||
|
|
||||||
if True in matches:
|
|
||||||
match_index = matches.index(True)
|
|
||||||
return match_index
|
|
||||||
return None
|
|
||||||
|
|
Binary file not shown.
|
@ -14,4 +14,5 @@ class Photo(db.Model):
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
created_at = db.Column(db.String(50), nullable=False)
|
created_at = db.Column(db.String(50), nullable=False)
|
||||||
classification = db.Column(db.Boolean, default=False)
|
classification = db.Column(db.Boolean, default=False)
|
||||||
|
classification_confidence = db.Column(db.Float, default=0.0) # ✅ 添加字段
|
||||||
classification_image_path = db.Column(db.String(255), nullable=True)
|
classification_image_path = db.Column(db.String(255), nullable=True)
|
|
@ -7,15 +7,16 @@ body {
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin: 0 auto;
|
margin: 50px auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
margin-top: 50px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
color: #333;
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
@ -23,7 +24,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
@ -40,19 +41,47 @@ input[type="text"], input[type="file"], input[type="date"] {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"] {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
button[type="submit"]:hover {
|
button[type="submit"]:hover {
|
||||||
background-color: #45a049;
|
background-color: #45a049;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-edit, .btn-delete {
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background-color: #007BFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background-color: #DC3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
@ -68,48 +97,35 @@ th, td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-edit, .btn-delete {
|
/* 照片展示样式 */
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: #007BFF;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-delete {
|
|
||||||
background-color: #DC3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-delete:hover {
|
|
||||||
background-color: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-gallery {
|
.photo-gallery {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-item {
|
.photo-item {
|
||||||
margin: 10px;
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-item img {
|
.photo-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: auto;
|
||||||
object-fit: cover;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 用户头像 */
|
||||||
.user-photo {
|
.user-photo {
|
||||||
max-width: 100px; /* 设置最大宽度 */
|
max-width: 100px;
|
||||||
max-height: 100px; /* 设置最大高度 */
|
max-height: 100px;
|
||||||
width: auto; /* 保持宽高比例 */
|
width: auto;
|
||||||
height: auto; /* 保持宽高比例 */
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
|
@ -17,7 +17,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="date">日期:</label>
|
<label for="date">日期:</label>
|
||||||
<input type="text" id="date" name="date" placeholder="请输入日期 (YYYY-MM-DD)" required>
|
<input type="date" id="date" name="date" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">查询照片</button>
|
<button type="submit" class="btn">查询照片</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -27,11 +27,12 @@
|
||||||
<div class="photo-gallery">
|
<div class="photo-gallery">
|
||||||
{% for photo in photos %}
|
{% for photo in photos %}
|
||||||
<div class="photo-item">
|
<div class="photo-item">
|
||||||
<img src="{{ url_for('static', filename='uploads/users/' + photo.user.username + '/photo.jpg') }}" alt="用户照片">
|
<img src="{{ url_for('static', filename=photo.classification_image_path.replace('static/', '')) }}" alt="用户照片">
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -20,14 +20,18 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if photos %}
|
{% if photos %}
|
||||||
<h2>上传照片的分类结果:</h2>
|
<h2>分类结果:</h2>
|
||||||
<div class="photo-gallery">
|
<div class="photo-gallery">
|
||||||
{% for photo in photos %}
|
{% for photo in photos %}
|
||||||
<div class="photo-item">
|
<div class="photo-item">
|
||||||
<img src="{{ url_for('static', filename='uploads/temp/' + photo.filename) }}" alt="分类照片">
|
<img src="{{ url_for('static', filename=photo['filename'].replace('static/', '')) }}" alt="分类照片">
|
||||||
|
<p><strong>姓名:</strong> {{ photo['username'] }}</p>
|
||||||
|
<p><strong>时间:</strong> {{ photo['created_at'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p style="margin-top: 20px; font-size: 18px; color: gray;">暂无分类结果,请上传照片。</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -48,8 +48,8 @@
|
||||||
alt="用户照片" class="user-photo">
|
alt="用户照片" class="user-photo">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.encoding %}
|
{% if user.encoding is not none and user.encoding | length > 0 %}
|
||||||
<pre>{{ user.encoding[:10] | join(", ") }}...</pre>
|
<pre>{{ user.encoding[:5] }} ...</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
无数据
|
无数据
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in New Issue