SuriRaja commited on
Commit
355fec3
·
verified ·
1 Parent(s): 1dbd3a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +300 -69
app.py CHANGED
@@ -4,84 +4,315 @@ import pandas as pd
4
  import tempfile
5
  import cv2
6
  import os
 
 
 
 
 
 
 
7
 
8
  st.set_page_config(page_title="AI Health Lab", layout="wide")
9
 
10
- st.title("🧬 Face-based Health Lab")
11
- st.markdown("Upload a **facial image** (for Hb) and a **20-30 s face video** (for HR).\n\n"
12
- "This is a baseline app – models will be added for each test step-by-step.")
13
-
14
- # -------------------------------------------------
15
- # Sidebar instructions
16
- # -------------------------------------------------
17
- st.sidebar.header("Instructions")
18
- st.sidebar.info(
19
- "1. Upload a good quality **face / eye image** for Hemoglobin.\n"
20
- "2. Upload a **20-30 s face video** for Heart-Rate.\n"
21
- "3. Click **Run Analysis** to see test results.\n\n"
22
- "Later we will add more tests and replace the dummy predictions with real models."
23
  )
24
 
25
- # -------------------------------------------------
26
- # Input widgets
27
- # -------------------------------------------------
28
- uploaded_img = st.file_uploader("Upload Face / Eye Image for Hemoglobin", type=["jpg","jpeg","png"])
29
- uploaded_vid = st.file_uploader("Upload Face Video for Heart-Rate (20-30 s)", type=["mp4","avi","mov"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- run_btn = st.button("Run Analysis")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- # -------------------------------------------------
34
  # Placeholder prediction functions
35
- # -------------------------------------------------
36
- def predict_hemoglobin(image: np.ndarray) -> float:
37
- # TODO: replace with actual Hb model
38
- return float(np.random.uniform(11.0,15.0))
39
-
40
- def predict_heart_rate(video_path: str) -> float:
41
- # TODO: replace with real rPPG inference
42
- return float(np.random.uniform(65,85))
43
-
44
- # -------------------------------------------------
45
- # Run inference
46
- # -------------------------------------------------
47
- if run_btn:
48
- results = []
49
-
50
- # ---- Hemoglobin ----
51
- if uploaded_img is not None:
52
- file_bytes = np.asarray(bytearray(uploaded_img.read()), dtype=np.uint8)
53
- img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
54
- hb = predict_hemoglobin(img)
55
- results.append({"Test": "Hemoglobin", "Result": f"{hb:.2f} g/dL"})
56
- else:
57
- results.append({"Test": "Hemoglobin", "Result": "No image uploaded"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- # ---- Heart Rate ----
60
- if uploaded_vid is not None:
61
- # save video temporarily
62
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmpf:
63
- tmpf.write(uploaded_vid.read())
64
- tmp_path = tmpf.name
 
 
 
 
 
 
 
 
 
 
65
 
66
- hr = predict_heart_rate(tmp_path)
67
- results.append({"Test": "Heart Rate", "Result": f"{hr:.1f} bpm"})
68
 
69
- # clean up
70
- os.remove(tmp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  else:
72
- results.append({"Test": "Heart Rate", "Result": "No video uploaded"})
73
-
74
- # ---- Future tests placeholders ----
75
- results.append({"Test": "SpO₂", "Result": "—"})
76
- results.append({"Test": "Respiration Rate", "Result": "—"})
77
- # ... add more as needed later
78
-
79
- # -------------------------------------------------
80
- # Show results table
81
- # -------------------------------------------------
82
- df = pd.DataFrame(results)
83
- st.subheader("🩺 Test Results")
84
- st.table(df)
85
-
86
- else:
87
- st.info("⬆ Upload image and/or video, then click **Run Analysis**.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import tempfile
5
  import cv2
6
  import os
7
+ from typing import List, Dict, Any, Optional
8
+
9
+ # For live video capture via WebRTC
10
+ from streamlit_webrtc import webrtc_streamer, WebRtcMode, RTCConfiguration
11
+ import av
12
+ import time
13
+ from collections import deque
14
 
15
  st.set_page_config(page_title="AI Health Lab", layout="wide")
16
 
17
+ # =========================================================
18
+ # Config
19
+ # =========================================================
20
+ # ICE config for WebRTC (use public Google STUN; replace with your TURN for prod)
21
+ RTC_CONFIGURATION = RTCConfiguration(
22
+ {"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}
 
 
 
 
 
 
 
23
  )
24
 
25
+ # List your 27 tests here (we’ll fill values as models arrive)
26
+ TESTS = [
27
+ "Hemoglobin (Hb)", # 1
28
+ "Heart Rate (HR)", # 2
29
+ "SpO₂", # 3
30
+ "Respiration Rate (RR)", # 4
31
+ "Systolic BP", # 5
32
+ "Diastolic BP", # 6
33
+ "Body Temperature", # 7
34
+ "BMI", # 8
35
+ "Blood Glucose (Fasting)", # 9
36
+ "Blood Glucose (PP)", #10
37
+ "Total Cholesterol", #11
38
+ "LDL", #12
39
+ "HDL", #13
40
+ "Triglycerides", #14
41
+ "HbA1c", #15
42
+ "Hematocrit (HCT)", #16
43
+ "RBC Count", #17
44
+ "WBC Count", #18
45
+ "Platelet Count", #19
46
+ "Serum Creatinine", #20
47
+ "eGFR", #21
48
+ "Uric Acid", #22
49
+ "Vitamin D", #23
50
+ "Calcium", #24
51
+ "Sodium", #25
52
+ "Potassium", #26
53
+ "CRP" #27
54
+ ]
55
 
56
+ UNITS = {
57
+ "Hemoglobin (Hb)": "g/dL",
58
+ "Heart Rate (HR)": "bpm",
59
+ "SpO₂": "%",
60
+ "Respiration Rate (RR)": "breaths/min",
61
+ "Systolic BP": "mmHg",
62
+ "Diastolic BP": "mmHg",
63
+ "Body Temperature": "°C",
64
+ "BMI": "kg/m²",
65
+ "Blood Glucose (Fasting)": "mg/dL",
66
+ "Blood Glucose (PP)": "mg/dL",
67
+ "Total Cholesterol": "mg/dL",
68
+ "LDL": "mg/dL",
69
+ "HDL": "mg/dL",
70
+ "Triglycerides": "mg/dL",
71
+ "HbA1c": "%",
72
+ "Hematocrit (HCT)": "%",
73
+ "RBC Count": "10^6/µL",
74
+ "WBC Count": "10^3/µL",
75
+ "Platelet Count": "10^3/µL",
76
+ "Serum Creatinine": "mg/dL",
77
+ "eGFR": "mL/min/1.73m²",
78
+ "Uric Acid": "mg/dL",
79
+ "Vitamin D": "ng/mL",
80
+ "Calcium": "mg/dL",
81
+ "Sodium": "mEq/L",
82
+ "Potassium": "mEq/L",
83
+ "CRP": "mg/L"
84
+ }
85
 
86
+ # =========================================================
87
  # Placeholder prediction functions
88
+ # (Swap these with real model calls as you implement tests)
89
+ # =========================================================
90
+ def predict_hemoglobin_from_image(image_bgr: np.ndarray) -> Optional[float]:
91
+ """
92
+ TODO: Replace with real Hb inference (e.g., conjunctiva crop + CNN/ViT model).
93
+ Returns float(g/dL) or None if unavailable.
94
+ """
95
+ if image_bgr is None:
96
+ return None
97
+ # Simple sanity check: require "face-like" size
98
+ h, w = image_bgr.shape[:2]
99
+ if h < 64 or w < 64:
100
+ return None
101
+ # Placeholder random (stable per run by seeding if you want)
102
+ return float(np.random.uniform(11.0, 15.0))
103
+
104
+
105
+ def estimate_hr_from_video_file(video_path: str) -> Optional[float]:
106
+ """
107
+ TODO: Replace with actual rPPG pipeline (e.g., MTTS-CAN/PhysNet).
108
+ Returns float(bpm) or None.
109
+ """
110
+ if not video_path or not os.path.exists(video_path):
111
+ return None
112
+
113
+ cap = cv2.VideoCapture(video_path)
114
+ frames = 0
115
+ while True:
116
+ ret, _ = cap.read()
117
+ if not ret:
118
+ break
119
+ frames += 1
120
+ cap.release()
121
+ if frames < 30: # ~1s at 30fps; require more in real use
122
+ return None
123
+
124
+ return float(np.random.uniform(65, 85))
125
+
126
+
127
+ # =========================================================
128
+ # Live Video Processor (WebRTC): collect frames for N seconds
129
+ # =========================================================
130
+ class HRCollectorVideoProcessor:
131
+ """
132
+ Minimal frame collector. We buffer incoming frames for the duration the user records,
133
+ then write them to a temporary .mp4 to feed into estimate_hr_from_video_file().
134
+ Replace with a live rPPG inference pipeline when ready.
135
+ """
136
+ def __init__(self):
137
+ self.frames = deque(maxlen=30 * 60) # up to ~60s at 30fps
138
+ self.recording = False
139
+
140
+ def recv(self, frame: av.VideoFrame) -> av.VideoFrame:
141
+ img = frame.to_ndarray(format="bgr24")
142
+ if self.recording:
143
+ self.frames.append(img)
144
+ # pass-through (optional: draw overlay)
145
+ return av.VideoFrame.from_ndarray(img, format="bgr24")
146
+
147
+ def start(self):
148
+ self.recording = True
149
+ self.frames.clear()
150
+
151
+ def stop_and_dump_to_file(self) -> Optional[str]:
152
+ self.recording = False
153
+ if len(self.frames) < 30:
154
+ return None
155
+
156
+ # Write temp mp4
157
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
158
+ tmp_path = tmp.name
159
+ tmp.close()
160
+
161
+ # Guess size from first frame
162
+ h, w = self.frames[0].shape[:2]
163
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
164
+ out = cv2.VideoWriter(tmp_path, fourcc, 30.0, (w, h))
165
+ for frm in self.frames:
166
+ out.write(frm)
167
+ out.release()
168
+ return tmp_path
169
+
170
+
171
+ # =========================================================
172
+ # UI
173
+ # =========================================================
174
+ st.title("🧬 Face-based Health Lab")
175
+ st.caption("Capture via camera or upload files. Results table lists **27 tests** with numbering. "
176
+ "Swap out placeholders with real models as you go.")
177
 
178
+ with st.sidebar:
179
+ st.header("Capture Options")
180
+ cap_hb_mode = st.radio(
181
+ "Hemoglobin image source:",
182
+ ["Camera", "Upload"],
183
+ index=0,
184
+ help="Capture a face/eye image for Hb from the camera or upload a file"
185
+ )
186
+ cap_hr_mode = st.radio(
187
+ "Heart-rate video source:",
188
+ ["Camera (Live)", "Upload"],
189
+ index=0,
190
+ help="Record a short (20–30s) face video via camera or upload a video file"
191
+ )
192
+ st.info("Tip: Good lighting and steady face improve HR/Hb quality. "
193
+ "This app is a scaffold; accuracy depends on the models you integrate.")
194
 
195
+ # ==== Columns for inputs ====
196
+ col_img, col_vid = st.columns(2, gap="large")
197
 
198
+ with col_img:
199
+ st.subheader("Hemoglobin (Image)")
200
+ img_np_bgr = None
201
+
202
+ if cap_hb_mode == "Camera":
203
+ cam_img = st.camera_input("Capture Face / Eye Image")
204
+ if cam_img is not None:
205
+ file_bytes = np.asarray(bytearray(cam_img.read()), dtype=np.uint8)
206
+ img_np_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
207
+ st.image(cv2.cvtColor(img_np_bgr, cv2.COLOR_BGR2RGB), caption="Captured Image", use_column_width=True)
208
+ else:
209
+ up_img = st.file_uploader("Upload Image", type=["jpg", "jpeg", "png"])
210
+ if up_img is not None:
211
+ file_bytes = np.asarray(bytearray(up_img.read()), dtype=np.uint8)
212
+ img_np_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
213
+ st.image(cv2.cvtColor(img_np_bgr, cv2.COLOR_BGR2RGB), caption="Uploaded Image", use_column_width=True)
214
+
215
+ with col_vid:
216
+ st.subheader("Heart Rate (Video)")
217
+ temp_video_path = None
218
+
219
+ if cap_hr_mode == "Upload":
220
+ up_vid = st.file_uploader("Upload Video (mp4/avi/mov)", type=["mp4", "avi", "mov"])
221
+ if up_vid is not None:
222
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmpf:
223
+ tmpf.write(up_vid.read())
224
+ temp_video_path = tmpf.name
225
+ st.video(temp_video_path)
226
  else:
227
+ st.markdown("**Live Camera (WebRTC)** — Click **Start Recording**, wait 20–30s, then **Stop & Use**.")
228
+ if "webrtc_ctx" not in st.session_state:
229
+ st.session_state.webrtc_ctx = None
230
+ st.session_state.hr_processor = HRCollectorVideoProcessor()
231
+ st.session_state.is_recording = False
232
+ st.session_state.record_start = 0.0
233
+
234
+ # Start WebRTC streamer
235
+ ctx = webrtc_streamer(
236
+ key="hr-webrtc",
237
+ mode=WebRtcMode.SENDRECV,
238
+ rtc_configuration=RTC_CONFIGURATION,
239
+ media_stream_constraints={"video": True, "audio": False},
240
+ video_processor_factory=lambda: st.session_state.hr_processor,
241
+ )
242
+
243
+ col_btn1, col_btn2, col_btn3 = st.columns([1,1,2])
244
+ with col_btn1:
245
+ if st.button("Start Recording", disabled=not ctx.state.playing or st.session_state.is_recording):
246
+ st.session_state.hr_processor.start()
247
+ st.session_state.is_recording = True
248
+ st.session_state.record_start = time.time()
249
+ with col_btn2:
250
+ if st.button("Stop & Use", disabled=not st.session_state.is_recording):
251
+ dump_path = st.session_state.hr_processor.stop_and_dump_to_file()
252
+ st.session_state.is_recording = False
253
+ if dump_path and os.path.exists(dump_path):
254
+ temp_video_path = dump_path
255
+ st.success("Video captured.")
256
+ st.video(temp_video_path)
257
+ else:
258
+ st.warning("Captured video too short. Please record ~20–30 seconds.")
259
+
260
+ if st.session_state.is_recording:
261
+ elapsed = time.time() - st.session_state.record_start
262
+ st.info(f"Recording… {int(elapsed)} s")
263
+
264
+ # ==== Run Analysis ====
265
+ run = st.button("Run Analysis", type="primary", use_container_width=True)
266
+
267
+ def init_results_table() -> List[Dict[str, Any]]:
268
+ rows = []
269
+ for i, test in enumerate(TESTS, start=1):
270
+ rows.append({
271
+ "No.": i,
272
+ "Test": test,
273
+ "Result": "—",
274
+ "Unit": UNITS.get(test, ""),
275
+ "Status": "Pending"
276
+ })
277
+ return rows
278
+
279
+ if "results_rows" not in st.session_state:
280
+ st.session_state.results_rows = init_results_table()
281
+
282
+ if run:
283
+ # Reset results table each run
284
+ st.session_state.results_rows = init_results_table()
285
+
286
+ # ---- Hemoglobin from image ----
287
+ hb_value = predict_hemoglobin_from_image(img_np_bgr)
288
+ for row in st.session_state.results_rows:
289
+ if row["Test"] == "Hemoglobin (Hb)":
290
+ if hb_value is None:
291
+ row["Result"] = "No image"
292
+ row["Status"] = "No input"
293
+ else:
294
+ row["Result"] = f"{hb_value:.2f}"
295
+ row["Status"] = "OK"
296
+ break
297
+
298
+ # ---- Heart Rate from video ----
299
+ hr_value = estimate_hr_from_video_file(temp_video_path) if temp_video_path else None
300
+ for row in st.session_state.results_rows:
301
+ if row["Test"] == "Heart Rate (HR)":
302
+ if hr_value is None:
303
+ row["Result"] = "No video"
304
+ row["Status"] = "No input"
305
+ else:
306
+ row["Result"] = f"{hr_value:.1f}"
307
+ row["Status"] = "OK"
308
+ break
309
+
310
+ # ==== Display Results Table ====
311
+ st.subheader("🩺 Test Results (27)")
312
+ df = pd.DataFrame(st.session_state.results_rows, columns=["No.", "Test", "Result", "Unit", "Status"])
313
+ st.dataframe(df, use_container_width=True, hide_index=True)
314
+
315
+ st.caption(
316
+ "Note: Only Hb and HR have placeholder logic today. "
317
+ "As we build each test, swap in real model inference calls while keeping this UI and table."
318
+ )