commit ff7b93479700c588924a8b01dce1dc80054bfc7a Author: sairate Date: Wed Feb 5 12:29:13 2025 +0800 Signed-off-by: sairate diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e1582d9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6b3a436 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/photo.iml b/.idea/photo.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/photo.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..285ec61 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +下面是一个示例 README 文档,你可以将其保存为 `README.md` 文件,并根据实际需求进行调整: + +--- + +# Photo Project + +本项目是一个基于 Flask 的人脸识别应用,用于处理用户头像上传和照片匹配。系统通过提取上传图片中最大的人脸区域生成面部特征码,并将用户信息和照片信息存储到 SQLite 数据库中,支持查询用户照片。 + +## 项目功能 + +- **用户管理** + - 上传用户头像,自动提取最大人脸并生成面部特征码。 + - 编辑和删除用户信息。 + +- **照片上传** + - 支持批量照片上传。 + - 自动检测每张照片中最大的那张人脸,提取面部特征码,与已注册用户进行比对。 + - 匹配成功后将照片移动到用户目录,并记录拍摄时间。 + +- **查询功能** + - 根据用户名和拍摄日期查询用户照片。 + +- **其他功能** + - 根据图片 EXIF 信息获取拍摄时间。 + - 自动校正图片方向。 + +## 项目结构 + +``` +photo_project/ # 项目根目录 +├── .venv/ # 虚拟环境目录(可选) +├── 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/ # 临时目录,用于存储上传处理中的图片,分类完成的照片会删除 +``` + +## 安装与配置 + +1. **克隆项目** + + ```bash + git clone + cd photo_project + ``` + +2. **创建虚拟环境并安装依赖** + + ```bash + python -m venv .venv + source .venv/bin/activate # Linux/macOS + .venv\Scripts\activate # Windows + pip install -r requirements.txt + ``` + +3. **配置环境变量** + + 如果需要自定义密钥,可以设置环境变量 `SECRET_KEY`,例如: + + ```bash + export SECRET_KEY='your_secret_key' + ``` + +4. **初始化数据库** + + 运行项目时,系统会自动在应用上下文中创建数据库表。 + +## 运行项目 + +在项目根目录下执行: + +```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`。 + +## 版权与许可证 + +本项目仅供学习和测试使用,具体版权和许可证信息请根据实际情况添加。 + diff --git a/__pycache__/functions.cpython-310.pyc b/__pycache__/functions.cpython-310.pyc new file mode 100644 index 0000000..519bcf2 Binary files /dev/null and b/__pycache__/functions.cpython-310.pyc differ diff --git a/__pycache__/models.cpython-310.pyc b/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..9cff4f4 Binary files /dev/null and b/__pycache__/models.cpython-310.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..2e2611b --- /dev/null +++ b/app.py @@ -0,0 +1,94 @@ +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 models import db, User, Photo +import os +from flask_sqlalchemy import SQLAlchemy + +# 初始化Flask应用 +app = Flask(__name__) +app.secret_key = 'your_secret_key' + +# 设置数据库配置 +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' # 修改为你的数据库URI +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁用修改追踪 + +# 初始化SQLAlchemy +db.init_app(app) + +# 创建数据库表 +with app.app_context(): + db.create_all() + +# 首页路由 +@app.route('/') +def index(): + return render_template('index.html') + +# 用户管理页面 +@app.route('/user_management') +def user_management(): + users = User.query.all() + return render_template('user_management.html', users=users) + +@app.route('/get_encoding/') +def get_encoding(user_id): + user = User.query.get(user_id) + return jsonify({"encoding": user.encoding if user and user.encoding else "无数据"}) + + +# 添加用户 +@app.route('/add_user', methods=['POST']) +def add_new_user(): + username = request.form['username'] + userphoto = request.files['userphoto'] + if add_user(username, userphoto): + flash("用户添加成功", "success") + else: + flash("用户添加失败", "danger") + return redirect(url_for('user_management')) + +# 删除用户 +@app.route('/delete_user/', methods=['POST']) +def delete_existing_user(user_id): + if delete_user(user_id): + flash("用户删除成功", "success") + else: + flash("用户删除失败", "danger") + return redirect(url_for('user_management')) + +# 编辑用户 +@app.route('/edit_user/', methods=['GET', 'POST']) +def edit_existing_user(user_id): + user = get_user(user_id) + if request.method == 'POST': + username = request.form['username'] + userphoto = request.files.get('userphoto') + if edit_user(user_id, username, userphoto): + flash("用户信息更新成功", "success") + else: + flash("更新失败", "danger") + return redirect(url_for('user_management')) + return render_template('edit_user.html', user=user) + +# 照片上传页面 +@app.route('/upload', methods=['GET', 'POST']) +def upload(): + if request.method == 'POST': + photos = request.files.getlist('photos') + classify_photos(photos) # 调用分类处理函数 + return render_template('upload.html', photos=photos) + return render_template('upload.html') + + +# 照片搜索 +@app.route('/search', methods=['GET', 'POST']) +def search(): + if request.method == 'POST': + name = request.form['name'] + date = request.form['date'] + photos = search_photos(name, date) + return render_template('search.html', photos=photos) + return render_template('search.html') + +if __name__ == '__main__': + app.run(debug=True) diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..3342ad0 --- /dev/null +++ b/functions.py @@ -0,0 +1,243 @@ +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 numpy as np + +def process_face_encoding(photo_path): + """提取人脸特征编码并裁剪出最大的面部区域""" + # 加载图片 + image = face_recognition.load_image_file(photo_path) + + # 获取所有人脸的位置 + face_locations = face_recognition.face_locations(image) + + if not face_locations: + return None # 没有检测到人脸 + + # 找到最大的人脸 + largest_face = max(face_locations, key=lambda x: (x[2] - x[0]) * (x[3] - x[1])) # 按照面积选取最大的人脸 + + # 获取该人脸的特征编码 + face_encodings = face_recognition.face_encodings(image, [largest_face]) + + if face_encodings: + # 裁剪出最大的人脸部分 + top, right, bottom, left = largest_face + 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): + """添加用户并存储人脸特征""" + if 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 + +# 编辑用户 +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) + + new_photo_path = os.path.join(user_dir, photo.filename) + + try: + with Image.open(photo) as img: + img.save(new_photo_path, quality=95) + except Exception as e: + print(f"保存图片时出错: {e}") + + # 保存到数据库 + photo_record = Photo(user_id=user.id, created_at=timestamp, classification=True, + classification_image_path=new_photo_path) + db.session.add(photo_record) + db.session.commit() + + print(f"照片 {photo.filename} 已保存至 {new_photo_path}") + + +def classify_photos(photos): + """处理上传的照片""" + for photo in photos: + temp_photo_path = save_temp_photo(photo) + cropped_face_path = get_largest_face(temp_photo_path) + + if not cropped_face_path: + print(f"没有检测到人脸:{photo.filename}") + continue + + matched_user = match_face(cropped_face_path) + timestamp = get_exif_time(temp_photo_path) + + if matched_user: + save_classified_photo(photo, matched_user, timestamp) + else: + print(f"没有找到匹配的人脸:{photo.filename}") + + +# 根据姓名和时间查找照片 +def search_photos(name, date): + user = User.query.filter_by(username=name).first() + if not user: + 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): + # 加载图片 + unknown_image = face_recognition.load_image_file(image_path) + + # 获取图片中的人脸特征编码 + 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 diff --git a/instance/database.db b/instance/database.db new file mode 100644 index 0000000..1dbbf68 Binary files /dev/null and b/instance/database.db differ diff --git a/models.py b/models.py new file mode 100644 index 0000000..9ec99cc --- /dev/null +++ b/models.py @@ -0,0 +1,17 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(50), unique=True, nullable=False) + encoding = db.Column(db.PickleType, nullable=True) # 存储人脸特征 + photo = db.Column(db.String(255), nullable=True) + + +class Photo(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.String(50), nullable=False) + classification = db.Column(db.Boolean, default=False) + classification_image_path = db.Column(db.String(255), nullable=True) \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..0f93783 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,115 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.container { + width: 80%; + margin: 0 auto; + padding: 20px; + background-color: white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + margin-top: 50px; +} + +h1, h2 { + color: #333; +} + +form { + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 10px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="text"], input[type="file"], input[type="date"] { + width: 100%; + padding: 8px; + margin: 5px 0; + border-radius: 4px; + border: 1px solid #ddd; +} + +button[type="submit"] { + padding: 10px 20px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button[type="submit"]:hover { + background-color: #45a049; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +table, th, td { + border: 1px solid #ddd; +} + +th, td { + padding: 10px; + 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 { + display: flex; + flex-wrap: wrap; +} + +.photo-item { + margin: 10px; + width: 200px; + height: 200px; + overflow: hidden; +} + +.photo-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.user-photo { + max-width: 100px; /* 设置最大宽度 */ + max-height: 100px; /* 设置最大高度 */ + width: auto; /* 保持宽高比例 */ + height: auto; /* 保持宽高比例 */ +} diff --git a/static/uploads/temp/IMG_20250119_120604.jpg b/static/uploads/temp/IMG_20250119_120604.jpg new file mode 100644 index 0000000..2a44ef8 Binary files /dev/null and b/static/uploads/temp/IMG_20250119_120604.jpg differ diff --git a/static/uploads/temp/IMG_20250119_120604_face.jpg b/static/uploads/temp/IMG_20250119_120604_face.jpg new file mode 100644 index 0000000..4fb7ea9 Binary files /dev/null and b/static/uploads/temp/IMG_20250119_120604_face.jpg differ diff --git a/static/uploads/users/张庭嘉/photo.jpg b/static/uploads/users/张庭嘉/photo.jpg new file mode 100644 index 0000000..2a44ef8 Binary files /dev/null and b/static/uploads/users/张庭嘉/photo.jpg differ diff --git a/static/uploads/users/张庭嘉/photo_cropped.jpg b/static/uploads/users/张庭嘉/photo_cropped.jpg new file mode 100644 index 0000000..4fb7ea9 Binary files /dev/null and b/static/uploads/users/张庭嘉/photo_cropped.jpg differ diff --git a/templates/edit_user.html b/templates/edit_user.html new file mode 100644 index 0000000..0b35dbe --- /dev/null +++ b/templates/edit_user.html @@ -0,0 +1,25 @@ + + + + + + 编辑用户 + + + +
+

