Giddycrypt commited on
Commit
e5f3fc8
·
verified ·
1 Parent(s): 83196e2

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -5
  2. app.py +83 -50
  3. requirements.txt +4 -2
Dockerfile CHANGED
@@ -2,21 +2,29 @@ FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
5
- # Install system dependencies required for OpenCV
6
  RUN apt-get update && apt-get install -y \
7
  libgl1 \
8
  libglib2.0-0 \
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- # Install python dependencies
 
 
 
 
 
 
12
  COPY requirements.txt .
13
  RUN pip install --no-cache-dir -r requirements.txt
14
 
15
- # Copy app code
16
  COPY app.py .
17
 
18
- # Hugging Face Spaces expose port 7860
19
  EXPOSE 7860
20
 
21
- # Run the API
 
22
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
2
 
3
  WORKDIR /app
4
 
5
+ # System dependencies for OpenCV
6
  RUN apt-get update && apt-get install -y \
7
  libgl1 \
8
  libglib2.0-0 \
9
+ git \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
+ # Clone the MiVOLO repository
13
+ RUN git clone https://github.com/WildChlamydia/MiVOLO.git /app/mivolo_repo
14
+
15
+ # Install PyTorch CPU-only (much lighter than full GPU build)
16
+ RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
17
+
18
+ # Install remaining API dependencies
19
  COPY requirements.txt .
20
  RUN pip install --no-cache-dir -r requirements.txt
21
 
22
+ # Copy our API entrypoint
23
  COPY app.py .
24
 
25
+ # Hugging Face Spaces use port 7860
26
  EXPOSE 7860
27
 
28
+ # NOTE: We do NOT pre-load models here to avoid OOM during build.
29
+ # Models download on the first API request at runtime (16GB RAM available).
30
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,15 +1,21 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from deepface import DeepFace
4
  import cv2
5
  import numpy as np
6
  import tempfile
7
- import os
8
  import logging
 
 
 
 
9
 
10
- logging.getLogger('tf_keras').setLevel(logging.ERROR)
 
11
 
12
- app = FastAPI(title="DeepFace Age Detection API Hybrid Model")
 
 
 
13
 
