medtechdev commited on
Commit
55eae89
·
verified ·
1 Parent(s): e3ae41a

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +388 -0
  2. buccal_best.pt +3 -0
  3. model.pkl +3 -0
  4. palm_best.pt +3 -0
app.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MULTI-SITE NON-INVASIVE ANEMIA SCREENING
3
+ =========================================
4
+ Palpebral (required) + Buccal (optional) + Palm (optional)
5
+ """
6
+
7
+ import gradio as gr
8
+ import numpy as np
9
+ import pandas as pd
10
+ import pickle
11
+ import cv2
12
+ import uuid
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ # ============================================================
17
+ # CONFIG
18
+ # ============================================================
19
+ MODEL_PATH = "model.pkl"
20
+ YOLO_PALP = "best.pt"
21
+ YOLO_BUCC = "buccal_best.pt"
22
+ YOLO_PALM = "palm_best.pt"
23
+ DATA_LOG = "collected_data.csv"
24
+
25
+ # ============================================================
26
+ # LOAD MODELS
27
+ # ============================================================
28
+ def load_models():
29
+ global ml_model, feature_names, yolo_palp, yolo_bucc, yolo_palm
30
+
31
+ with open(MODEL_PATH, 'rb') as f:
32
+ data = pickle.load(f)
33
+ ml_model = data['model']
34
+ feature_names = data['features']
35
+ print(f"ML model: {data.get('name', 'Unknown')}, {len(feature_names)} features")
36
+
37
+ from ultralytics import YOLO
38
+ yolo_palp = YOLO(YOLO_PALP)
39
+ print("Palpebral YOLO loaded")
40
+
41
+ try:
42
+ yolo_bucc = YOLO(YOLO_BUCC)
43
+ print("Buccal YOLO loaded")
44
+ except:
45
+ yolo_bucc = None
46
+ print("Buccal YOLO not found")
47
+
48
+ try:
49
+ yolo_palm = YOLO(YOLO_PALM)
50
+ print("Palm YOLO loaded")
51
+ except:
52
+ yolo_palm = None
53
+ print("Palm YOLO not found")
54
+
55
+ # ============================================================
56
+ # IMAGE PROCESSING
57
+ # ============================================================
58
+ def calibrate_image(img):
59
+ try:
60
+ if img is None:
61
+ return None
62
+ if len(img.shape) == 2:
63
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
64
+ elif img.shape[2] == 4:
65
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_RGBA2RGB)
66
+ else:
67
+ img_rgb = img.copy()
68
+
69
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
70
+ _, thresh = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY)
71
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
72
+
73
+ if not contours:
74
+ return img_rgb
75
+
76
+ c = max(contours, key=cv2.contourArea)
77
+ x, y, w, h = cv2.boundingRect(c)
78
+ if w * h < 1000:
79
+ return img_rgb
80
+
81
+ patch = img_rgb[y+h//4:y+3*h//4, x+w//4:x+3*w//4]
82
+ if patch.size == 0:
83
+ return img_rgb
84
+
85
+ avg = np.mean(patch, axis=(0, 1))
86
+ if any(v < 10 for v in avg):
87
+ return img_rgb
88
+
89
+ scale = np.array([128, 128, 128]) / avg
90
+ return np.clip(img_rgb * scale, 0, 255).astype(np.uint8)
91
+ except:
92
+ return img
93
+
94
+
95
+ def extract_features(img, yolo, prefix):
96
+ if img is None or yolo is None:
97
+ return None
98
+
99
+ try:
100
+ img_cal = calibrate_image(img)
101
+ if img_cal is None:
102
+ return None
103
+
104
+ results = yolo(img_cal, verbose=False)
105
+
106
+ if len(results) > 0 and results[0].masks is not None:
107
+ masks = results[0].masks.data.cpu().numpy()
108
+ confs = results[0].boxes.conf.cpu().numpy()
109
+ if len(confs) > 0:
110
+ m = masks[np.argmax(confs)]
111
+ m = cv2.resize(m, (img_cal.shape[1], img_cal.shape[0]))
112
+ mask = (m > 0.5).astype(np.uint8) * 255
113
+ else:
114
+ h, w = img_cal.shape[:2]
115
+ mask = np.zeros((h, w), dtype=np.uint8)
116
+ mask[h//4:3*h//4, w//4:3*w//4] = 255
117
+ else:
118
+ h, w = img_cal.shape[:2]
119
+ mask = np.zeros((h, w), dtype=np.uint8)
120
+ mask[h//4:3*h//4, w//4:3*w//4] = 255
121
+
122
+ px = img_cal[mask > 0]
123
+ if len(px) < 100:
124
+ return None
125
+
126
+ R, G, B = [np.mean(px[:, i]) for i in range(3)]
127
+ hsv = cv2.cvtColor(img_cal, cv2.COLOR_RGB2HSV)
128
+ H, S, V = [np.mean(hsv[mask > 0][:, i]) for i in range(3)]
129
+ lab = cv2.cvtColor(img_cal, cv2.COLOR_RGB2LAB)
130
+ L, A, Bc = [np.mean(lab[mask > 0][:, i]) for i in range(3)]
131
+
132
+ return {
133
+ f'{prefix}_R': R, f'{prefix}_G': G, f'{prefix}_B': B,
134
+ f'{prefix}_H': H, f'{prefix}_S': S, f'{prefix}_V': V,
135
+ f'{prefix}_L_color': L, f'{prefix}_A_color': A, f'{prefix}_B_color': Bc
136
+ }
137
+ except Exception as e:
138
+ print(f"Error {prefix}: {e}")
139
+ return None
140
+
141
+
142
+ # ============================================================
143
+ # FORM TO FEATURES
144
+ # ============================================================
145
+ def form_to_features(age, income_range, family_size, diet,
146
+ food_grains, food_leafy, food_cereals, food_pulses,
147
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
148
+ fruit_freq, veg_freq, tea_coffee,
149
+ comorb_dm, comorb_htn, comorb_thyroid,
150
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
151
+ menstrual, pads_per_day,
152
+ gravida, para, live_births, abortions, stillbirths,
153
+ family_dm, family_htn):
154
+
155
+ income_map = {"Below ₹5,000": 2500, "₹5,000 - ₹10,000": 7500, "₹10,000 - ₹15,000": 12500,
156
+ "₹15,000 - ₹25,000": 20000, "₹25,000 - ₹50,000": 37500, "Above ₹50,000": 75000}
157
+ freq_map = {"Daily": 4, "Twice a week": 3, "Once a week": 2, "Rarely": 1, "Never": 0}
158
+ 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}
159
+
160
+ income = income_map.get(income_range, 20000)
161
+
162
+ return {
163
+ 'age': age, 'income': income, 'family_size': family_size,
164
+ 'income_per_capita': income / max(family_size, 1),
165
+ 'diet_veg': 1 if diet == "Vegetarian" else 0,
166
+ 'diet_nonveg': 1 if diet == "Non-vegetarian" else 0,
167
+ 'diet_mixed': 1 if diet == "Mixed" else 0,
168
+ 'food_whole_grains': int(food_grains), 'food_green_leafy_vegetables': int(food_leafy),
169
+ 'food_cereals': int(food_cereals), 'food_pulses': int(food_pulses),
170
+ 'food_fish': int(food_fish), 'food_eggs': int(food_eggs),
171
+ 'food_meat': int(food_meat), 'food_jaggery': int(food_jaggery), 'food_nuts': int(food_nuts),
172
+ 'food_iron_count': sum([food_grains, food_leafy, food_cereals, food_pulses, food_fish, food_eggs, food_meat, food_jaggery, food_nuts]),
173
+ 'fruit_freq': freq_map.get(fruit_freq, 2), 'veg_freq': freq_map.get(veg_freq, 2),
174
+ 'tea_coffee_daily': 1 if tea_coffee == "Yes" else 0, 'tea_coffee_daily_missing': 0,
175
+ 'comorb_dm': int(comorb_dm), 'comorb_htn': int(comorb_htn), 'comorb_thyroid': int(comorb_thyroid),
176
+ 'comorb_none': 1 if not any([comorb_dm, comorb_htn, comorb_thyroid]) else 0,
177
+ 'comorb_count': sum([int(comorb_dm), int(comorb_htn), int(comorb_thyroid)]),
178
+ 'prev_anemia': 1 if prev_anemia == "Yes" else 0, 'prev_anemia_missing': 0,
179
+ 'family_anemia': 1 if family_anemia == "Yes" else 0, 'family_anemia_missing': 0,
180
+ 'hookworm_history': 1 if hookworm == "Yes" else 0, 'hookworm_history_missing': 0,
181
+ 'bleeding_disorder': 1 if bleeding_disorder == "Yes" else 0, 'bleeding_disorder_missing': 0,
182
+ 'deworming_done': 1 if deworming == "Yes" else 0, 'deworming_done_missing': 0,
183
+ 'menstrual_regular': 1 if menstrual == "Regular" else 0,
184
+ 'menstrual_irregular': 1 if menstrual == "Irregular" else 0,
185
+ 'pads_per_day': pads_map.get(pads_per_day, 2.5), 'pads_missing': 0,
186
+ 'gravida': gravida, 'para': para, 'live_births': live_births,
187
+ 'abortions': abortions, 'stillbirths': stillbirths,
188
+ 'has_obstetric_history': 1 if gravida > 0 else 0,
189
+ 'pregnancy_loss': abortions + stillbirths,
190
+ 'parity_ratio': para / gravida if gravida > 0 else 0,
191
+ 'family_dm': 1 if family_dm == "Yes" else 0, 'family_dm_missing': 0,
192
+ 'family_htn': 1 if family_htn == "Yes" else 0, 'family_htn_missing': 0,
193
+ }
194
+
195
+
196
+ # ============================================================
197
+ # PREDICTION
198
+ # ============================================================
199
+ def predict_hb(img_palp, img_bucc, img_palm,
200
+ age, income_range, family_size, diet,
201
+ food_grains, food_leafy, food_cereals, food_pulses,
202
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
203
+ fruit_freq, veg_freq, tea_coffee,
204
+ comorb_dm, comorb_htn, comorb_thyroid,
205
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
206
+ menstrual, pads_per_day,
207
+ gravida, para, live_births, abortions, stillbirths,
208
+ family_dm, family_htn, actual_hb):
209
+
210
+ session_id = str(uuid.uuid4())[:8]
211
+ sites = []
212
+
213
+ if img_palp is None:
214
+ return "❌ Palpebral image required.", "", ""
215
+
216
+ all_features = {}
217
+
218
+ # Palpebral (required)
219
+ palp_feat = extract_features(img_palp, yolo_palp, 'palpebral')
220
+ if palp_feat is None:
221
+ return "❌ Could not process palpebral image.", "", ""
222
+ all_features.update(palp_feat)
223
+ sites.append('palpebral')
224
+
225
+ # Buccal (optional)
226
+ if img_bucc is not None and yolo_bucc:
227
+ bucc_feat = extract_features(img_bucc, yolo_bucc, 'buccal')
228
+ if bucc_feat:
229
+ all_features.update(bucc_feat)
230
+ sites.append('buccal')
231
+
232
+ # Palm (optional)
233
+ if img_palm is not None and yolo_palm:
234
+ palm_feat = extract_features(img_palm, yolo_palm, 'palm')
235
+ if palm_feat:
236
+ all_features.update(palm_feat)
237
+ sites.append('palm')
238
+
239
+ # Tabular
240
+ tab_feat = form_to_features(age, income_range, family_size, diet,
241
+ food_grains, food_leafy, food_cereals, food_pulses,
242
+ food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
243
+ fruit_freq, veg_freq, tea_coffee,
244
+ comorb_dm, comorb_htn, comorb_thyroid,
245
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
246
+ menstrual, pads_per_day,
247
+ gravida, para, live_births, abortions, stillbirths,
248
+ family_dm, family_htn)
249
+ all_features.update(tab_feat)
250
+
251
+ # Default values for missing image features
252
+ defaults = {
253
+ 'palpebral_R': 150, 'palpebral_G': 100, 'palpebral_B': 100,
254
+ 'palpebral_H': 10, 'palpebral_S': 100, 'palpebral_V': 150,
255
+ 'palpebral_L_color': 130, 'palpebral_A_color': 150, 'palpebral_B_color': 140,
256
+ 'buccal_R': 160, 'buccal_G': 110, 'buccal_B': 110,
257
+ 'buccal_H': 8, 'buccal_S': 90, 'buccal_V': 160,
258
+ 'buccal_L_color': 140, 'buccal_A_color': 145, 'buccal_B_color': 135,
259
+ 'palm_R': 170, 'palm_G': 130, 'palm_B': 120,
260
+ 'palm_H': 12, 'palm_S': 70, 'palm_V': 170,
261
+ 'palm_L_color': 150, 'palm_A_color': 140, 'palm_B_color': 130,
262
+ }
263
+
264
+ for f in feature_names:
265
+ if f not in all_features:
266
+ all_features[f] = defaults.get(f, 0)
267
+
268
+ X = pd.DataFrame([all_features])[feature_names]
269
+ hb = round(float(ml_model.predict(X)[0]), 1)
270
+
271
+ if hb >= 12:
272
+ sev, col, rec = "Normal", "🟢", "No anemia detected."
273
+ elif hb >= 11:
274
+ sev, col, rec = "Mild Anemia", "🟡", "Consider iron-rich foods."
275
+ elif hb >= 8:
276
+ sev, col, rec = "Moderate Anemia", "🟠", "Consult healthcare provider."
277
+ else:
278
+ sev, col, rec = "Severe Anemia", "🔴", "Seek immediate medical attention."
279
+
280
+ # Log
281
+ try:
282
+ record = {'session_id': session_id, 'timestamp': datetime.now().isoformat(),
283
+ 'predicted_hb': hb, 'actual_hb': actual_hb if actual_hb > 0 else None,
284
+ 'sites': ','.join(sites), **all_features}
285
+ df = pd.DataFrame([record])
286
+ if Path(DATA_LOG).exists():
287
+ df.to_csv(DATA_LOG, mode='a', header=False, index=False)
288
+ else:
289
+ df.to_csv(DATA_LOG, index=False)
290
+ except:
291
+ pass
292
+
293
+ result = f"## {col} Estimated Hb: {hb} g/dL\n### {sev}\n{rec}\n\n---\n**Sites:** {', '.join(sites)} | **Session:** {session_id}"
294
+ return result, session_id, f"{hb} g/dL"
295
+
296
+
297
+ # ============================================================
298
+ # INTERFACE
299
+ # ============================================================
300
+ def create_app():
301
+ with gr.Blocks(title="Anemia Screening", theme=gr.themes.Soft()) as app:
302
+ gr.Markdown("# 🩺 Multi-Site Anemia Screening\nPalpebral required. Buccal & Palm optional.")
303
+
304
+ with gr.Row():
305
+ with gr.Column(scale=1):
306
+ img_palp = gr.Image(label="Palpebral Conjunctiva (Required)", type="numpy")
307
+ img_bucc = gr.Image(label="Buccal Mucosa (Optional)", type="numpy")
308
+ img_palm = gr.Image(label="Palm (Optional)", type="numpy")
309
+
310
+ with gr.Column(scale=2):
311
+ with gr.Accordion("Demographics", open=True):
312
+ with gr.Row():
313
+ age = gr.Slider(10, 80, 25, step=1, label="Age")
314
+ family_size = gr.Slider(1, 15, 4, step=1, label="Family Size")
315
+ income_range = gr.Dropdown(["Below ₹5,000", "₹5,000 - ₹10,000", "₹10,000 - ₹15,000", "₹15,000 - ₹25,000", "₹25,000 - ₹50,000", "Above ₹50,000"], value="₹10,000 - ₹15,000", label="Income")
316
+
317
+ with gr.Accordion("Diet", open=True):
318
+ diet = gr.Radio(["Vegetarian", "Non-vegetarian", "Mixed"], value="Mixed", label="Diet")
319
+ with gr.Row():
320
+ food_grains = gr.Checkbox(label="Whole Grains", value=True)
321
+ food_leafy = gr.Checkbox(label="Leafy Veg", value=True)
322
+ food_cereals = gr.Checkbox(label="Cereals", value=True)
323
+ with gr.Row():
324
+ food_pulses = gr.Checkbox(label="Pulses", value=True)
325
+ food_fish = gr.Checkbox(label="Fish")
326
+ food_eggs = gr.Checkbox(label="Eggs")
327
+ with gr.Row():
328
+ food_meat = gr.Checkbox(label="Meat")
329
+ food_jaggery = gr.Checkbox(label="Jaggery")
330
+ food_nuts = gr.Checkbox(label="Nuts")
331
+ with gr.Row():
332
+ fruit_freq = gr.Dropdown(["Daily", "Twice a week", "Once a week", "Rarely", "Never"], value="Twice a week", label="Fruits")
333
+ veg_freq = gr.Dropdown(["Daily", "Twice a week", "Once a week", "Rarely", "Never"], value="Daily", label="Vegetables")
334
+ tea_coffee = gr.Radio(["Yes", "No"], value="Yes", label="Daily Tea/Coffee")
335
+
336
+ with gr.Accordion("Medical History", open=False):
337
+ with gr.Row():
338
+ comorb_dm = gr.Checkbox(label="Diabetes")
339
+ comorb_htn = gr.Checkbox(label="Hypertension")
340
+ comorb_thyroid = gr.Checkbox(label="Thyroid")
341
+ with gr.Row():
342
+ prev_anemia = gr.Radio(["Yes", "No"], value="No", label="Anemia (past 12mo)")
343
+ family_anemia = gr.Radio(["Yes", "No"], value="No", label="Family Anemia")
344
+ with gr.Row():
345
+ hookworm = gr.Radio(["Yes", "No"], value="No", label="Hookworm")
346
+ bleeding_disorder = gr.Radio(["Yes", "No"], value="No", label="Bleeding Disorder")
347
+ deworming = gr.Radio(["Yes", "No"], value="Yes", label="Deworming Done")
348
+ with gr.Row():
349
+ family_dm = gr.Radio(["Yes", "No"], value="No", label="Family Diabetes")
350
+ family_htn = gr.Radio(["Yes", "No"], value="No", label="Family Hypertension")
351
+
352
+ with gr.Accordion("Menstrual/Obstetric", open=False):
353
+ with gr.Row():
354
+ menstrual = gr.Radio(["Regular", "Irregular", "Not applicable"], value="Regular", label="Menstrual")
355
+ pads_per_day = gr.Dropdown(["1-2", "2-3", "3-4", "4-5", "More than 5", "Not applicable"], value="2-3", label="Pads/day")
356
+ with gr.Row():
357
+ gravida = gr.Slider(0, 10, 0, step=1, label="Gravida")
358
+ para = gr.Slider(0, 10, 0, step=1, label="Para")
359
+ live_births = gr.Slider(0, 10, 0, step=1, label="Live Births")
360
+ with gr.Row():
361
+ abortions = gr.Slider(0, 5, 0, step=1, label="Abortions")
362
+ stillbirths = gr.Slider(0, 5, 0, step=1, label="Stillbirths")
363
+
364
+ with gr.Row():
365
+ actual_hb = gr.Number(label="Actual Hb (optional)", value=0, minimum=0, maximum=25)
366
+ submit = gr.Button("🔬 Estimate Hb", variant="primary", size="lg")
367
+
368
+ result = gr.Markdown()
369
+ with gr.Row():
370
+ session = gr.Textbox(label="Session ID", interactive=False)
371
+ hb_out = gr.Textbox(label="Estimated Hb", interactive=False)
372
+
373
+ gr.Markdown("---\n🔒 Anonymous data collection. No PII stored.")
374
+
375
+ submit.click(predict_hb,
376
+ [img_palp, img_bucc, img_palm, age, income_range, family_size, diet,
377
+ food_grains, food_leafy, food_cereals, food_pulses, food_fish, food_eggs, food_meat, food_jaggery, food_nuts,
378
+ fruit_freq, veg_freq, tea_coffee, comorb_dm, comorb_htn, comorb_thyroid,
379
+ prev_anemia, family_anemia, hookworm, bleeding_disorder, deworming,
380
+ menstrual, pads_per_day, gravida, para, live_births, abortions, stillbirths,
381
+ family_dm, family_htn, actual_hb],
382
+ [result, session, hb_out])
383
+
384
+ return app
385
+
386
+ if __name__ == "__main__":
387
+ load_models()
388
+ create_app().launch()
buccal_best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:abc15008c5c9f6a6c93d25e77b41c1e74192b73333c4c4f43269853f42316b4d
3
+ size 6782196
model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3219978d7aaee438012322843641dc3c683fbec6770b588d886a84015fb56336
3
+ size 1315168
palm_best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b7d10d54fcf18b06bff467c60324fcbb8b400f01fb9f61c303a94ecf4acc917
3
+ size 6782132