Spaces:
Sleeping
Sleeping
| """ | |
| Flask web app for FER inference. | |
| Run: python app.py (from the inference/ directory) | |
| Deployment: | |
| Local: weights loaded from ../models/model_weights.pth | |
| HF Spaces: set HF_REPO_ID env var (e.g. "yourname/fer-weights") | |
| weights are downloaded from HF Model Hub at startup | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import io | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| from flask import Flask, jsonify, render_template, request | |
| from flask_cors import CORS | |
| from PIL import Image | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from inference import FERPredictor | |
| app = Flask(__name__) | |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB upload limit | |
| # Allow the Vercel/GitHub Pages frontend to call /predict cross-origin | |
| CORS(app, resources={r"/predict": {"origins": "*"}}) | |
| # Weights: download from HF Model Hub on Spaces, load locally otherwise | |
| _HF_REPO_ID = os.getenv('HF_REPO_ID') | |
| if _HF_REPO_ID: | |
| from huggingface_hub import hf_hub_download | |
| WEIGHTS_PATH = hf_hub_download(repo_id=_HF_REPO_ID, filename='model_weights.pth') | |
| else: | |
| WEIGHTS_PATH = str(Path(__file__).parent.parent / 'models' / 'model_weights.pth') | |
| predictor: FERPredictor | None = None | |
| def get_predictor() -> FERPredictor: | |
| global predictor | |
| if predictor is None: | |
| predictor = FERPredictor(weights_path=WEIGHTS_PATH, device='auto') | |
| return predictor | |
| def annotate_image(pil_img: Image.Image, face_results: list[dict]) -> str: | |
| """Draw bounding boxes on image, return base64-encoded JPEG.""" | |
| img_bgr = cv2.cvtColor(np.array(pil_img.convert('RGB')), cv2.COLOR_RGB2BGR) | |
| EMOTION_BGR = { | |
| 'happy': (80, 200, 46), | |
| 'angry': (60, 60, 231), | |
| 'sad': (200, 80, 52), | |
| 'fear': (0, 130, 230), | |
| 'surprise': (0, 210, 240), | |
| 'disgust': (150, 50, 130), | |
| 'neutral': (160, 160, 160), | |
| } | |
| for res in face_results: | |
| bbox = res.get('bbox') | |
| if bbox is None: | |
| continue | |
| x, y, w, h = (int(v) for v in bbox) | |
| emotion = res['emotion'] | |
| conf = res['confidence'] | |
| color = EMOTION_BGR.get(emotion, (200, 200, 200)) | |
| cv2.rectangle(img_bgr, (x, y), (x + w, y + h), color, 2) | |
| label = f"{emotion} {conf:.2f}" | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| scale, thickness = 0.6, 2 | |
| (tw, th), baseline = cv2.getTextSize(label, font, scale, thickness) | |
| ty = max(y - 6, th + 4) | |
| cv2.rectangle(img_bgr, (x, ty - th - 4), (x + tw + 6, ty + baseline), color, cv2.FILLED) | |
| cv2.putText(img_bgr, label, (x + 3, ty - 2), font, scale, (255, 255, 255), thickness, cv2.LINE_AA) | |
| rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| pil_out = Image.fromarray(rgb) | |
| buf = io.BytesIO() | |
| pil_out.save(buf, format='JPEG', quality=90) | |
| return 'data:image/jpeg;base64,' + base64.b64encode(buf.getvalue()).decode() | |
| def pil_to_b64(pil_img: Image.Image) -> str: | |
| buf = io.BytesIO() | |
| pil_img.convert('RGB').save(buf, format='JPEG', quality=90) | |
| return 'data:image/jpeg;base64,' + base64.b64encode(buf.getvalue()).decode() | |
| def index(): | |
| return render_template('index.html') | |
| def predict(): | |
| try: | |
| p = get_predictor() | |
| data = request.get_json(silent=True) | |
| if data and data.get('image'): | |
| # Base64 image from webcam | |
| b64 = data['image'] | |
| if ',' in b64: | |
| b64 = b64.split(',', 1)[1] | |
| img_bytes = base64.b64decode(b64) | |
| pil_img = Image.open(io.BytesIO(img_bytes)).convert('RGB') | |
| elif 'file' in request.files: | |
| f = request.files['file'] | |
| pil_img = Image.open(f.stream).convert('RGB') | |
| else: | |
| return jsonify({'error': 'No image provided'}), 400 | |
| face_results = p.predict_with_face_detection(pil_img, method='mtcnn') | |
| # If MTCNN found no faces, fall back to whole-image prediction | |
| if not face_results or all(r.get('bbox') is None for r in face_results): | |
| result = p.predict_image(pil_img) | |
| result.update({'bbox': None, 'face_index': 0}) | |
| face_results = [result] | |
| annotated_b64 = annotate_image(pil_img, face_results) | |
| faces_out = [] | |
| for r in face_results: | |
| faces_out.append({ | |
| 'emotion': r['emotion'], | |
| 'confidence': round(r['confidence'], 4), | |
| 'probabilities': {k: round(v, 4) for k, v in r['probabilities'].items()}, | |
| 'top3': [[e, round(p_, 4)] for e, p_ in r.get('top3', [])], | |
| 'bbox': r.get('bbox'), | |
| }) | |
| return jsonify({ | |
| 'annotated_image': annotated_b64, | |
| 'faces': faces_out, | |
| }) | |
| except Exception as exc: | |
| import traceback | |
| traceback.print_exc() | |
| return jsonify({'error': str(exc)}), 500 | |
| if __name__ == '__main__': | |
| print("[INFO] Pre-loading model...") | |
| get_predictor() | |
| port = int(os.getenv('PORT', 7860)) # HF Spaces uses 7860 | |
| app.run(host='0.0.0.0', port=port, debug=False) | |