dpv007 commited on
Commit
99d1f26
·
verified ·
1 Parent(s): 7da846e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +9 -459
app.py CHANGED
@@ -1,459 +1,9 @@
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)
 
1
+ fastapi==0.95.2
2
+ uvicorn[standard]==0.22.0
3
+ numpy==1.26.0
4
+ opencv-python-headless==4.8.0.74
5
+ Pillow==10.0.1
6
+ facenet-pytorch==3.4.0 # optional, keep if you use facenet-pytorch
7
+ torch==2.2.0 # optional, keep if you use facenet-pytorch
8
+ python-multipart==0.0.6
9
+ aiofiles==23.1.0