Spaces:
Running
Running
| """ | |
| Flask Web Application for YOLOv12 Face Detection | |
| Supports image upload, video upload, and live webcam streaming | |
| """ | |
| from flask import Flask, render_template, request, jsonify, send_file | |
| from werkzeug.utils import secure_filename | |
| from pathlib import Path | |
| import os | |
| import cv2 | |
| import numpy as np | |
| import base64 | |
| import logging | |
| from face_detection_yolov12 import YOLOv12FaceDetector, detect_from_video | |
| # Initialize Flask app | |
| app = Flask(__name__, template_folder='../web/templates') | |
| # Configuration | |
| 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 | |
| # Model cache | |
| detector_cache = {} | |
| 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 model selection | |
| model = request.form.get('model', 'yolov12l-face.pt') | |
| 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.35, max_width=480) | |
| else: | |
| # Use standard detection for uploaded files | |
| detections = detector.detect_faces(image, conf_threshold=0.35) | |
| # Draw detections | |
| result_image = detector.draw_faces(image, detections, show_confidence=True) | |
| if result_image is None: | |
| return jsonify({'error': 'Failed to process image'}), 500 | |
| # 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) | |
| ] | |
| } | |
| } | |
| return jsonify(response) | |
| except Exception as e: | |
| 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.35 | |
| ) | |
| # 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 as e: | |
| # 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""" | |
| # Cập nhật danh sách đầy đủ 4 models | |
| 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' | |
| } | |
| } | |
| # Chỉ trả về những model thực sự tồn tại trong thư mục | |
| available = {} | |
| for key, info in models.items(): | |
| model_path = MODELS_DIR / info['name'] | |
| if model_path.exists(): | |
| available[key] = info | |
| # Sắp xếp theo thứ tự kích thước để hiển thị đẹp hơn | |
| order = ['nano', 'small', 'medium', 'large'] | |
| sorted_available = {k: available[k] for k in order if k in available} | |
| return jsonify(sorted_available) | |
| 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 | |
| if __name__ == '__main__': | |
| print("\n" + "="*70) | |
| print("🌐 Starting YOLOv12 Face Detection Web Server") | |
| print("="*70) | |
| print("\n📍 Server: http://localhost:5000") | |
| 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 | |
| ) |