Spaces:
Running
Running
| """ | |
| Flask Web Application for YOLOv12 Face Detection | |
| Supports image upload, video upload, and live webcam streaming | |
| """ | |
| import base64 | |
| import logging | |
| import os | |
| from datetime import datetime | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| from dotenv import load_dotenv | |
| from flask import Flask, jsonify, render_template, request, send_file | |
| from flask_limiter import Limiter | |
| from flask_limiter.errors import RateLimitExceeded | |
| from flask_limiter.util import get_remote_address | |
| from flask_sqlalchemy import SQLAlchemy | |
| from flask_sqlalchemy.model import Model | |
| from sqlalchemy.exc import OperationalError | |
| from werkzeug.utils import secure_filename | |
| from face_detection_yolov12 import YOLOv12FaceDetector, detect_from_video | |
| # Initialize Flask app | |
| load_dotenv() | |
| app = Flask(__name__, template_folder="../web/templates") | |
| # Configuration | |
| app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DB_URL") | |
| app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "fallback_secret_key_neu_khong_co") | |
| app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False | |
| app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"pool_recycle": 280} # Giữ kết nối MySQL ổn định | |
| db = SQLAlchemy(app) | |
| PROJECT_ROOT = Path(__file__).parent.parent | |
| UPLOAD_FOLDER = PROJECT_ROOT / "data" / "uploads" | |
| MODELS_DIR = PROJECT_ROOT / "models" | |
| ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "mp4", "avi", "mov", "mkv"} | |
| MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB | |
| ALLOWED_MODELS = { | |
| "yolov12n-face.pt", | |
| "yolov12s-face.pt", | |
| "yolov12m-face.pt", | |
| "yolov12l-face.pt", | |
| } | |
| UPLOAD_FOLDER.mkdir(exist_ok=True) | |
| app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER | |
| app.config["MAX_CONTENT_LENGTH"] = MAX_FILE_SIZE | |
| limiter = Limiter( | |
| key_func=get_remote_address, | |
| app=app, | |
| default_limits=["10000 per day", "1000 per hour"], | |
| storage_uri="memory://", | |
| ) | |
| # Model cache | |
| detector_cache = {} | |
| class Feedback(db.Model): # type: ignore | |
| __tablename__ = "feedbacks" | |
| id = db.Column(db.Integer, primary_key=True) | |
| filename = db.Column(db.String(255)) | |
| model_name = db.Column(db.String(50)) | |
| rating = db.Column(db.Integer) | |
| comment = db.Column(db.Text) | |
| created_at = db.Column(db.DateTime, default=datetime.utcnow) | |
| with app.app_context(): | |
| try: | |
| db.create_all() | |
| print("Connect to MySQL successfully!") | |
| except Exception as e: | |
| print(f"Connection error: {e}") | |
| def get_detector(model_name): | |
| """Get or create detector instance (cached)""" | |
| safe_name = secure_filename(model_name) | |
| if safe_name not in ALLOWED_MODELS: | |
| logging.error(f"Attempt to load unsupported model: {safe_name}") | |
| raise ValueError(f"Unsupported model: {safe_name}") | |
| if safe_name not in detector_cache: | |
| model_path = MODELS_DIR / safe_name | |
| try: | |
| final_path = model_path.resolve() | |
| safe_root = MODELS_DIR.resolve() | |
| if not str(final_path).startswith(str(safe_root)): | |
| logging.error(f"Security Alert: Symlink attack detected! {final_path}") | |
| raise ValueError("Invalid model path (Symlink violation)") | |
| except Exception as e: | |
| logging.error(f"Error resolving model path: {str(e)}") | |
| raise FileNotFoundError(f"Model path error: {str(e)}") | |
| if not final_path.exists(): | |
| logging.error(f"Model file not found: {final_path}") | |
| raise FileNotFoundError(f"Model not found: {final_path}") | |
| detector_cache[safe_name] = YOLOv12FaceDetector(str(final_path)) | |
| return detector_cache[safe_name] | |
| def allowed_file(filename): | |
| """Check if file extension is allowed""" | |
| return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS | |
| def is_image(filename): | |
| """Check if file is image""" | |
| ext = filename.rsplit(".", 1)[1].lower() | |
| return ext in {"jpg", "jpeg", "png", "gif"} | |
| def is_video(filename): | |
| """Check if file is video""" | |
| ext = filename.rsplit(".", 1)[1].lower() | |
| return ext in {"mp4", "avi", "mov", "mkv"} | |
| def index(): | |
| """Main page""" | |
| return render_template("index.html") | |
| def detect_image(): | |
| """Detect faces in uploaded image""" | |
| try: | |
| if "file" not in request.files: | |
| return jsonify({"error": "No file provided"}), 400 | |
| file = request.files["file"] | |
| if file.filename == "": | |
| return jsonify({"error": "No file selected"}), 400 | |
| if not allowed_file(file.filename) or not is_image(file.filename): | |
| return jsonify({"error": "Only image files allowed"}), 400 | |
| # Get parameters | |
| model = request.form.get("model", "yolov12l-face.pt") | |
| blur = request.form.get("blur") == "true" | |
| if model not in ALLOWED_MODELS: | |
| app.logger.info(f"Invalid model '{model}' requested. Fallback to default.") | |
| model = "yolov12l-face.pt" | |
| # Get detector | |
| detector = get_detector(model) | |
| # Read image directly from file object | |
| image_data = file.read() | |
| nparr = np.frombuffer(image_data, np.uint8) | |
| image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) | |
| # For webcam frames, use optimized detection with reduced resolution | |
| is_webcam = "webcam" in file.filename.lower() | |
| if is_webcam: | |
| # Use optimized detection for speed | |
| detections = detector.detect_faces_optimized(image, conf_threshold=0.32, max_width=480) | |
| else: | |
| # Use standard detection for uploaded files | |
| detections = detector.detect_faces(image, conf_threshold=0.32) | |
| # Process image: Draw or Blur | |
| if blur: | |
| result_image = detector.blur_faces(image, detections) | |
| else: | |
| result_image = detector.draw_faces(image, detections, show_confidence=True) | |
| if result_image is None: | |
| return jsonify({"error": "Failed to process image"}), 500 | |
| # Extract crops for gallery | |
| crops_base64 = [] | |
| if len(detections) > 0: | |
| crops = detector.get_face_crops(image, detections) | |
| for crop in crops: | |
| _, buffer = cv2.imencode(".jpg", crop) | |
| crops_base64.append(base64.b64encode(buffer).decode()) | |
| # Convert result to base64 for display | |
| _, buffer = cv2.imencode(".jpg", result_image) | |
| img_base64 = base64.b64encode(buffer).decode() | |
| # Prepare response | |
| response = { | |
| "success": True, | |
| "image": f"data:image/jpeg;base64,{img_base64}", | |
| "detections": { | |
| "count": len(detections), | |
| "faces": [ | |
| { | |
| "id": i + 1, | |
| "confidence": f"{det['confidence']:.2%}", | |
| "width": det["w"], | |
| "height": det["h"], | |
| "position": f"({det['x1']}, {det['y1']})", | |
| } | |
| for i, det in enumerate(detections) | |
| ], | |
| "crops": crops_base64, | |
| }, | |
| } | |
| return jsonify(response) | |
| except Exception: | |
| logging.exception("Error during image detection") | |
| return jsonify({"error": "Internal server error during image detection"}), 500 | |
| def detect_video(): | |
| """Detect faces in uploaded video""" | |
| try: | |
| if "file" not in request.files: | |
| return jsonify({"error": "No file provided"}), 400 | |
| file = request.files["file"] | |
| if file.filename == "": | |
| return jsonify({"error": "No file selected"}), 400 | |
| if not allowed_file(file.filename) or not is_video(file.filename): | |
| return jsonify({"error": "Only video files allowed"}), 400 | |
| # Get model selection | |
| model = request.form.get("model", "yolov12m-face.pt") | |
| if model not in ALLOWED_MODELS: | |
| app.logger.info(f"Invalid model '{model}' requested. Fallback to default.") | |
| model = "yolov12m-face.pt" | |
| # Save uploaded file | |
| filename = secure_filename(file.filename) | |
| input_path = UPLOAD_FOLDER / f"input_{filename}" | |
| output_path = UPLOAD_FOLDER / f"output_{filename}" | |
| file.save(input_path) | |
| # Detect faces in video | |
| detect_from_video( | |
| video_path=str(input_path), | |
| model_path=str(MODELS_DIR / model), | |
| output_path=str(output_path), | |
| conf_threshold=0.32, | |
| ) | |
| # Return file info | |
| response = { | |
| "success": True, | |
| "message": "Video processing complete", | |
| "output_file": output_path.name, | |
| "download_url": f"/api/download/{output_path.name}", | |
| } | |
| return jsonify(response) | |
| except Exception: | |
| # Log the full exception server-side without exposing details to the client | |
| app.logger.exception("Error while processing video detection request") | |
| return jsonify({"error": "Internal server error"}), 500 | |
| def download_file(filename): | |
| """Download processed file""" | |
| try: | |
| filepath = UPLOAD_FOLDER / secure_filename(filename) | |
| if not filepath.exists(): | |
| return jsonify({"error": "File not found"}), 404 | |
| return send_file(filepath, as_attachment=True) | |
| except Exception as e: | |
| # Log the full exception server-side without exposing details to the client | |
| app.logger.exception("Error while processing download request for %s", filename) | |
| return jsonify({"error": "Internal server error"}), 500 | |
| def get_models(): | |
| """Get ALL available models for dropdown selection""" | |
| models = { | |
| "nano": { | |
| "name": "yolov12n-face.pt", | |
| "label": "Nano (n) - Fastest", | |
| "description": "Real-time speed, best for CPU/Webcam", | |
| "size": "Smallest", | |
| }, | |
| "small": { | |
| "name": "yolov12s-face.pt", | |
| "label": "Small (s) - Balanced", | |
| "description": "Good balance of speed and accuracy", | |
| "size": "Small", | |
| }, | |
| "medium": { | |
| "name": "yolov12m-face.pt", | |
| "label": "Medium (m) - High Precision", | |
| "description": "High accuracy, requires decent GPU", | |
| "size": "Medium", | |
| }, | |
| "large": { | |
| "name": "yolov12l-face.pt", | |
| "label": "Large (l) - Max Accuracy", | |
| "description": "Best detection quality, slowest speed", | |
| "size": "Large", | |
| }, | |
| } | |
| available = {} | |
| for key, info in models.items(): | |
| model_path = MODELS_DIR / info["name"] | |
| if model_path.exists(): | |
| available[key] = info | |
| order = ["nano", "small", "medium", "large"] | |
| sorted_available = {k: available[k] for k in order if k in available} | |
| return jsonify(sorted_available) | |
| def submit_feedback(): | |
| try: | |
| data = request.json | |
| new_fb = Feedback( | |
| filename=data.get("filename"), | |
| model_name=data.get("model"), | |
| rating=data.get("rating"), | |
| comment=data.get("comment", ""), | |
| ) | |
| db.session.add(new_fb) | |
| db.session.commit() | |
| return jsonify({"success": True, "message": "Rating saved!"}) | |
| except Exception as e: | |
| app.logger.error(f"DB Error: {e}") | |
| return jsonify({"error": "Database error"}), 500 | |
| def health_check(): | |
| """Health check endpoint""" | |
| return jsonify({"status": "healthy", "service": "Face Detection API"}) | |
| def request_entity_too_large(error): | |
| """Handle file size exceeded""" | |
| return jsonify({"error": "File too large. Maximum 500MB allowed"}), 413 | |
| def internal_error(error): | |
| """Handle internal server error""" | |
| return jsonify({"error": "Internal server error"}), 500 | |
| def handle_rate_limit_error(e): | |
| """Handle rate limit exceeded errors""" | |
| app.logger.warning(f"Rate limit exceeded: {e.description}") | |
| return ( | |
| jsonify( | |
| { | |
| "error": "Too many requests", | |
| "message": f"Too fast! Please wait a moment. ({e.description})", | |
| } | |
| ), | |
| 429, | |
| ) | |
| if __name__ == "__main__": | |
| print("\n" + "=" * 70) | |
| print("🌐 Starting YOLOv12 Face Detection Web Server") | |
| print("=" * 70) | |
| print("\n📍 Server: http://localhost:7860") | |
| print("📁 Upload folder: ", UPLOAD_FOLDER) | |
| print("🔧 Models folder: ", MODELS_DIR) | |
| print("\n🎯 Available endpoints:") | |
| print(" GET / - Web interface") | |
| print(" POST /api/detect-image - Detect faces in image") | |
| print(" POST /api/detect-video - Detect faces in video") | |
| print(" GET /api/models - Get available models") | |
| print(" GET /api/health - Health check") | |
| print("\n" + "=" * 70 + "\n") | |
| # Determine debug mode from environment (default: disabled) | |
| debug_mode = os.getenv("FLASK_ENV") == "development" | |
| # Run Flask app | |
| app.run(host="0.0.0.0", port=7860, debug=debug_mode, use_reloader=False) |