编辑用户

+
+
+ + +
+
+ + +
+ +
+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..593ba65 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,21 @@ + + + + + + 首页 + + + +
+

欢迎来到照片管理系统

+ +
+ + diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..fd2011d --- /dev/null +++ b/templates/search.html @@ -0,0 +1,37 @@ + + + + + + 查询照片 + + + +
+

根据姓名和时间查找照片

+ +
+
+ + +
+
+ + +
+ +
+ + {% if photos %} +

查询结果:

+ + {% endif %} +
+ + diff --git a/templates/upload.html b/templates/upload.html new file mode 100644 index 0000000..177c169 --- /dev/null +++ b/templates/upload.html @@ -0,0 +1,34 @@ + + + + + + 上传照片 + + + +
+

上传照片并进行分类

+ + +
+
+ + +
+ +
+ + {% if photos %} +

上传照片的分类结果:

+ + {% endif %} +
+ + diff --git a/templates/user_management.html b/templates/user_management.html new file mode 100644 index 0000000..e5c8dba --- /dev/null +++ b/templates/user_management.html @@ -0,0 +1,71 @@ + + + + + + 用户管理 + + + +
+

用户管理

+ + +
+

添加用户

+
+
+ + +
+
+ + +
+ +
+
+ +
+

用户列表

+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
ID用户名照片面部编码操作
{{ user.id }}{{ user.username }} + 用户照片 + + {% if user.encoding %} +
{{ user.encoding[:10] | join(", ") }}...
+ {% else %} + 无数据 + {% endif %} +
+ 编辑 +
+ +
+
+
+ +
+ + diff --git a/test.py b/test.py new file mode 100644 index 0000000..3bb401a --- /dev/null +++ b/test.py @@ -0,0 +1,2 @@ +import functions +print(functions.get_exif_date("./static/uploads/temp/IMG_20250119_120604.jpg")) \ No newline at end of file