dpv007 commited on
Commit
16905b5
·
verified ·
1 Parent(s): ed7595b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +459 -0
app.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Final `app.py` — ready to run (designed for HF Spaces / Docker).
3
+ - Uses facenet-pytorch's MTCNN (preferred) but will attempt to fall back to mtcnn if needed.
4
+ - Binds to port 7860 in the __main__ block for local testing (Spaces expects port 7860).
5
+ - Writes only to /tmp for any temporary files.
6
+ - Keeps BackgroundTasks short and resilient.
7
+
8
+ Install dependencies (recommended):
9
+ fastapi uvicorn[standard] pillow numpy opencv-python-headless facenet-pytorch torch
10
+
11
+ If you must use the older `mtcnn` (tensorflow-backed) package, install it explicitly and
12
+ the fallback will try to use it.
13
+ """
14
+
15
+ import io
16
+ import uuid
17
+ import asyncio
18
+ from typing import Dict, Any, Optional
19
+ from datetime import datetime
20
+ from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException
21
+ from fastapi.middleware.cors import CORSMiddleware
22
+ from PIL import Image
23
+ import numpy as np
24
+ import os
25
+ import traceback
26
+
27
+ # Try facenet-pytorch MTCNN first (recommended)
28
+ try:
29
+ from facenet_pytorch import MTCNN as FacenetMTCNN
30
+ _MTCNN_IMPL = "facenet_pytorch"
31
+ except Exception:
32
+ FacenetMTCNN = None
33
+ _MTCNN_IMPL = None
34
+
35
+ # Fallback to the classic mtcnn package if facenet-pytorch is not available
36
+ if _MTCNN_IMPL is None:
37
+ try:
38
+ from mtcnn import MTCNN as ClassicMTCNN
39
+ _MTCNN_IMPL = "mtcnn"
40
+ except Exception:
41
+ ClassicMTCNN = None
42
+
43
+ # Initialize MTCNN detector depending on availability
44
+ def create_mtcnn():
45
+ if _MTCNN_IMPL == "facenet_pytorch" and FacenetMTCNN is not None:
46
+ # keep device CPU by default; Spaces typically doesn't provide GPUs
47
+ return FacenetMTCNN(keep_all=False, device="cpu")
48
+ elif _MTCNN_IMPL == "mtcnn" and ClassicMTCNN is not None:
49
+ return ClassicMTCNN()
50
+ else:
51
+ return None
52
+
53
+ mtcnn = create_mtcnn()
54
+
55
+ app = FastAPI(title="Elderly HealthWatch AI Backend")
56
+
57
+ # CORS for demo
58
+ app.add_middleware(
59
+ CORSMiddleware,
60
+ allow_origins=["*"],
61
+ allow_credentials=True,
62
+ allow_methods=["*"],
63
+ allow_headers=["*"],
64
+ )
65
+
66
+ # In-memory store for demo (replace with persistent DB in prod)
67
+ screenings_db: Dict[str, Dict[str, Any]] = {}
68
+
69
+ # Utility: safe image load from UploadFile bytes -> PIL.Image
70
+ def load_image_from_bytes(bytes_data: bytes) -> Image.Image:
71
+ return Image.open(io.BytesIO(bytes_data)).convert("RGB")
72
+
73
+ # Heuristic eye openness: uses detection probability/confidence and face size
74
+ def estimate_eye_openness_from_detection(detection_result: Dict[str, Any]) -> float:
75
+ """
76
+ Returns a float in [0.0, 1.0] estimating eye openness.
77
+ For facenet-pytorch, detection_result may be (box, prob, landmarks) depending on API.
78
+ For classic mtcnn, the detect_faces() dict is used.
79
+ We keep a conservative, simple heuristic: rely on detection probability and landmark presence.
80
+ """
81
+ try:
82
+ # facenet-pytorch path: we might get prob and landmarks separately upstream,
83
+ # but this helper expects a uniform dict-like structure if possible.
84
+ if isinstance(detection_result, dict) and "confidence" in detection_result:
85
+ conf = float(detection_result.get("confidence", 0.0))
86
+ elif isinstance(detection_result, (list, tuple)) and len(detection_result) >= 2:
87
+ # some APIs return (boxes, probs) or similar structures — guard against that upstream
88
+ conf = float(detection_result[1]) if detection_result[1] is not None else 0.0
89
+ else:
90
+ conf = 0.0
91
+
92
+ # Basic scaling: make confidence map into [0,1], nudge slightly
93
+ openness = min(max((conf * 1.15), 0.0), 1.0)
94
+ return openness
95
+ except Exception:
96
+ return 0.0
97
+
98
+ @app.get("/")
99
+ async def read_root():
100
+ return {"message": "Elderly HealthWatch AI Backend"}
101
+
102
+ @app.get("/health")
103
+ async def health_check():
104
+ return {"status": "healthy", "mtcnn_impl": _MTCNN_IMPL}
105
+
106
+ @app.post("/api/v1/validate-eye-photo")
107
+ async def validate_eye_photo(image: UploadFile = File(...)):
108
+ """
109
+ Validate an eye photo: detects a face and returns an eye_openness_score and landmarks.
110
+ Uses MTCNN implementation available in the container.
111
+ """
112
+ if mtcnn is None:
113
+ raise HTTPException(status_code=500, detail="No MTCNN implementation available in this environment.")
114
+
115
+ try:
116
+ content = await image.read()
117
+ if not content:
118
+ raise HTTPException(status_code=400, detail="Empty file uploaded.")
119
+ pil_img = load_image_from_bytes(content)
120
+
121
+ # Convert to numpy RGB for some detectors that expect np arrays
122
+ img_arr = np.asarray(pil_img)
123
+
124
+ # Two possible MTCNN APIs:
125
+ # - facenet_pytorch.MTCNN: has detect and forward methods (detect returns boxes, probs, landmarks)
126
+ # - mtcnn (older): has detect_faces(image) returning list of dicts with 'confidence' and 'keypoints'
127
+ if _MTCNN_IMPL == "facenet_pytorch":
128
+ try:
129
+ # facenet_pytorch.MTCNN.detect returns (boxes, probs, landmarks)
130
+ boxes, probs, landmarks = mtcnn.detect(pil_img, landmarks=True)
131
+ if boxes is None or len(boxes) == 0:
132
+ return {
133
+ "valid": False,
134
+ "face_detected": False,
135
+ "eye_openness_score": 0.0,
136
+ "message_english": "No face detected. Please ensure your face is clearly visible in the frame.",
137
+ "message_hindi": "कोई चेहरा नहीं मिला। कृपया सुनिश्चित करें कि आपका चेहरा फ्रेम में स्पष्ट रूप से दिखाई दे रहा है।"
138
+ }
139
+
140
+ # Use first detection
141
+ prob = float(probs[0]) if probs is not None else 0.0
142
+ lm = landmarks[0] if landmarks is not None else None
143
+ if lm is not None and len(lm) >= 2:
144
+ # facenet landmarks are [[left_eye_x, left_eye_y], [right_eye_x, right_eye_y], ...]
145
+ left_eye = {"x": float(lm[0][0]), "y": float(lm[0][1])}
146
+ right_eye = {"x": float(lm[1][0]), "y": float(lm[1][1])}
147
+ else:
148
+ left_eye = right_eye = None
149
+
150
+ # Estimate openness
151
+ eye_openness_score = estimate_eye_openness_from_detection((None, prob))
152
+ is_valid = eye_openness_score >= 0.3
153
+
154
+ return {
155
+ "valid": bool(is_valid),
156
+ "face_detected": True,
157
+ "eye_openness_score": round(eye_openness_score, 2),
158
+ "message_english": "Photo looks good! Eyes are properly open." if is_valid else "Eyes appear to be closed or partially closed. Please open your eyes wide and try again.",
159
+ "message_hindi": "फोटो अच्छी है! आंखें ठीक से खुली हैं।" if is_valid else "आंखें बंद या आंशिक रूप से बंद दिखाई दे रही हैं। कृपया अपनी आंखें चौड़ी खोलें और पुनः प्रयास करें।",
160
+ "eye_landmarks": {
161
+ "left_eye": left_eye,
162
+ "right_eye": right_eye
163
+ }
164
+ }
165
+ except Exception as e:
166
+ # Facenet path failed unexpectedly; fall through to generic error handling
167
+ raise
168
+
169
+ elif _MTCNN_IMPL == "mtcnn":
170
+ # classic mtcnn: detect_faces returns list of dicts
171
+ try:
172
+ detections = mtcnn.detect_faces(img_arr)
173
+ except Exception:
174
+ # some mtcnn implementations accept PIL images
175
+ detections = mtcnn.detect_faces(pil_img)
176
+
177
+ if not detections:
178
+ return {
179
+ "valid": False,
180
+ "face_detected": False,
181
+ "eye_openness_score": 0.0,
182
+ "message_english": "No face detected. Please ensure your face is clearly visible in the frame.",
183
+ "message_hindi": "कोई चेहरा नहीं मिला। कृपया सुनिश्चित करें कि आपका चेहरा फ्रेम में स्पष्ट रूप से दिखाई दे रहा है।"
184
+ }
185
+
186
+ face = detections[0]
187
+ keypoints = face.get("keypoints", {})
188
+ left_eye = keypoints.get("left_eye")
189
+ right_eye = keypoints.get("right_eye")
190
+ confidence = float(face.get("confidence", 0.0))
191
+
192
+ eye_openness_score = estimate_eye_openness_from_detection({"confidence": confidence})
193
+ is_valid = eye_openness_score >= 0.3
194
+
195
+ return {
196
+ "valid": bool(is_valid),
197
+ "face_detected": True,
198
+ "eye_openness_score": round(eye_openness_score, 2),
199
+ "message_english": "Photo looks good! Eyes are properly open." if is_valid else "Eyes appear to be closed or partially closed. Please open your eyes wide and try again.",
200
+ "message_hindi": "फोटो अच्छी है! आंखें ठीक से खुली हैं।" if is_valid else "आंखें बंद या आंशिक रूप से बंद दिखाई दे रही हैं। कृपया अपनी आंखें चौड़ी खोलें और पुनः प्रयास करें।",
201
+ "eye_landmarks": {
202
+ "left_eye": left_eye,
203
+ "right_eye": right_eye
204
+ }
205
+ }
206
+ else:
207
+ raise HTTPException(status_code=500, detail="No face detector available in this deployment.")
208
+
209
+ except HTTPException:
210
+ raise
211
+ except Exception as e:
212
+ # Log traceback to container logs for debugging
213
+ traceback.print_exc()
214
+ return {
215
+ "valid": False,
216
+ "face_detected": False,
217
+ "eye_openness_score": 0.0,
218
+ "message_english": "Error processing image. Please try again.",
219
+ "message_hindi": "छवि प्रोसेस करने में त्रुटि। कृपया पुनः प्रयास करें।",
220
+ "error": str(e)
221
+ }
222
+
223
+
224
+ @app.post("/api/v1/upload")
225
+ async def upload_images(
226
+ background_tasks: BackgroundTasks,
227
+ face_image: UploadFile = File(...),
228
+ eye_image: UploadFile = File(...)
229
+ ):
230
+ """
231
+ Accept face and eye images and enqueue background processing.
232
+ Stores minimal metadata in an in-memory dict. Use external storage in production.
233
+ """
234
+ try:
235
+ screening_id = str(uuid.uuid4())
236
+ now = datetime.utcnow().isoformat() + "Z"
237
+
238
+ # In production you would persist the bytes to S3 or to a DB.
239
+ # For demo we store temp bytes in /tmp/<screening_id> (ephemeral)
240
+ tmp_dir = "/tmp/elderly_healthwatch"
241
+ os.makedirs(tmp_dir, exist_ok=True)
242
+
243
+ face_path = os.path.join(tmp_dir, f"{screening_id}_face.jpg")
244
+ eye_path = os.path.join(tmp_dir, f"{screening_id}_eye.jpg")
245
+
246
+ # Save raw bytes quickly
247
+ face_bytes = await face_image.read()
248
+ eye_bytes = await eye_image.read()
249
+ with open(face_path, "wb") as f:
250
+ f.write(face_bytes)
251
+ with open(eye_path, "wb") as f:
252
+ f.write(eye_bytes)
253
+
254
+ screenings_db[screening_id] = {
255
+ "id": screening_id,
256
+ "timestamp": now,
257
+ "face_image_path": face_path,
258
+ "eye_image_path": eye_path,
259
+ "status": "queued",
260
+ "quality_metrics": {},
261
+ "ai_results": {},
262
+ "disease_predictions": [],
263
+ "recommendations": {}
264
+ }
265
+
266
+ # Start background processing (short tasks preferred)
267
+ background_tasks.add_task(process_screening, screening_id)
268
+
269
+ return {"screening_id": screening_id}
270
+ except Exception as e:
271
+ traceback.print_exc()
272
+ raise HTTPException(status_code=500, detail=f"Failed to upload images: {e}")
273
+
274
+
275
+ @app.post("/api/v1/analyze/{screening_id}")
276
+ async def analyze_screening(screening_id: str, background_tasks: BackgroundTasks):
277
+ """Trigger analysis for an existing screening (re-run)."""
278
+ if screening_id not in screenings_db:
279
+ raise HTTPException(status_code=404, detail="Screening not found")
280
+ if screenings_db[screening_id].get("status") == "processing":
281
+ return {"message": "Already processing"}
282
+ screenings_db[screening_id]["status"] = "queued"
283
+ background_tasks.add_task(process_screening, screening_id)
284
+ return {"message": "Analysis enqueued"}
285
+
286
+
287
+ @app.get("/api/v1/status/{screening_id}")
288
+ async def get_status(screening_id: str):
289
+ if screening_id not in screenings_db:
290
+ raise HTTPException(status_code=404, detail="Screening not found")
291
+ status = screenings_db[screening_id].get("status", "unknown")
292
+ progress = 50 if status == "processing" else (100 if status == "completed" else 0)
293
+ return {
294
+ "screening_id": screening_id,
295
+ "status": status,
296
+ "progress": progress
297
+ }
298
+
299
+
300
+ @app.get("/api/v1/results/{screening_id}")
301
+ async def get_results(screening_id: str):
302
+ if screening_id not in screenings_db:
303
+ raise HTTPException(status_code=404, detail="Screening not found")
304
+ return screenings_db[screening_id]
305
+
306
+
307
+ @app.get("/api/v1/history/{user_id}")
308
+ async def get_history(user_id: str):
309
+ # This demo does not link screenings to users by default
310
+ history = [s for s in screenings_db.values() if s.get("user_id") == user_id]
311
+ return {"screenings": history}
312
+
313
+
314
+ async def process_screening(screening_id: str):
315
+ """
316
+ Background processing pipeline:
317
+ - quick image checks using MTCNN
318
+ - placeholder VLM / medical LM stages simulated with sleeps
319
+ - populates quality_metrics, ai_results, disease_predictions, recommendations
320
+ Keep tasks reasonably short to avoid container restarts killing long jobs.
321
+ """
322
+ try:
323
+ if screening_id not in screenings_db:
324
+ print(f"[process_screening] screening {screening_id} not found")
325
+ return
326
+
327
+ screenings_db[screening_id]["status"] = "processing"
328
+ print(f"[process_screening] Starting {screening_id}")
329
+
330
+ entry = screenings_db[screening_id]
331
+ face_path = entry.get("face_image_path")
332
+ eye_path = entry.get("eye_image_path")
333
+
334
+ # Basic file checks
335
+ if not (face_path and os.path.exists(face_path)):
336
+ raise RuntimeError("Face image missing")
337
+ if not (eye_path and os.path.exists(eye_path)):
338
+ raise RuntimeError("Eye image missing")
339
+
340
+ # Load images
341
+ face_img = Image.open(face_path).convert("RGB")
342
+ eye_img = Image.open(eye_path).convert("RGB")
343
+
344
+ # Stage 1: face detection + quality metrics (fast)
345
+ face_detected = False
346
+ face_confidence = 0.0
347
+ left_eye_coord = right_eye_coord = None
348
+
349
+ if mtcnn is not None:
350
+ try:
351
+ if _MTCNN_IMPL == "facenet_pytorch":
352
+ boxes, probs, landmarks = mtcnn.detect(face_img, landmarks=True)
353
+ if boxes is not None and len(boxes) > 0:
354
+ face_detected = True
355
+ face_confidence = float(probs[0]) if probs is not None else 0.0
356
+ if landmarks is not None:
357
+ lm = landmarks[0]
358
+ if len(lm) >= 2:
359
+ left_eye_coord = {"x": float(lm[0][0]), "y": float(lm[0][1])}
360
+ right_eye_coord = {"x": float(lm[1][0]), "y": float(lm[1][1])}
361
+ else:
362
+ # classic mtcnn
363
+ arr = np.asarray(face_img)
364
+ detections = mtcnn.detect_faces(arr)
365
+ if detections:
366
+ face_detected = True
367
+ face_confidence = float(detections[0].get("confidence", 0.0))
368
+ k = detections[0].get("keypoints", {})
369
+ left_eye_coord = k.get("left_eye")
370
+ right_eye_coord = k.get("right_eye")
371
+ except Exception:
372
+ traceback.print_exc()
373
+
374
+ # Simple quality metrics (placeholders but useful)
375
+ face_quality_score = 0.85 if face_detected and face_confidence > 0.6 else 0.45
376
+ quality_metrics = {
377
+ "face_detected": face_detected,
378
+ "face_confidence": round(face_confidence, 3),
379
+ "face_quality_score": round(face_quality_score, 2),
380
+ "eye_coords": {"left_eye": left_eye_coord, "right_eye": right_eye_coord},
381
+ "face_brightness": int(np.mean(np.asarray(face_img.convert("L")))),
382
+ "face_blur_estimate": int(np.var(np.asarray(face_img.convert("L")))) # crude proxy
383
+ }
384
+ screenings_db[screening_id]["quality_metrics"] = quality_metrics
385
+
386
+ # Stage 2/3: placeholder Visual and Medical model steps (simulate with sleeps)
387
+ await asyncio.sleep(1) # simulate feature extraction
388
+ vlm_face_desc = "Patient appears to have normal facial tone; no severe jaundice visible."
389
+ vlm_eye_desc = "Sclera shows mild yellowing." # placeholder
390
+
391
+ await asyncio.sleep(1) # simulate medical LM analysis
392
+ medical_insights = {
393
+ "hemoglobin_estimate": 11.2,
394
+ "bilirubin_estimate": 1.8,
395
+ "anemia_indicators": ["pale skin"],
396
+ "jaundice_indicators": ["mild scleral yellowing"],
397
+ "confidence": 0.82
398
+ }
399
+
400
+ # Stage 4: disease inference and recommendations
401
+ hem = medical_insights["hemoglobin_estimate"]
402
+ bil = medical_insights["bilirubin_estimate"]
403
+
404
+ ai_results = {
405
+ "hemoglobin_g_dl": hem,
406
+ "anemia_status": "Mild Anemia" if hem < 12 else "Normal",
407
+ "anemia_confidence": medical_insights["confidence"],
408
+ "bilirubin_mg_dl": bil,
409
+ "jaundice_status": "Normal" if bil < 2.5 else "Elevated",
410
+ "jaundice_confidence": medical_insights["confidence"],
411
+ "vlm_face_description": vlm_face_desc,
412
+ "vlm_eye_description": vlm_eye_desc,
413
+ "medical_insights": medical_insights,
414
+ "processing_time_ms": 1200
415
+ }
416
+ screenings_db[screening_id]["ai_results"] = ai_results
417
+
418
+ disease_predictions = [
419
+ {
420
+ "condition": "Iron Deficiency Anemia",
421
+ "risk_level": "Medium" if hem < 12 else "Low",
422
+ "probability": 0.76 if hem < 12 else 0.23,
423
+ "confidence": medical_insights["confidence"]
424
+ },
425
+ {
426
+ "condition": "Jaundice",
427
+ "risk_level": "Low" if bil < 2.5 else "Medium",
428
+ "probability": 0.23 if bil < 2.5 else 0.45,
429
+ "confidence": medical_insights["confidence"]
430
+ }
431
+ ]
432
+
433
+ recommendations = {
434
+ "action_needed": "consult" if hem < 12 else "monitor",
435
+ "message_english": f"Your hemoglobin is {hem} g/dL. Please consult a doctor within 2 weeks for blood tests.",
436
+ "message_hindi": f"आपका हीमोग्लोबिन {hem} g/dL है। कृपया 2 सप्ताह में डॉक्टर से परामर्श करें।"
437
+ }
438
+
439
+ screenings_db[screening_id].update({
440
+ "status": "completed",
441
+ "disease_predictions": disease_predictions,
442
+ "recommendations": recommendations
443
+ })
444
+
445
+ print(f"[process_screening] Completed {screening_id}")
446
+
447
+ except Exception as e:
448
+ traceback.print_exc()
449
+ if screening_id in screenings_db:
450
+ screenings_db[screening_id]["status"] = "failed"
451
+ screenings_db[screening_id]["error"] = str(e)
452
+ else:
453
+ print(f"[process_screening] Failed for unknown screening {screening_id}: {e}")
454
+
455
+
456
+ if __name__ == "__main__":
457
+ # Local debug run (Spaces will run uvicorn via Dockerfile/CMD)
458
+ import uvicorn
459
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)