Download app.py from crangana/face-attribute-classifier: direct link, hf CLI and curl.
- Browser
- Download file 5.03 kB
-
https://huggingface.co/spaces/crangana/face-attribute-classifier/resolve/main/app.py
- Command line
-
hf download hf://spaces/crangana/face-attribute-classifier/app.py
-
curl -L -o app.py https://huggingface.co/spaces/crangana/face-attribute-classifier/resolve/main/app.py
5.03 kB
| import os | |
| import base64 | |
| import requests | |
| import gradio as gr | |
| import spaces | |
| from PIL import Image | |
| from transformers import ViTImageProcessor, ViTForImageClassification, pipeline | |
| # --- Roboflow face-detection key, read from a Space secret (not hardcoded) --- | |
| ROBOFLOW_API_KEY = os.environ.get("ROBOFLOW_API_KEY", "") | |
| # --- Load all three classifiers once at startup --- | |
| # These run locally in this container via `transformers`, instead of calling | |
| # Hugging Face's shared serverless Inference API (which no longer serves the | |
| # gender/race models β only age currently has an attached provider). | |
| def load_vit_pipeline(model_name): | |
| """ | |
| Load explicitly as ViT rather than relying on AutoImageProcessor/AutoModel. | |
| These models were fine-tuned ~2 years ago using the standard HF ViT | |
| image-classification recipe; newer `transformers` versions sometimes | |
| fail to auto-detect older model configs, so we bypass Auto* entirely. | |
| """ | |
| processor = ViTImageProcessor.from_pretrained(model_name) | |
| model = ViTForImageClassification.from_pretrained(model_name) | |
| return pipeline("image-classification", model=model, image_processor=processor) | |
| print("Loading race/age/gender models β this can take a bit on first boot...") | |
| race_pipe = load_vit_pipeline("crangana/trained-race") | |
| age_pipe = load_vit_pipeline("crangana/trained-age") | |
| gender_pipe = load_vit_pipeline("crangana/trained-gender") | |
| print("Models loaded.") | |
| def crop_face(image_path): | |
| """Detect and crop the face using Roboflow's hosted face detector.""" | |
| if not ROBOFLOW_API_KEY: | |
| raise ValueError( | |
| "ROBOFLOW_API_KEY is not set. Add it under this Space's " | |
| "Settings β Repository secrets." | |
| ) | |
| with open(image_path, "rb") as image_file: | |
| image_base64 = base64.b64encode(image_file.read()).decode("utf-8") | |
| url = "https://detect.roboflow.com/face-det-npvna/1" | |
| params = {"api_key": ROBOFLOW_API_KEY} | |
| headers = {"Content-Type": "application/x-www-form-urlencoded"} | |
| response = requests.post(url, params=params, data=image_base64, headers=headers) | |
| try: | |
| crop_data = response.json() | |
| except ValueError: | |
| raise ValueError( | |
| f"Roboflow returned a non-JSON response (status {response.status_code}): " | |
| f"{response.text[:300]}" | |
| ) | |
| print("Roboflow response:", crop_data) # visible in the Space's Logs tab | |
| if "error" in crop_data: | |
| raise ValueError(f"Roboflow error: {crop_data['error']}") | |
| if response.status_code != 200: | |
| raise ValueError(f"Roboflow returned status {response.status_code}: {crop_data}") | |
| if not crop_data.get("predictions"): | |
| raise ValueError( | |
| "Roboflow ran successfully but found no face in this image. " | |
| "Try a clearer, front-facing photo." | |
| ) | |
| pred = crop_data["predictions"][0] | |
| width, height = pred["width"], pred["height"] | |
| x = pred["x"] - width / 2 | |
| y = pred["y"] - height / 2 | |
| image = Image.open(image_path) | |
| cropped = image.crop((x, y, x + width, y + height)) | |
| # Force an exact 224x224 square before it reaches the models β the | |
| # Roboflow bounding box isn't guaranteed to be square, and this | |
| # particular model's built-in preprocessing doesn't fully correct | |
| # for that on its own (was producing e.g. 227x224 instead of 224x224, | |
| # which the model's fixed patch grid can't accept). | |
| cropped = cropped.convert("RGB").resize((224, 224), Image.LANCZOS) | |
| cropped_path = "/tmp/cropped_face.jpg" | |
| cropped.save(cropped_path) | |
| return cropped_path | |
| def get_top(results): | |
| """Pick the highest-confidence label from a list of {label, score} dicts.""" | |
| best = max(results, key=lambda r: r["score"]) | |
| return best["label"], best["score"] | |
| def classify(image): | |
| if image is None: | |
| return "No image provided.", "", "" | |
| tmp_path = "/tmp/input_face.jpg" | |
| image.convert("RGB").save(tmp_path) | |
| try: | |
| cropped_path = crop_face(tmp_path) | |
| except Exception as e: | |
| return f"Face detection failed: {e}", "β", "β" | |
| try: | |
| age_label, age_score = get_top(age_pipe(cropped_path)) | |
| gender_label, gender_score = get_top(gender_pipe(cropped_path)) | |
| race_label, race_score = get_top(race_pipe(cropped_path)) | |
| except Exception as e: | |
| return f"Classification failed: {e}", "β", "β" | |
| return ( | |
| f"{age_label} ({age_score:.0%})", | |
| f"{gender_label} ({gender_score:.0%})", | |
| f"{race_label} ({race_score:.0%})", | |
| ) | |
| demo = gr.Interface( | |
| fn=classify, | |
| inputs=gr.Image(type="pil", label="Upload a face photo"), | |
| outputs=[ | |
| gr.Textbox(label="Age"), | |
| gr.Textbox(label="Gender"), | |
| gr.Textbox(label="Ethnicity"), | |
| ], | |
| title="Face Attribute Classifier", | |
| description="Predicts age, gender, and ethnicity from a face photo.", | |
| api_name="classify", # names the programmatic API endpoint /gradio_api/call/classify | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |