medtechdev commited on
Commit
cb47ee6
·
verified ·
1 Parent(s): b384287

Upload 5 files

Browse files
Files changed (5) hide show
  1. DEPLOYMENT_GUIDE.md +132 -0
  2. app.py +595 -0
  3. best.pt +3 -0
  4. model.pkl +3 -0
  5. requirements.txt +7 -0
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 DEPLOYMENT GUIDE
2
+
3
+ ## Deploy to Hugging Face Spaces (FREE)
4
+
5
+ ### Step 1: Create Hugging Face Account
6
+ 1. Go to https://huggingface.co
7
+ 2. Sign up (free)
8
+ 3. Verify email
9
+
10
+ ### Step 2: Create New Space
11
+ 1. Click your profile → "New Space"
12
+ 2. Settings:
13
+ - **Name:** `anemia-screening` (or your choice)
14
+ - **License:** MIT
15
+ - **SDK:** Gradio
16
+ - **Hardware:** CPU Basic (free) — upgrade to GPU if slow
17
+ - **Visibility:** Public (or Private)
18
+ 3. Click "Create Space"
19
+
20
+ ### Step 3: Upload Files
21
+ Upload these files to your Space:
22
+
23
+ ```
24
+ your-space/
25
+ ├── app.py # Main application (provided)
26
+ ├── requirements.txt # Dependencies (provided)
27
+ ├── README.md # Space config (provided)
28
+ ├── model.pkl # YOUR trained model from Colab
29
+ └── best.pt # YOUR YOLO weights
30
+ ```
31
+
32
+ **Option A: Via Web Interface**
33
+ - Click "Files" tab → "Add file" → Upload each file
34
+
35
+ **Option B: Via Git**
36
+ ```bash
37
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/anemia-screening
38
+ cd anemia-screening
39
+ # Copy your files here
40
+ git add .
41
+ git commit -m "Initial deployment"
42
+ git push
43
+ ```
44
+
45
+ ### Step 4: Wait for Build
46
+ - Space will automatically install dependencies and start
47
+ - Takes 2-5 minutes
48
+ - Check "Logs" tab if errors occur
49
+
50
+ ### Step 5: Test
51
+ - Once running, test with a sample image
52
+ - Share the URL with others
53
+
54
+ ---
55
+
56
+ ## Files You Need from Colab
57
+
58
+ After Colab finishes, download from your GDrive `outputs/` folder:
59
+
60
+ | File | What it is |
61
+ |------|------------|
62
+ | `model.pkl` | Trained ML model |
63
+
64
+ You already have:
65
+ | File | What it is |
66
+ |------|------------|
67
+ | `best.pt` | YOLO segmentation weights |
68
+
69
+ ---
70
+
71
+ ## Common Issues
72
+
73
+ ### "Model not found"
74
+ - Ensure `model.pkl` and `best.pt` are in the root of your Space (not in a subfolder)
75
+
76
+ ### "Out of memory"
77
+ - Upgrade to GPU hardware (still free tier available)
78
+ - Or reduce image size in `app.py`
79
+
80
+ ### "Module not found"
81
+ - Check `requirements.txt` has all dependencies
82
+ - Restart the Space
83
+
84
+ ### YOLO errors
85
+ - Ensure `best.pt` is a segmentation model (not detection-only)
86
+ - Check ultralytics version compatibility
87
+
88
+ ---
89
+
90
+ ## Custom Domain (Optional)
91
+
92
+ Hugging Face provides URL like:
93
+ `https://huggingface.co/spaces/YOUR_USERNAME/anemia-screening`
94
+
95
+ For custom domain (e.g., `anemia.yourdomain.com`):
96
+ 1. Go to Space Settings
97
+ 2. Add custom domain
98
+ 3. Configure DNS CNAME record
99
+
100
+ ---
101
+
102
+ ## Data Collection
103
+
104
+ Collected data saves to `collected_data.csv` in the Space.
105
+
106
+ To download:
107
+ 1. Go to "Files" tab
108
+ 2. Click `collected_data.csv`
109
+ 3. Download
110
+
111
+ For production, connect to Google Sheets or a database instead.
112
+
113
+ ---
114
+
115
+ ## Updating the Model
116
+
117
+ When you retrain with more data:
118
+ 1. Download new `model.pkl` from Colab
119
+ 2. Upload to Space (replace old file)
120
+ 3. Space auto-restarts
121
+
122
+ ---
123
+
124
+ ## Cost
125
+
126
+ | Tier | Cost | Specs |
127
+ |------|------|-------|
128
+ | CPU Basic | FREE | 2 vCPU, 16GB RAM |
129
+ | CPU Upgrade | $0.03/hr | 8 vCPU, 32GB RAM |
130
+ | GPU T4 | $0.40/hr | Good for heavy YOLO usage |
131
+
132
+ For most use cases, **FREE tier is sufficient**.
app.py ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NON-INVASIVE ANEMIA SCREENING WEBAPP
3
+ =====================================
4
+ Estimates hemoglobin from palpebral conjunctiva image + standardized form inputs.
5
+
6
+ Deploy on Hugging Face Spaces (free).
7
+ Collects anonymized data for model improvement.
8
+
9
+ Author: JIPMER Anemia Screening Study
10
+ """
11
+
12
+ import gradio as gr
13
+ import numpy as np
14
+ import pandas as pd
15
+ import pickle
16
+ import cv2
17
+ import uuid
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ # ============================================================
22
+ # CONFIGURATION
23
+ # ============================================================
24
+ MODEL_PATH = "model.pkl"
25
+ YOLO_PATH = "best.pt"
26
+ DATA_LOG_PATH = "collected_data.csv" # Local fallback; use Google Sheets in production
27
+
28
+ # ============================================================
29
+ # LOAD MODELS (done once at startup)
30
+ # ============================================================
31
+ def load_models():
32
+ """Load ML model and YOLO segmentation model."""
33
+ global ml_model, feature_names, yolo_model
34
+
35
+ # Load ML model
36
+ with open(MODEL_PATH, 'rb') as f:
37
+ model_data = pickle.load(f)
38
+ ml_model = model_data['model']
39
+ feature_names = model_data['features']
40
+ print(f"✅ Loaded ML model: {model_data.get('name', 'Unknown')}")
41
+ print(f" Features: {len(feature_names)}")
42
+
43
+ # Load YOLO
44
+ from ultralytics import YOLO
45
+ yolo_model = YOLO(YOLO_PATH)
46
+ print(f"✅ Loaded YOLO model")
47
+
48
+ return True
49
+
50
+ # ============================================================
51
+ # IMAGE PROCESSING
52
+ # ============================================================
53
+ def calibrate_image(img):
54
+ """White balance calibration using grey card."""
55
+ try:
56
+ if img is None:
57
+ return None
58
+
59
+ # Convert to RGB if needed
60
+ if len(img.shape) == 2:
61
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
62
+ elif img.shape[2] == 4:
63
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_RGBA2RGB)
64
+ else:
65
+ img_rgb = img.copy()
66
+
67
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
68
+ _, thresh = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY)
69
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
70
+
71
+ if not contours:
72
+ _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
73
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
74
+
75
+ if not contours:
76
+ return img_rgb
77
+
78
+ c = max(contours, key=cv2.contourArea)
79
+ x, y, w, h = cv2.boundingRect(c)
80
+
81
+ if w * h < 1000:
82
+ return img_rgb
83
+
84
+ patch = img_rgb[y+h//4:y+3*h//4, x+w//4:x+3*w//4]
85
+ if patch.size == 0:
86
+ return img_rgb
87
+
88
+ avg = np.mean(patch, axis=(0, 1))
89
+ if any(v < 10 for v in avg):
90
+ return img_rgb
91
+
92
+ scale = np.array([128, 128, 128]) / avg
93
+ return np.clip(img_rgb * scale, 0, 255).astype(np.uint8)
94
+ except Exception as e:
95
+ print(f"Calibration error: {e}")
96
+ return img
97
+
98
+
99
+ def extract_image_features(img):
100
+ """Run YOLO segmentation and extract color features."""
101
+ try:
102
+ # Calibrate
103
+ img_cal = calibrate_image(img)
104
+ if img_cal is None:
105
+ return None
106
+
107
+ # YOLO segmentation
108
+ results = yolo_model(img_cal, verbose=False)
109
+
110
+ if len(results) > 0 and results[0].masks is not None:
111
+ masks = results[0].masks.data.cpu().numpy()
112
+ confs = results[0].boxes.conf.cpu().numpy()
113
+ if len(confs) > 0:
114
+ m = masks[np.argmax(confs)]
115
+ m = cv2.resize(m, (img_cal.shape[1], img_cal.shape[0]))
116
+ roi_mask = (m > 0.5).astype(np.uint8) * 255
117
+ else:
118
+ # Fallback to center crop
119
+ h, w = img_cal.shape[:2]
120
+ roi_mask = np.zeros((h, w), dtype=np.uint8)
121
+ roi_mask[h//4:3*h//4, w//4:3*w//4] = 255
122
+ else:
123
+ h, w = img_cal.shape[:2]
124
+ roi_mask = np.zeros((h, w), dtype=np.uint8)
125
+ roi_mask[h//4:3*h//4, w//4:3*w//4] = 255
126
+
127
+ # Extract color features from ROI
128
+ px = img_cal[roi_mask > 0]
129
+ if len(px) == 0:
130
+ return None
131
+
132
+ R, G, B = [np.mean(px[:, i]) for i in range(3)]
133
+
134
+ hsv = cv2.cvtColor(img_cal, cv2.COLOR_RGB2HSV)
135
+ hpx = hsv[roi_mask > 0]
136
+ H, S, V = [np.mean(hpx[:, i]) for i in range(3)]
137
+
138
+ lab = cv2.cvtColor(img_cal, cv2.COLOR_RGB2LAB)
139
+ lpx = lab[roi_mask > 0]
140
+ L, A, Bc = [np.mean(lpx[:, i]) for i in range(3)]
141
+
142
+ return {
143
+ 'R': R, 'G': G, 'B': B,
144
+ 'H': H, 'S': S, 'V': V,
145
+ 'L_color': L, 'A_color': A, 'B_color': Bc
146
+ }
147
+ except Exception as e:
148
+ print(f"Image processing error: {e}")
149
+ return None
150
+
151
+
152
+ # ============================================================
153
+ # FORM INPUT → FEATURE VECTOR MAPPING
154
+ # ============================================================
155
+ def form_to_features(
156
+ age, income_range, family_size, diet,
157
+ food_grains, food_leafy, food_cereals, food_pulses,
158
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
159
+ fruit_freq, veg_freq, tea_coffee,
160
+ comorb_dm, comorb_htn, comorb_thyroid,
161
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
162
+ menstrual, pads_per_day,
163
+ gravida, para, live_births, abortions, stillbirths,
164
+ family_dm, family_htn
165
+ ):
166
+ """Convert standardized form inputs to feature vector."""
167
+
168
+ # Income mapping
169
+ income_map = {
170
+ "Below ₹5,000": 2500,
171
+ "₹5,000 - ₹10,000": 7500,
172
+ "₹10,000 - ₹15,000": 12500,
173
+ "₹15,000 - ₹25,000": 20000,
174
+ "₹25,000 - ₹50,000": 37500,
175
+ "Above ₹50,000": 75000
176
+ }
177
+ income = income_map.get(income_range, 20000)
178
+ income_per_capita = income / max(family_size, 1)
179
+
180
+ # Diet one-hot
181
+ diet_veg = 1 if diet == "Vegetarian" else 0
182
+ diet_nonveg = 1 if diet == "Non-vegetarian" else 0
183
+ diet_mixed = 1 if diet == "Mixed" else 0
184
+
185
+ # Iron foods
186
+ food_iron_count = sum([food_grains, food_leafy, food_cereals, food_pulses,
187
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts])
188
+
189
+ # Frequency mapping
190
+ freq_map = {"Daily": 4, "Twice a week": 3, "Once a week": 2, "Rarely": 1, "Never": 0}
191
+ fruit_freq_val = freq_map.get(fruit_freq, 2)
192
+ veg_freq_val = freq_map.get(veg_freq, 2)
193
+
194
+ # Binary mappings
195
+ tea_coffee_val = 1 if tea_coffee == "Yes" else 0
196
+ comorb_dm_val = 1 if comorb_dm else 0
197
+ comorb_htn_val = 1 if comorb_htn else 0
198
+ comorb_thyroid_val = 1 if comorb_thyroid else 0
199
+ comorb_none = 1 if not any([comorb_dm, comorb_htn, comorb_thyroid]) else 0
200
+ comorb_count = comorb_dm_val + comorb_htn_val + comorb_thyroid_val
201
+
202
+ prev_anemia_val = 1 if prev_anemia == "Yes" else 0
203
+ family_anemia_val = 1 if family_anemia == "Yes" else 0
204
+ hookworm_val = 1 if hookworm == "Yes" else 0
205
+ bleeding_val = 1 if bleeding_disorder == "Yes" else 0
206
+ deworming_val = 1 if deworming == "Yes" else 0
207
+
208
+ # Menstrual
209
+ menstrual_regular = 1 if menstrual == "Regular" else 0
210
+ menstrual_irregular = 1 if menstrual == "Irregular" else 0
211
+
212
+ # Pads mapping
213
+ pads_map = {"1-2": 1.5, "2-3": 2.5, "3-4": 3.5, "4-5": 4.5, "More than 5": 6, "Not applicable": 2.5}
214
+ pads_val = pads_map.get(pads_per_day, 2.5)
215
+
216
+ # Obstetric
217
+ has_obstetric = 1 if gravida > 0 else 0
218
+ pregnancy_loss = abortions + stillbirths
219
+ parity_ratio = para / gravida if gravida > 0 else 0
220
+
221
+ family_dm_val = 1 if family_dm == "Yes" else 0
222
+ family_htn_val = 1 if family_htn == "Yes" else 0
223
+
224
+ # Build feature dict (order must match training!)
225
+ features = {
226
+ 'age': age,
227
+ 'income': income,
228
+ 'family_size': family_size,
229
+ 'income_per_capita': income_per_capita,
230
+ 'diet_veg': diet_veg,
231
+ 'diet_nonveg': diet_nonveg,
232
+ 'diet_mixed': diet_mixed,
233
+ 'food_whole_grains': 1 if food_grains else 0,
234
+ 'food_green_leafy_vegetables': 1 if food_leafy else 0,
235
+ 'food_cereals': 1 if food_cereals else 0,
236
+ 'food_pulses': 1 if food_pulses else 0,
237
+ 'food_fish': 1 if food_fish else 0,
238
+ 'food_eggs': 1 if food_eggs else 0,
239
+ 'food_meat': 1 if food_meat else 0,
240
+ 'food_jaggery': 1 if food_jaggery else 0,
241
+ 'food_nuts': 1 if food_nuts else 0,
242
+ 'food_iron_count': food_iron_count,
243
+ 'fruit_freq': fruit_freq_val,
244
+ 'veg_freq': veg_freq_val,
245
+ 'tea_coffee_daily': tea_coffee_val,
246
+ 'tea_coffee_daily_missing': 0,
247
+ 'comorb_dm': comorb_dm_val,
248
+ 'comorb_htn': comorb_htn_val,
249
+ 'comorb_thyroid': comorb_thyroid_val,
250
+ 'comorb_none': comorb_none,
251
+ 'comorb_count': comorb_count,
252
+ 'prev_anemia': prev_anemia_val,
253
+ 'prev_anemia_missing': 0,
254
+ 'family_anemia': family_anemia_val,
255
+ 'family_anemia_missing': 0,
256
+ 'hookworm_history': hookworm_val,
257
+ 'hookworm_history_missing': 0,
258
+ 'bleeding_disorder': bleeding_val,
259
+ 'bleeding_disorder_missing': 0,
260
+ 'deworming_done': deworming_val,
261
+ 'deworming_done_missing': 0,
262
+ 'menstrual_regular': menstrual_regular,
263
+ 'menstrual_irregular': menstrual_irregular,
264
+ 'pads_per_day': pads_val,
265
+ 'pads_missing': 0,
266
+ 'gravida': gravida,
267
+ 'para': para,
268
+ 'live_births': live_births,
269
+ 'abortions': abortions,
270
+ 'stillbirths': stillbirths,
271
+ 'has_obstetric_history': has_obstetric,
272
+ 'pregnancy_loss': pregnancy_loss,
273
+ 'parity_ratio': parity_ratio,
274
+ 'family_dm': family_dm_val,
275
+ 'family_dm_missing': 0,
276
+ 'family_htn': family_htn_val,
277
+ 'family_htn_missing': 0,
278
+ }
279
+
280
+ return features
281
+
282
+
283
+ # ============================================================
284
+ # DATA LOGGING
285
+ # ============================================================
286
+ def log_data(session_id, features, image_features, predicted_hb, actual_hb=None):
287
+ """Log anonymized data for model improvement."""
288
+ try:
289
+ record = {
290
+ 'session_id': session_id,
291
+ 'timestamp': datetime.now().isoformat(),
292
+ 'predicted_hb': predicted_hb,
293
+ 'actual_hb': actual_hb,
294
+ **features,
295
+ **image_features
296
+ }
297
+
298
+ df = pd.DataFrame([record])
299
+
300
+ # Append to CSV (or send to Google Sheets in production)
301
+ if Path(DATA_LOG_PATH).exists():
302
+ df.to_csv(DATA_LOG_PATH, mode='a', header=False, index=False)
303
+ else:
304
+ df.to_csv(DATA_LOG_PATH, index=False)
305
+
306
+ return True
307
+ except Exception as e:
308
+ print(f"Logging error: {e}")
309
+ return False
310
+
311
+
312
+ # ============================================================
313
+ # MAIN PREDICTION FUNCTION
314
+ # ============================================================
315
+ def predict_hb(
316
+ image,
317
+ age, income_range, family_size, diet,
318
+ food_grains, food_leafy, food_cereals, food_pulses,
319
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
320
+ fruit_freq, veg_freq, tea_coffee,
321
+ comorb_dm, comorb_htn, comorb_thyroid,
322
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
323
+ menstrual, pads_per_day,
324
+ gravida, para, live_births, abortions, stillbirths,
325
+ family_dm, family_htn,
326
+ actual_hb_input
327
+ ):
328
+ """Main prediction function."""
329
+
330
+ # Generate session ID
331
+ session_id = str(uuid.uuid4())[:8]
332
+
333
+ # Validate image
334
+ if image is None:
335
+ return "❌ Please upload an image of the palpebral conjunctiva with color card.", "", ""
336
+
337
+ # Extract image features
338
+ image_features = extract_image_features(image)
339
+ if image_features is None:
340
+ return "❌ Could not process image. Please ensure the palpebral conjunctiva is clearly visible.", "", ""
341
+
342
+ # Convert form to features
343
+ tabular_features = form_to_features(
344
+ age, income_range, family_size, diet,
345
+ food_grains, food_leafy, food_cereals, food_pulses,
346
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
347
+ fruit_freq, veg_freq, tea_coffee,
348
+ comorb_dm, comorb_htn, comorb_thyroid,
349
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
350
+ menstrual, pads_per_day,
351
+ gravida, para, live_births, abortions, stillbirths,
352
+ family_dm, family_htn
353
+ )
354
+
355
+ # Combine features
356
+ all_features = {**tabular_features, **image_features}
357
+
358
+ # Create feature vector in correct order
359
+ X = pd.DataFrame([all_features])[feature_names]
360
+
361
+ # Predict
362
+ hb_pred = ml_model.predict(X)[0]
363
+ hb_pred = round(float(hb_pred), 1)
364
+
365
+ # Classify severity
366
+ if hb_pred >= 12:
367
+ severity = "Normal"
368
+ color = "🟢"
369
+ recommendation = "No signs of anemia detected. Maintain a balanced diet."
370
+ elif hb_pred >= 11:
371
+ severity = "Mild Anemia"
372
+ color = "🟡"
373
+ recommendation = "Mild anemia indicated. Consider iron-rich foods and consult a healthcare provider."
374
+ elif hb_pred >= 8:
375
+ severity = "Moderate Anemia"
376
+ color = "🟠"
377
+ recommendation = "Moderate anemia detected. Please consult a healthcare provider for a confirmatory blood test."
378
+ else:
379
+ severity = "Severe Anemia"
380
+ color = "🔴"
381
+ recommendation = "Severe anemia indicated. Please seek immediate medical attention."
382
+
383
+ # Log data
384
+ actual_hb = float(actual_hb_input) if actual_hb_input and actual_hb_input > 0 else None
385
+ log_data(session_id, tabular_features, image_features, hb_pred, actual_hb)
386
+
387
+ # Format result
388
+ result = f"""
389
+ ## {color} Estimated Hemoglobin: {hb_pred} g/dL
390
+
391
+ ### Status: {severity}
392
+
393
+ {recommendation}
394
+
395
+ ---
396
+ *Session ID: {session_id}*
397
+ *Save this ID if you want to provide actual Hb value later.*
398
+ """
399
+
400
+ return result, session_id, f"{hb_pred} g/dL"
401
+
402
+
403
+ # ============================================================
404
+ # GRADIO INTERFACE
405
+ # ============================================================
406
+ def create_interface():
407
+ """Build the Gradio UI."""
408
+
409
+ with gr.Blocks(title="Anemia Screening Tool", theme=gr.themes.Soft()) as app:
410
+
411
+ gr.Markdown("""
412
+ # 🩺 Non-Invasive Anemia Screening Tool
413
+
414
+ Estimate hemoglobin levels from a photo of the palpebral conjunctiva (inner eyelid).
415
+
416
+ **Instructions:**
417
+ 1. Upload a clear photo of the lower inner eyelid WITH the color calibration card visible
418
+ 2. Fill in the health information form
419
+ 3. Click "Estimate Hemoglobin"
420
+
421
+ ⚠️ *This is a screening tool, not a diagnosis. Please consult a healthcare provider for confirmation.*
422
+
423
+ ---
424
+ """)
425
+
426
+ with gr.Row():
427
+ # Left column - Image upload
428
+ with gr.Column(scale=1):
429
+ gr.Markdown("### 📷 Image Upload")
430
+ image_input = gr.Image(
431
+ label="Palpebral Conjunctiva Image (with color card)",
432
+ type="numpy"
433
+ )
434
+ gr.Markdown("*Ensure the grey/white color card is visible in the image for accurate calibration.*")
435
+
436
+ # Right column - Form
437
+ with gr.Column(scale=2):
438
+ gr.Markdown("### 📋 Health Information")
439
+
440
+ with gr.Accordion("Demographics", open=True):
441
+ with gr.Row():
442
+ age = gr.Slider(minimum=10, maximum=80, value=25, step=1, label="Age (years)")
443
+ family_size = gr.Slider(minimum=1, maximum=15, value=4, step=1, label="Family Size")
444
+ income_range = gr.Dropdown(
445
+ choices=["Below ₹5,000", "₹5,000 - ₹10,000", "₹10,000 - ₹15,000",
446
+ "₹15,000 - ₹25,000", "₹25,000 - ₹50,000", "Above ₹50,000"],
447
+ value="₹10,000 - ₹15,000",
448
+ label="Monthly Family Income"
449
+ )
450
+
451
+ with gr.Accordion("Diet & Nutrition", open=True):
452
+ diet = gr.Radio(
453
+ choices=["Vegetarian", "Non-vegetarian", "Mixed"],
454
+ value="Mixed",
455
+ label="Dietary Habit"
456
+ )
457
+ gr.Markdown("**Iron-rich foods consumed regularly:**")
458
+ with gr.Row():
459
+ food_grains = gr.Checkbox(label="Whole Grains", value=True)
460
+ food_leafy = gr.Checkbox(label="Leafy Vegetables", value=True)
461
+ food_cereals = gr.Checkbox(label="Cereals", value=True)
462
+ with gr.Row():
463
+ food_pulses = gr.Checkbox(label="Pulses/Lentils", value=True)
464
+ food_fish = gr.Checkbox(label="Fish", value=False)
465
+ food_eggs = gr.Checkbox(label="Eggs", value=False)
466
+ with gr.Row():
467
+ food_meat = gr.Checkbox(label="Meat", value=False)
468
+ food_jaggery = gr.Checkbox(label="Jaggery", value=False)
469
+ food_nuts = gr.Checkbox(label="Nuts", value=False)
470
+ with gr.Row():
471
+ fruit_freq = gr.Dropdown(
472
+ choices=["Daily", "Twice a week", "Once a week", "Rarely", "Never"],
473
+ value="Twice a week",
474
+ label="Fruit Consumption"
475
+ )
476
+ veg_freq = gr.Dropdown(
477
+ choices=["Daily", "Twice a week", "Once a week", "Rarely", "Never"],
478
+ value="Daily",
479
+ label="Vegetable Consumption"
480
+ )
481
+ tea_coffee = gr.Radio(
482
+ choices=["Yes", "No"],
483
+ value="Yes",
484
+ label="Daily Tea/Coffee Consumption"
485
+ )
486
+
487
+ with gr.Accordion("Medical History", open=False):
488
+ gr.Markdown("**Current Conditions:**")
489
+ with gr.Row():
490
+ comorb_dm = gr.Checkbox(label="Diabetes", value=False)
491
+ comorb_htn = gr.Checkbox(label="Hypertension", value=False)
492
+ comorb_thyroid = gr.Checkbox(label="Thyroid Disorder", value=False)
493
+ with gr.Row():
494
+ prev_anemia = gr.Radio(choices=["Yes", "No"], value="No", label="Anemia in past 12 months")
495
+ family_anemia = gr.Radio(choices=["Yes", "No"], value="No", label="Family history of anemia")
496
+ with gr.Row():
497
+ hookworm = gr.Radio(choices=["Yes", "No"], value="No", label="History of hookworm")
498
+ bleeding_disorder = gr.Radio(choices=["Yes", "No"], value="No", label="Bleeding disorder")
499
+ with gr.Row():
500
+ deworming = gr.Radio(choices=["Yes", "No"], value="Yes", label="Deworming done")
501
+ with gr.Row():
502
+ family_dm = gr.Radio(choices=["Yes", "No"], value="No", label="Family history: Diabetes")
503
+ family_htn = gr.Radio(choices=["Yes", "No"], value="No", label="Family history: Hypertension")
504
+
505
+ with gr.Accordion("Menstrual & Obstetric History", open=False):
506
+ with gr.Row():
507
+ menstrual = gr.Radio(
508
+ choices=["Regular", "Irregular", "Not applicable"],
509
+ value="Regular",
510
+ label="Menstrual Cycle"
511
+ )
512
+ pads_per_day = gr.Dropdown(
513
+ choices=["1-2", "2-3", "3-4", "4-5", "More than 5", "Not applicable"],
514
+ value="2-3",
515
+ label="Pads per day (first 2-3 days)"
516
+ )
517
+ gr.Markdown("**Obstetric History (enter 0 if not applicable):**")
518
+ with gr.Row():
519
+ gravida = gr.Slider(minimum=0, maximum=10, value=0, step=1, label="Gravida (pregnancies)")
520
+ para = gr.Slider(minimum=0, maximum=10, value=0, step=1, label="Para (deliveries)")
521
+ live_births = gr.Slider(minimum=0, maximum=10, value=0, step=1, label="Live Births")
522
+ with gr.Row():
523
+ abortions = gr.Slider(minimum=0, maximum=5, value=0, step=1, label="Abortions")
524
+ stillbirths = gr.Slider(minimum=0, maximum=5, value=0, step=1, label="Stillbirths")
525
+
526
+ gr.Markdown("---")
527
+
528
+ with gr.Row():
529
+ with gr.Column():
530
+ actual_hb_input = gr.Number(
531
+ label="Already have a blood test result? Enter actual Hb (g/dL) — Optional",
532
+ value=0,
533
+ minimum=0,
534
+ maximum=25
535
+ )
536
+ with gr.Column():
537
+ submit_btn = gr.Button("🔬 Estimate Hemoglobin", variant="primary", size="lg")
538
+
539
+ gr.Markdown("---")
540
+
541
+ # Results
542
+ with gr.Row():
543
+ result_output = gr.Markdown(label="Result")
544
+ with gr.Row():
545
+ session_output = gr.Textbox(label="Session ID", interactive=False)
546
+ hb_output = gr.Textbox(label="Estimated Hb", interactive=False)
547
+
548
+ # Privacy notice
549
+ gr.Markdown("""
550
+ ---
551
+ ### 🔒 Privacy Notice
552
+
553
+ Your data is collected **anonymously** to improve this screening tool. We do NOT collect:
554
+ - Name, address, phone number, or email
555
+ - Raw images (only extracted color features are stored)
556
+ - Any personally identifiable information
557
+
558
+ By using this tool, you consent to anonymous data collection for research purposes.
559
+
560
+ ---
561
+ *Developed by JIPMER Anemia Screening Study | For research use only*
562
+ """)
563
+
564
+ # Connect button
565
+ submit_btn.click(
566
+ fn=predict_hb,
567
+ inputs=[
568
+ image_input,
569
+ age, income_range, family_size, diet,
570
+ food_grains, food_leafy, food_cereals, food_pulses,
571
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
572
+ fruit_freq, veg_freq, tea_coffee,
573
+ comorb_dm, comorb_htn, comorb_thyroid,
574
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
575
+ menstrual, pads_per_day,
576
+ gravida, para, live_births, abortions, stillbirths,
577
+ family_dm, family_htn,
578
+ actual_hb_input
579
+ ],
580
+ outputs=[result_output, session_output, hb_output]
581
+ )
582
+
583
+ return app
584
+
585
+
586
+ # ============================================================
587
+ # MAIN
588
+ # ============================================================
589
+ if __name__ == "__main__":
590
+ print("Loading models...")
591
+ load_models()
592
+
593
+ print("Starting app...")
594
+ app = create_interface()
595
+ app.launch()
best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:855d25f15fdb3c145219cdb418c6686aa74a43aa85b493f2d1b5c98bb909bc17
3
+ size 6779380
model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:09c7c101f7a5788273dcde5063d5eddda26c80e76142bd5b3935c824d7165634
3
+ size 1351178
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ numpy>=1.24.0
3
+ pandas>=2.0.0
4
+ opencv-python-headless>=4.8.0
5
+ ultralytics>=8.0.0
6
+ scikit-learn>=1.3.0
7
+ xgboost>=2.0.0