14
  app.add_middleware(
15
  CORSMiddleware,
@@ -18,84 +24,111 @@ app.add_middleware(
18
  allow_headers=["*"],
19
  )
20
 
21
- # --- HYBRID LOGIC ---
22
- # DeepFace/VGG-Face is unreliable for children under 15.
23
- # We remap those raw predictions to the nearest Adience bracket.
24
- CHILD_BRACKETS = [
25
- (0, 2, "0 - 2 yrs", 0.82),
26
- (3, 6, "4 - 6 yrs", 0.80),
27
- (7, 14, "8 - 12 yrs", 0.72),
28
- ]
 
 
 
 
 
 
29
 
30
- def apply_hybrid_age_logic(deepface_age: int):
31
- if deepface_age < 15:
32
- for low, high, label, conf in CHILD_BRACKETS:
33
- if low <= deepface_age <= high:
34
- return label, conf, True
35
- return "8 - 12 yrs", 0.68, True
36
 
37
- # For adults (15+), trust DeepFace's exact regression number
38
- corrected_age = deepface_age
39
- if 15 <= deepface_age <= 25:
40
- corrected_age = deepface_age + 1
41
- elif deepface_age > 50:
42
- corrected_age = deepface_age - 1
43
 
44
- return str(corrected_age), 0.92, False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
 
47
  @app.get("/")
48
- def read_root():
49
- return {"status": "Hybrid DeepFace API is running!", "model": "VGG-Face + Adience Hybrid"}
 
 
 
50
 
51
 
52
  @app.post("/predict")
53
  async def predict_age_gender(file: UploadFile = File(...)):
54
  tmp_path = None
55
  try:
 
56
  contents = await file.read()
57
  nparr = np.frombuffer(contents, np.uint8)
58
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
59
 
60
  if img is None:
61
- raise HTTPException(status_code=400, detail="Invalid image file uploaded.")
62
 
63
- # Write to a temp file so DeepFace receives a stable file path
64
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
65
  tmp_path = tmp.name
66
  cv2.imwrite(tmp_path, img)
67
 
68
- results = DeepFace.analyze(
69
- img_path=tmp_path,
70
- actions=['age', 'gender'],
71
- enforce_detection=False,
72
- detector_backend='opencv',
73
- silent=True
74
- )
 
 
75
 
76
- if isinstance(results, list):
77
- result = results[0]
78
- else:
79
- result = results
80
 
81
- raw_age = result.get("age", 0)
82
- gender = result.get("dominant_gender", "Unknown")
83
 
84
- display_age, confidence, is_child = apply_hybrid_age_logic(raw_age)
85
 
86
  return {
87
  "success": True,
88
- "age": display_age,
89
  "gender": gender,
90
- "confidence": confidence,
91
- "model_used": "Adience Bracket (Child)" if is_child else "DeepFace VGG-Face (Adult)",
92
- "raw_age": raw_age
93
  }
94
 
 
 
95
  except Exception as e:
 
96
  raise HTTPException(status_code=500, detail=str(e))
97
 
98
  finally:
99
- # Always clean up the temp file
100
  if tmp_path and os.path.exists(tmp_path):
101
  os.remove(tmp_path)
 
1
+ import sys
2
+ import os
 
3
  import cv2
4
  import numpy as np
5
  import tempfile
 
6
  import logging
7
+ import argparse
8
+
9
+ from fastapi import FastAPI, File, UploadFile, HTTPException
10
+ from fastapi.middleware.cors import CORSMiddleware
11
 
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
 
15
+ # Add the cloned MiVOLO repo to Python path
16
+ sys.path.insert(0, '/app/mivolo_repo')
17
+
18
+ app = FastAPI(title="MiVOLO Age & Gender Detection API")
19
 
20
  app.add_middleware(
21
  CORSMiddleware,
 
24
  allow_headers=["*"],
25
  )
26
 
27
+ # Global predictor loaded lazily on first request to avoid OOM during build
28
+ predictor = None
29
+
30
+
31
+ def get_predictor():
32
+ """
33
+ Lazy-loads the MiVOLO predictor on the first request.
34
+ Downloads model weights from Hugging Face Hub automatically.
35
+ """
36
+ global predictor
37
+ if predictor is not None:
38
+ return predictor
39
+
40
+ logger.info("Loading MiVOLO predictor for the first time...")
41
 
42
+ from huggingface_hub import hf_hub_download
43
+ from mivolo.predictor import Predictor
 
 
 
 
44
 
45
+ # Download the YOLOv8 person+face detector weights
46
+ detector_weights = hf_hub_download(
47
+ repo_id="WildChlamydia/MiVOLO_D1",
48
+ filename="yolov8x_person_face.pt"
49
+ )
 
50
 
51
+ # Download the MiVOLO age+gender estimator weights
52
+ checkpoint = hf_hub_download(
53
+ repo_id="WildChlamydia/MiVOLO_D1",
54
+ filename="mivolo_d1_age_gender.pth.tar"
55
+ )
56
+
57
+ # Build MiVOLO config
58
+ config = argparse.Namespace(
59
+ detector_weights=detector_weights,
60
+ checkpoint=checkpoint,
61
+ device="cpu",
62
+ with_persons=True, # Use full-body context for better accuracy
63
+ disable_faces=False, # Also use face features
64
+ draw=False
65
+ )
66
+
67
+ predictor = Predictor(config, verbose=False)
68
+ logger.info("MiVOLO predictor loaded successfully.")
69
+ return predictor
70
 
71
 
72
  @app.get("/")
73
+ def health_check():
74
+ return {
75
+ "status": "MiVOLO API is running!",
76
+ "model": "MiVOLO D1 — State-of-the-Art Age & Gender Estimation"
77
+ }
78
 
79
 
80
  @app.post("/predict")
81
  async def predict_age_gender(file: UploadFile = File(...)):
82
  tmp_path = None
83
  try:
84
+ # Read and decode image
85
  contents = await file.read()
86
  nparr = np.frombuffer(contents, np.uint8)
87
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
88
 
89
  if img is None:
90
+ raise HTTPException(status_code=400, detail="Invalid or unreadable image file.")
91
 
92
+ # Write to temp file so MiVOLO can read it from disk
93
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
94
  tmp_path = tmp.name
95
  cv2.imwrite(tmp_path, img)
96
 
97
+ # Run MiVOLO prediction
98
+ pred = get_predictor()
99
+ detected_objects, _ = pred.recognize(tmp_path)
100
+
101
+ if detected_objects is None or not detected_objects.ages:
102
+ raise HTTPException(
103
+ status_code=422,
104
+ detail="No face detected. Please use a clear, well-lit photo."
105
+ )
106
 
107
+ # Take the primary (highest-confidence) detection
108
+ age = round(float(detected_objects.ages[0]))
109
+ gender_raw = detected_objects.genders[0] # "male" or "female"
110
+ gender_score = float(detected_objects.gender_scores[0])
111
 
112
+ # Format gender to match dashboard expectations
113
+ gender = "Man" if gender_raw == "male" else "Woman"
114
 
115
+ logger.info(f"MiVOLO Result — Age: {age}, Gender: {gender} ({gender_score:.2f})")
116
 
117
  return {
118
  "success": True,
119
+ "age": age,
120
  "gender": gender,
121
+ "confidence": round(gender_score, 2),
122
+ "model_used": "MiVOLO D1"
 
123
  }
124
 
125
+ except HTTPException:
126
+ raise
127
  except Exception as e:
128
+ logger.error(f"Prediction error: {str(e)}")
129
  raise HTTPException(status_code=500, detail=str(e))
130
 
131
  finally:
132
+ # Always clean up temp file
133
  if tmp_path and os.path.exists(tmp_path):
134
  os.remove(tmp_path)
requirements.txt CHANGED
@@ -1,6 +1,8 @@
1
  fastapi==0.104.1
2
  uvicorn==0.24.0
3
  python-multipart==0.0.6
4
- deepface==0.0.79
5
- tensorflow==2.15.0
6
  opencv-python-headless==4.8.1.78
 
 
 
 
 
1
  fastapi==0.104.1
2
  uvicorn==0.24.0
3
  python-multipart==0.0.6
 
 
4
  opencv-python-headless==4.8.1.78
5
+ timm>=0.9.2
6
+ ultralytics>=8.0.0
7
+ huggingface_hub>=0.19.0
8
+ numpy<2.0