""" Simple emotion classification with a Deit-Base model hosted on the Hugging Face Hub. Install: pip install torch timm safetensors huggingface_hub pillow Quick use: from emotion_classifier import EmotionClassifier clf = EmotionClassifier() result = clf.predict("path/to/image.jpg") print(result) # {'prediction': 'Neutral', 'confidence': 0.95, 'Optimistic': 0.01, # 'Pessimistic': 0.01, 'Hostile': 0.02, 'Neutral': 0.95} Command line use: python emotion_classifier.py path/to/image_or_folder python emotion_classifier.py path/to/folder --output results.csv """ import argparse from pathlib import Path import torch import torch.nn.functional as F import timm from PIL import Image from torchvision import transforms from safetensors.torch import load_file from huggingface_hub import hf_hub_download # --------------------------------------------------------------------------- # Fixed model settings (must match how the checkpoint was trained) # --------------------------------------------------------------------------- REPO_ID = "Simon14142/emotion-recognition-4class-deit" FILENAME = "DeiT-Base_checkpoint_final.safetensors" MODEL_NAME = "deit_base_patch16_224" IMG_SIZE = 224 EMOTION_NAMES = ["Optimistic", "Pessimistic", "Hostile", "Neutral"] IMAGENET_MEAN = [0.485, 0.456, 0.406] IMAGENET_STD = [0.229, 0.224, 0.225] IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"} class EmotionClassifier: """Loads the Deit-Base emotion model straight from the Hugging Face Hub.""" def __init__(self, repo_id: str = REPO_ID, filename: str = FILENAME, device: str | None = None, precision: str = "fp32"): self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu")) checkpoint_path = hf_hub_download(repo_id=repo_id, filename=filename) self.model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=len(EMOTION_NAMES)) state_dict = load_file(checkpoint_path, device="cpu") missing, unexpected = self.model.load_state_dict(state_dict, strict=True) if missing: print(f"Warning - missing keys: {missing}") if unexpected: print(f"Warning - unexpected keys: {unexpected}") self.precision = precision if precision == "bf16": if self.device.type == "cuda" and torch.cuda.is_bf16_supported(): self.model = self.model.to(torch.bfloat16) else: print("BF16 not supported here, falling back to FP32.") self.precision = "fp32" self.model = self.model.to(self.device).eval() self.transform = transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), ]) @torch.no_grad() def predict(self, image, threshold: float = 0.4) -> dict: """image: a file path (str/Path) or a PIL.Image. Returns a result dict.""" if not isinstance(image, Image.Image): image = Image.open(image).convert("RGB") tensor = self.transform(image).unsqueeze(0).to(self.device) if self.precision == "bf16": tensor = tensor.to(torch.bfloat16) logits = self.model(tensor) probs = F.softmax(logits.float(), dim=1).squeeze(0).cpu() pred_idx = int(probs.argmax()) conf = float(probs[pred_idx]) label = EMOTION_NAMES[pred_idx] if conf >= threshold else "Unknown" return { "prediction": label, "confidence": round(conf, 4), **{name: round(float(probs[i]), 4) for i, name in enumerate(EMOTION_NAMES)}, } def _collect_image_paths(input_path: str) -> list[str]: p = Path(input_path) if p.is_file(): return [str(p)] if p.is_dir(): paths = sorted(str(f) for f in p.iterdir() if f.suffix.lower() in IMAGE_EXTENSIONS) if not paths: raise FileNotFoundError(f"No images found in: {input_path}") return paths raise FileNotFoundError(f"Path does not exist: {input_path}") def main(): parser = argparse.ArgumentParser(description="Emotion classification with Deit-Base") parser.add_argument("input", help="Path to an image file or a folder of images") parser.add_argument("--threshold", type=float, default=0.4) parser.add_argument("--output", help="Optional CSV path to save results") args = parser.parse_args() clf = EmotionClassifier() paths = _collect_image_paths(args.input) rows = [] for path in paths: result = clf.predict(path, threshold=args.threshold) result["image_path"] = path rows.append(result) print(f"{Path(path).name:<40} -> {result['prediction']:<12} ({result['confidence']:.3f})") if args.output: import csv with open(args.output, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["image_path", "prediction", "confidence", *EMOTION_NAMES]) writer.writeheader() writer.writerows(rows) print(f"\nSaved {len(rows)} results to {args.output}") if __name__ == "__main__": main()