chrisnguyenx's picture
Deploy FaceID AI Service via Docker
75557db
Raw
History Blame Contribute Delete
8.23 kB
"""
api/faces.py — Endpoints quản lý khuôn mặt (upload & lấy ảnh)
POST /api/faces — Upload & đăng ký khuôn mặt mới
GET /api/face-crops/<id> — Lấy ảnh crop từ MongoDB
GET /api/face_crop/<id> — Legacy alias
"""
import logging
import os
from flask import Blueprint, Response, jsonify, request
import config
from face_model.analyzer import face_analyzer
from storage.mongo_storage import mongo_storage
from storage.pg_storage import pg_storage
from utils.image import crop_face, decode_image, image_to_bytes
logger = logging.getLogger("api.faces")
bp = Blueprint("faces", __name__, url_prefix="/api")
@bp.post("/faces")
def upload_face():
"""
POST /api/faces
Body: multipart/form-data
- file: ảnh JPEG/PNG
- name: tên người (form field hoặc query param, tùy chọn)
Luồng:
1. Detect khuôn mặt
2. Crop → lưu MongoDB face_crops (image_type='registration')
3. Đăng ký persons + face_embeddings trên Supabase
4. Cập nhật RAM cache
"""
if "file" not in request.files:
return jsonify({"error": {
"code": "FILE_REQUIRED",
"message": "Không tìm thấy file ảnh trong request.",
"status": 400,
}}), 400
file = request.files["file"]
if file.filename == "":
return jsonify({"error": {
"code": "EMPTY_FILENAME",
"message": "Tên file rỗng.",
"status": 400,
}}), 400
# Lấy tên người từ form/query
name = (
request.args.get("name")
or request.form.get("name")
or os.path.splitext(file.filename)[0]
)
try:
file_bytes = file.read()
img = decode_image(file_bytes)
if img is None:
return jsonify({"error": {
"code": "INVALID_IMAGE",
"message": "Không đọc được ảnh.",
"status": 400,
}}), 400
# Detect face
faces = face_analyzer.get_faces(img)
if not faces:
return jsonify({"error": {
"code": "NO_FACE_DETECTED",
"message": "Không tìm thấy khuôn mặt trong ảnh.",
"status": 422,
}}), 422
# Crop khuôn mặt đầu tiên
face = faces[0]
bbox_arr = face.bbox.astype(int)
embedding = face.normed_embedding
det_score = float(face.det_score) if hasattr(face, "det_score") else 0.0
face_img, bbox_coords = crop_face(img, bbox_arr, padding=20)
crop_bytes = image_to_bytes(face_img)
# Lưu raw image vào MongoDB (source_type = upload)
h, w = img.shape[:2]
raw_image_id = mongo_storage.save_raw_image(
img_bytes=file_bytes,
source_type="upload",
width=w,
height=h,
fmt="jpeg",
)
# Lưu face_crop vào MongoDB (image_type = registration)
crop_id = mongo_storage.save_face_crop(
img_bytes=crop_bytes,
raw_image_id=raw_image_id,
bbox=bbox_coords,
image_type="registration",
confidence=det_score,
quality_score=det_score,
model_name=config.MODEL_NAME,
)
if not crop_id:
return jsonify({"error": {
"code": "STORAGE_FAILED",
"message": "Không thể lưu ảnh vào MongoDB.",
"status": 500,
}}), 500
# Đăng ký lên Supabase
person_id, embedding_id = pg_storage.register_new_face(
name=name,
embedding=embedding.tolist(),
face_crop_mongo_id=crop_id,
)
# Cập nhật links trong face_crop
mongo_storage.update_face_crop_links(
crop_id=crop_id,
person_id=person_id,
visit_id="",
recognition_log_id="",
)
# Cập nhật RAM cache
face_analyzer.add_to_cache(
embedding=embedding,
name=name,
person_id=person_id,
embedding_id=embedding_id,
mongo_id=crop_id,
)
return jsonify({
"success": True,
"person_id": person_id,
"name": name,
"crop_id": crop_id,
"url": f"/api/face-crops/{crop_id}",
"total_faces": face_analyzer.total,
}), 201
except Exception as e:
logger.error(f"[upload_face] Error: {e}", exc_info=True)
return jsonify({"error": {
"code": "UPLOAD_FAILED",
"message": f"Lỗi khi đăng ký khuôn mặt: {e}",
"status": 500,
}}), 500
@bp.post("/upload")
def upload_legacy():
"""
Legacy alias → POST /api/faces (backward compatibility với frontend).
Response format giữ nguyên để frontend không cần sửa.
"""
if "file" not in request.files:
return jsonify({"detail": "Không tìm thấy file tải lên."}), 400
file = request.files["file"]
name = (
request.args.get("name")
or request.form.get("name")
or os.path.splitext(file.filename or "face")[0]
)
try:
file_bytes = file.read()
img = decode_image(file_bytes)
if img is None:
return jsonify({"detail": "Không đọc được ảnh."}), 400
faces = face_analyzer.get_faces(img)
if not faces:
return jsonify({"detail": "Không tìm thấy khuôn mặt trong ảnh."}), 422
face = faces[0]
bbox_arr = face.bbox.astype(int)
embedding = face.normed_embedding
det_score = float(face.det_score) if hasattr(face, "det_score") else 0.0
face_img, bbox_coords = crop_face(img, bbox_arr, padding=20)
crop_bytes = image_to_bytes(face_img)
h, w = img.shape[:2]
raw_image_id = mongo_storage.save_raw_image(
img_bytes=file_bytes, source_type="upload", width=w, height=h, fmt="jpeg"
)
crop_id = mongo_storage.save_face_crop(
img_bytes=crop_bytes, raw_image_id=raw_image_id,
bbox=bbox_coords, image_type="registration",
confidence=det_score, quality_score=det_score,
model_name=config.MODEL_NAME,
)
if not crop_id:
return jsonify({"detail": "Lỗi lưu trữ hình ảnh vào MongoDB Atlas."}), 500
person_id, embedding_id = pg_storage.register_new_face(
name=name, embedding=embedding.tolist(), face_crop_mongo_id=crop_id
)
mongo_storage.update_face_crop_links(
crop_id=crop_id, person_id=person_id, visit_id="", recognition_log_id=""
)
face_analyzer.add_to_cache(
embedding=embedding, name=name,
person_id=person_id, embedding_id=embedding_id, mongo_id=crop_id,
)
return jsonify({
"success": True,
"filename": f"{name}.jpg",
"name": name,
"url": f"/api/face-crops/{crop_id}",
"total_faces": face_analyzer.total,
}), 200
except Exception as e:
logger.error(f"[upload_legacy] Error: {e}", exc_info=True)
return jsonify({"detail": f"Lỗi khi đăng ký khuôn mặt: {e}"}), 500
# ── Serve ảnh crop ─────────────────────────────────────────────────────────
@bp.get("/face-crops/<crop_id>")
def get_face_crop(crop_id: str):
"""GET /api/face-crops/<crop_id> — Lấy ảnh crop từ MongoDB."""
img_bytes = mongo_storage.get_face_crop_bytes(crop_id)
if not img_bytes:
return jsonify({"error": {
"code": "NOT_FOUND",
"message": "Không tìm thấy hình ảnh khuôn mặt.",
"status": 404,
}}), 404
return Response(img_bytes, mimetype="image/jpeg")
@bp.get("/face_crop/<crop_id>")
def get_face_crop_legacy(crop_id: str):
"""Legacy alias → GET /api/face-crops/<crop_id>"""
return get_face_crop(crop_id)