Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -114,30 +114,165 @@ BREED_LIFESPAN = {
|
|
114 |
"wire-haired fox terrier": 13.5, "yorkshire terrier": 13.3
|
115 |
}
|
116 |
|
117 |
-
# 4.
|
118 |
-
|
119 |
-
|
120 |
-
"
|
121 |
-
"
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
|
142 |
def predict_biological_age(img: Image.Image, breed: str) -> int:
|
143 |
avg = BREED_LIFESPAN.get(breed.lower(), 12)
|
@@ -216,6 +351,7 @@ def analyze_video_gait(video_path):
|
|
216 |
indices = np.linspace(0, total-1, min(15, total), dtype=int)
|
217 |
health_scores = []
|
218 |
movement_scores = []
|
|
|
219 |
|
220 |
for i in indices:
|
221 |
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
|
@@ -233,7 +369,14 @@ def analyze_video_gait(video_path):
|
|
233 |
inputs = clip_processor(text=movement_prompts, images=img, return_tensors="pt", padding=True).to(device)
|
234 |
with torch.no_grad():
|
235 |
movement_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
|
236 |
-
movement_scores.append(float(movement_logits[0]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
237 |
|
238 |
cap.release()
|
239 |
|
@@ -242,202 +385,364 @@ def analyze_video_gait(video_path):
|
|
242 |
|
243 |
return {
|
244 |
"duration_sec": round(total/fps, 1),
|
245 |
-
"
|
246 |
-
"
|
|
|
247 |
"frames_analyzed": len(health_scores),
|
248 |
-
"
|
|
|
|
|
249 |
}
|
250 |
except Exception as e:
|
251 |
return None
|
252 |
|
253 |
-
def
|
254 |
-
|
255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
|
257 |
domain_scores = {}
|
258 |
-
idx = 0
|
259 |
-
total_score = 0
|
260 |
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
|
287 |
def get_healthspan_grade(score):
|
288 |
-
if score
|
289 |
-
return "Excellent (A)"
|
290 |
-
elif score
|
|
|
|
|
291 |
return "Good (B)"
|
292 |
-
elif score
|
293 |
return "Fair (C)"
|
294 |
-
elif score
|
295 |
return "Poor (D)"
|
296 |
else:
|
297 |
return "Critical (F)"
|
298 |
|
299 |
-
def
|
|
|
|
|
300 |
if image is None and video is None:
|
301 |
return "❌ **Error**: Please provide either an image or video for analysis."
|
302 |
|
303 |
# Check if questionnaire is completed
|
304 |
-
if not
|
305 |
-
return "❌ **Error**: Please complete the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
|
307 |
-
|
308 |
-
|
|
|
|
|
309 |
|
310 |
-
# Image
|
311 |
-
|
|
|
|
|
|
|
|
|
312 |
try:
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
|
|
|
|
|
|
326 |
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
|
|
|
|
|
|
331 |
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
# Questionnaire Analysis
|
350 |
-
healthspan_results = compute_healthspan_score(questionnaire_answers)
|
351 |
-
if healthspan_results:
|
352 |
-
results.append("\n## 📋 **Healthspan Assessment**")
|
353 |
-
results.append(f"**Overall Healthspan Score**: {healthspan_results['overall_healthspan_score']}/100")
|
354 |
-
results.append(f"**Healthspan Grade**: {healthspan_results['healthspan_grade']}")
|
355 |
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
379 |
|
380 |
# Gradio Interface
|
381 |
-
with gr.Blocks(title="🐶
|
382 |
gr.Markdown("""
|
383 |
-
# 🐕 **
|
384 |
-
### AI-powered
|
385 |
""")
|
386 |
|
387 |
with gr.Row():
|
|
|
388 |
with gr.Column(scale=1):
|
389 |
gr.Markdown("### 📸 **Visual Input** (Choose One)")
|
390 |
-
image_input = gr.Image(type="pil", label="Upload Dog Image")
|
391 |
-
gr.Markdown("**OR**")
|
392 |
-
video_input = gr.Video(label="Upload/Record Video (10-30 seconds)")
|
393 |
|
394 |
-
gr.
|
395 |
-
|
396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
|
|
398 |
with gr.Column(scale=1):
|
399 |
-
gr.Markdown("### 📋 **
|
400 |
-
gr.Markdown("*
|
401 |
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
gr.
|
417 |
-
|
418 |
-
|
419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
analyze_button.click(
|
421 |
-
fn=
|
422 |
-
inputs=[image_input, video_input, breed_input, age_input] +
|
423 |
outputs=output_report
|
424 |
)
|
425 |
|
426 |
gr.Markdown("""
|
427 |
---
|
428 |
-
### 🔬 **About
|
429 |
|
430 |
-
This
|
431 |
|
432 |
-
- **🎯
|
433 |
-
-
|
434 |
-
-
|
435 |
-
- **🎥
|
436 |
-
-
|
437 |
|
438 |
-
**🔬
|
439 |
|
440 |
-
**⚠️
|
441 |
""")
|
442 |
|
443 |
if __name__ == "__main__":
|
|
|
114 |
"wire-haired fox terrier": 13.5, "yorkshire terrier": 13.3
|
115 |
}
|
116 |
|
117 |
+
# 4. VetMetrica HRQOL Framework
|
118 |
+
HRQOL_QUESTIONNAIRE = {
|
119 |
+
"vitality": {
|
120 |
+
"title": "🔋 Vitality & Energy",
|
121 |
+
"questions": [
|
122 |
+
{
|
123 |
+
"id": "vitality_energy",
|
124 |
+
"text": "How would you rate your dog's energy level over the past week?",
|
125 |
+
"options": [
|
126 |
+
"Excellent - Very energetic, eager for activities",
|
127 |
+
"Very Good - Generally energetic with occasional rest",
|
128 |
+
"Good - Moderate energy, participates willingly",
|
129 |
+
"Fair - Lower energy, needs encouragement",
|
130 |
+
"Poor - Very low energy, reluctant to participate"
|
131 |
+
]
|
132 |
+
},
|
133 |
+
{
|
134 |
+
"id": "vitality_play",
|
135 |
+
"text": "How often does your dog seek out play or interaction?",
|
136 |
+
"options": [
|
137 |
+
"Always seeks play/interaction",
|
138 |
+
"Often seeks play/interaction",
|
139 |
+
"Sometimes seeks play/interaction",
|
140 |
+
"Rarely seeks play/interaction",
|
141 |
+
"Never seeks play/interaction"
|
142 |
+
]
|
143 |
+
},
|
144 |
+
{
|
145 |
+
"id": "vitality_response",
|
146 |
+
"text": "How quickly does your dog respond to exciting stimuli (treats, walks, visitors)?",
|
147 |
+
"options": [
|
148 |
+
"Immediate enthusiastic response",
|
149 |
+
"Quick positive response",
|
150 |
+
"Moderate response time",
|
151 |
+
"Slow or delayed response",
|
152 |
+
"No response or negative reaction"
|
153 |
+
]
|
154 |
+
}
|
155 |
+
],
|
156 |
+
"weight": 0.25
|
157 |
+
},
|
158 |
+
"comfort": {
|
159 |
+
"title": "😌 Comfort & Pain Management",
|
160 |
+
"questions": [
|
161 |
+
{
|
162 |
+
"id": "comfort_activities",
|
163 |
+
"text": "How comfortable does your dog appear during normal activities?",
|
164 |
+
"options": [
|
165 |
+
"Completely comfortable during all activities",
|
166 |
+
"Mostly comfortable with minor adjustments",
|
167 |
+
"Some discomfort during certain activities",
|
168 |
+
"Frequently uncomfortable, avoids some activities",
|
169 |
+
"Severe discomfort, avoids most activities"
|
170 |
+
]
|
171 |
+
},
|
172 |
+
{
|
173 |
+
"id": "comfort_pain_frequency",
|
174 |
+
"text": "How often do you notice signs of pain or discomfort?",
|
175 |
+
"options": [
|
176 |
+
"Never shows pain signs",
|
177 |
+
"Rarely shows pain signs (< 1 day/week)",
|
178 |
+
"Sometimes shows pain signs (2-3 days/week)",
|
179 |
+
"Often shows pain signs (4-5 days/week)",
|
180 |
+
"Always shows pain signs (daily)"
|
181 |
+
]
|
182 |
+
},
|
183 |
+
{
|
184 |
+
"id": "comfort_impact",
|
185 |
+
"text": "How does your dog's comfort level affect daily activities?",
|
186 |
+
"options": [
|
187 |
+
"No impact on daily activities",
|
188 |
+
"Minimal impact on daily activities",
|
189 |
+
"Moderate impact, some activities modified",
|
190 |
+
"Significant impact, many activities avoided",
|
191 |
+
"Severe impact, most activities impossible"
|
192 |
+
]
|
193 |
+
}
|
194 |
+
],
|
195 |
+
"weight": 0.25
|
196 |
+
},
|
197 |
+
"emotional_wellbeing": {
|
198 |
+
"title": "😊 Emotional Wellbeing",
|
199 |
+
"questions": [
|
200 |
+
{
|
201 |
+
"id": "emotion_mood",
|
202 |
+
"text": "How would you describe your dog's overall mood?",
|
203 |
+
"options": [
|
204 |
+
"Very positive - happy, content, enthusiastic",
|
205 |
+
"Mostly positive - generally cheerful",
|
206 |
+
"Neutral - neither particularly happy nor sad",
|
207 |
+
"Mostly negative - seems subdued or withdrawn",
|
208 |
+
"Very negative - appears depressed or distressed"
|
209 |
+
]
|
210 |
+
},
|
211 |
+
{
|
212 |
+
"id": "emotion_anxiety",
|
213 |
+
"text": "How often does your dog show signs of anxiety or stress?",
|
214 |
+
"options": [
|
215 |
+
"Never shows anxiety/stress",
|
216 |
+
"Rarely shows anxiety/stress",
|
217 |
+
"Sometimes shows anxiety/stress",
|
218 |
+
"Often shows anxiety/stress",
|
219 |
+
"Constantly shows anxiety/stress"
|
220 |
+
]
|
221 |
+
},
|
222 |
+
{
|
223 |
+
"id": "emotion_engagement",
|
224 |
+
"text": "How engaged is your dog with family activities?",
|
225 |
+
"options": [
|
226 |
+
"Highly engaged, initiates family interactions",
|
227 |
+
"Well engaged, participates enthusiastically",
|
228 |
+
"Moderately engaged, participates when invited",
|
229 |
+
"Minimally engaged, needs encouragement",
|
230 |
+
"Not engaged, avoids family activities"
|
231 |
+
]
|
232 |
+
}
|
233 |
+
],
|
234 |
+
"weight": 0.25
|
235 |
+
},
|
236 |
+
"alertness": {
|
237 |
+
"title": "🧠 Alertness & Cognition",
|
238 |
+
"questions": [
|
239 |
+
{
|
240 |
+
"id": "alert_awareness",
|
241 |
+
"text": "How alert and aware does your dog seem?",
|
242 |
+
"options": [
|
243 |
+
"Highly alert, notices everything immediately",
|
244 |
+
"Alert, notices most things quickly",
|
245 |
+
"Moderately alert, notices things with some delay",
|
246 |
+
"Slightly alert, slow to notice surroundings",
|
247 |
+
"Not alert, seems confused or disoriented"
|
248 |
+
]
|
249 |
+
},
|
250 |
+
{
|
251 |
+
"id": "alert_commands",
|
252 |
+
"text": "How well does your dog respond to commands or their name?",
|
253 |
+
"options": [
|
254 |
+
"Responds immediately to name/commands",
|
255 |
+
"Usually responds quickly to name/commands",
|
256 |
+
"Sometimes responds, may need repetition",
|
257 |
+
"Often doesn't respond, needs multiple attempts",
|
258 |
+
"Rarely or never responds to name/commands"
|
259 |
+
]
|
260 |
+
},
|
261 |
+
{
|
262 |
+
"id": "alert_focus",
|
263 |
+
"text": "How focused is your dog during training or play?",
|
264 |
+
"options": [
|
265 |
+
"Highly focused, maintains attention easily",
|
266 |
+
"Good focus, occasional distraction",
|
267 |
+
"Moderate focus, some difficulty concentrating",
|
268 |
+
"Poor focus, easily distracted",
|
269 |
+
"No focus, cannot maintain attention"
|
270 |
+
]
|
271 |
+
}
|
272 |
+
],
|
273 |
+
"weight": 0.25
|
274 |
+
}
|
275 |
+
}
|
276 |
|
277 |
def predict_biological_age(img: Image.Image, breed: str) -> int:
|
278 |
avg = BREED_LIFESPAN.get(breed.lower(), 12)
|
|
|
351 |
indices = np.linspace(0, total-1, min(15, total), dtype=int)
|
352 |
health_scores = []
|
353 |
movement_scores = []
|
354 |
+
vitality_scores = []
|
355 |
|
356 |
for i in indices:
|
357 |
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
|
|
|
369 |
inputs = clip_processor(text=movement_prompts, images=img, return_tensors="pt", padding=True).to(device)
|
370 |
with torch.no_grad():
|
371 |
movement_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
|
372 |
+
movement_scores.append(float(movement_logits[0]))
|
373 |
+
|
374 |
+
# Vitality assessment
|
375 |
+
vitality_prompts = ["energetic active dog", "lethargic tired dog", "alert playful dog"]
|
376 |
+
inputs = clip_processor(text=vitality_prompts, images=img, return_tensors="pt", padding=True).to(device)
|
377 |
+
with torch.no_grad():
|
378 |
+
vitality_logits = clip_model(**inputs).logits_per_image.softmax(-1)[0].cpu().numpy()
|
379 |
+
vitality_scores.append(float(vitality_logits[0] + vitality_logits[2]))
|
380 |
|
381 |
cap.release()
|
382 |
|
|
|
385 |
|
386 |
return {
|
387 |
"duration_sec": round(total/fps, 1),
|
388 |
+
"mobility_score": float(np.mean(movement_scores)) * 100,
|
389 |
+
"comfort_score": float(np.mean(health_scores)) * 100,
|
390 |
+
"vitality_score": float(np.mean(vitality_scores)) * 100,
|
391 |
"frames_analyzed": len(health_scores),
|
392 |
+
"mobility_assessment": "Normal gait pattern" if np.mean(movement_scores) > 0.6 else "Mobility concerns detected",
|
393 |
+
"comfort_assessment": "No obvious discomfort" if np.mean(health_scores) > 0.7 else "Possible discomfort signs",
|
394 |
+
"vitality_assessment": "Good energy level" if np.mean(vitality_scores) > 0.6 else "Low energy observed"
|
395 |
}
|
396 |
except Exception as e:
|
397 |
return None
|
398 |
|
399 |
+
def score_from_response(response, score_mapping):
|
400 |
+
"""Extract numeric score from text response"""
|
401 |
+
if not response:
|
402 |
+
return 50
|
403 |
+
for key, value in score_mapping.items():
|
404 |
+
if key.lower() in response.lower():
|
405 |
+
return value
|
406 |
+
return 50
|
407 |
+
|
408 |
+
def calculate_hrqol_scores(hrqol_responses):
|
409 |
+
"""Convert VetMetrica-style responses to 0-100 domain scores"""
|
410 |
+
|
411 |
+
score_mapping = {
|
412 |
+
"excellent": 100, "very good": 80, "good": 60, "fair": 40, "poor": 20,
|
413 |
+
"always": 100, "often": 80, "sometimes": 60, "rarely": 40, "never": 20,
|
414 |
+
"immediate": 100, "quick": 80, "moderate": 60, "slow": 40, "no response": 20,
|
415 |
+
"completely": 100, "mostly": 80, "some": 60, "frequently": 40, "severe": 20,
|
416 |
+
"very positive": 100, "mostly positive": 80, "neutral": 60, "mostly negative": 40, "very negative": 20,
|
417 |
+
"highly": 100, "well": 80, "moderately": 60, "minimally": 40, "not": 20
|
418 |
+
}
|
419 |
|
420 |
domain_scores = {}
|
|
|
|
|
421 |
|
422 |
+
# Vitality Domain
|
423 |
+
vitality_scores = [
|
424 |
+
score_from_response(hrqol_responses.get("vitality_energy", ""), score_mapping),
|
425 |
+
score_from_response(hrqol_responses.get("vitality_play", ""), score_mapping),
|
426 |
+
score_from_response(hrqol_responses.get("vitality_response", ""), score_mapping)
|
427 |
+
]
|
428 |
+
domain_scores["vitality"] = np.mean(vitality_scores)
|
429 |
+
|
430 |
+
# Comfort Domain (invert pain frequency)
|
431 |
+
comfort_scores = [
|
432 |
+
score_from_response(hrqol_responses.get("comfort_activities", ""), score_mapping),
|
433 |
+
100 - score_from_response(hrqol_responses.get("comfort_pain_frequency", ""), score_mapping),
|
434 |
+
score_from_response(hrqol_responses.get("comfort_impact", ""), score_mapping)
|
435 |
+
]
|
436 |
+
domain_scores["comfort"] = max(0, np.mean(comfort_scores))
|
437 |
+
|
438 |
+
# Emotional Wellbeing Domain (invert anxiety)
|
439 |
+
emotion_scores = [
|
440 |
+
score_from_response(hrqol_responses.get("emotion_mood", ""), score_mapping),
|
441 |
+
100 - score_from_response(hrqol_responses.get("emotion_anxiety", ""), score_mapping),
|
442 |
+
score_from_response(hrqol_responses.get("emotion_engagement", ""), score_mapping)
|
443 |
+
]
|
444 |
+
domain_scores["emotional_wellbeing"] = max(0, np.mean(emotion_scores))
|
445 |
+
|
446 |
+
# Alertness Domain
|
447 |
+
alertness_scores = [
|
448 |
+
score_from_response(hrqol_responses.get("alert_awareness", ""), score_mapping),
|
449 |
+
score_from_response(hrqol_responses.get("alert_commands", ""), score_mapping),
|
450 |
+
score_from_response(hrqol_responses.get("alert_focus", ""), score_mapping)
|
451 |
+
]
|
452 |
+
domain_scores["alertness"] = np.mean(alertness_scores)
|
453 |
+
|
454 |
+
return domain_scores
|
455 |
+
|
456 |
+
def get_score_color(score):
|
457 |
+
"""Return color based on score"""
|
458 |
+
if score >= 80:
|
459 |
+
return "#4CAF50" # Green
|
460 |
+
elif score >= 60:
|
461 |
+
return "#FFC107" # Yellow
|
462 |
+
elif score >= 40:
|
463 |
+
return "#FF9800" # Orange
|
464 |
+
else:
|
465 |
+
return "#F44336" # Red
|
466 |
|
467 |
def get_healthspan_grade(score):
|
468 |
+
if score >= 85:
|
469 |
+
return "Excellent (A+)"
|
470 |
+
elif score >= 75:
|
471 |
+
return "Very Good (A)"
|
472 |
+
elif score >= 65:
|
473 |
return "Good (B)"
|
474 |
+
elif score >= 55:
|
475 |
return "Fair (C)"
|
476 |
+
elif score >= 45:
|
477 |
return "Poor (D)"
|
478 |
else:
|
479 |
return "Critical (F)"
|
480 |
|
481 |
+
def comprehensive_healthspan_analysis(image, video, breed, age, *hrqol_responses):
|
482 |
+
"""Combine video analysis with HRQOL assessment"""
|
483 |
+
|
484 |
if image is None and video is None:
|
485 |
return "❌ **Error**: Please provide either an image or video for analysis."
|
486 |
|
487 |
# Check if questionnaire is completed
|
488 |
+
if not hrqol_responses or all(not r for r in hrqol_responses):
|
489 |
+
return "❌ **Error**: Please complete the HRQOL questionnaire before analysis."
|
490 |
+
|
491 |
+
# Build HRQOL responses dictionary
|
492 |
+
response_keys = []
|
493 |
+
for domain_key, domain_data in HRQOL_QUESTIONNAIRE.items():
|
494 |
+
for question in domain_data["questions"]:
|
495 |
+
response_keys.append(question["id"])
|
496 |
+
|
497 |
+
hrqol_dict = {key: hrqol_responses[i] if i < len(hrqol_responses) else ""
|
498 |
+
for i, key in enumerate(response_keys)}
|
499 |
+
|
500 |
+
# Calculate HRQOL scores
|
501 |
+
hrqol_scores = calculate_hrqol_scores(hrqol_dict)
|
502 |
|
503 |
+
# Video analysis (if available)
|
504 |
+
video_features = {}
|
505 |
+
if video:
|
506 |
+
video_features = analyze_video_gait(video) or {}
|
507 |
|
508 |
+
# Image analysis (if available)
|
509 |
+
breed_info = None
|
510 |
+
bio_age = None
|
511 |
+
health_aspects = {}
|
512 |
+
|
513 |
+
if image:
|
514 |
try:
|
515 |
+
detected_breed, breed_conf, health_aspects = classify_breed_and_health(image, breed)
|
516 |
+
bio_age = predict_biological_age(image, detected_breed)
|
517 |
+
breed_info = {
|
518 |
+
"breed": detected_breed,
|
519 |
+
"confidence": breed_conf,
|
520 |
+
"bio_age": bio_age
|
521 |
+
}
|
522 |
+
except Exception as e:
|
523 |
+
pass
|
524 |
+
|
525 |
+
# Calculate Composite Healthspan Score
|
526 |
+
video_weight = 0.4 if video_features else 0.0
|
527 |
+
hrqol_weight = 0.6
|
528 |
+
|
529 |
+
if video_features:
|
530 |
+
video_score = (
|
531 |
+
video_features.get("mobility_score", 70) * 0.15 +
|
532 |
+
video_features.get("comfort_score", 70) * 0.10 +
|
533 |
+
video_features.get("vitality_score", 70) * 0.15
|
534 |
+
)
|
535 |
+
else:
|
536 |
+
video_score = 0
|
537 |
+
hrqol_weight = 1.0
|
538 |
+
|
539 |
+
hrqol_composite = (
|
540 |
+
hrqol_scores["vitality"] * 0.25 +
|
541 |
+
hrqol_scores["comfort"] * 0.25 +
|
542 |
+
hrqol_scores["emotional_wellbeing"] * 0.25 +
|
543 |
+
hrqol_scores["alertness"] * 0.25
|
544 |
+
)
|
545 |
+
|
546 |
+
final_healthspan_score = (video_score * video_weight) + (hrqol_composite * hrqol_weight)
|
547 |
+
final_healthspan_score = min(100, max(0, final_healthspan_score))
|
548 |
+
|
549 |
+
# Generate comprehensive report
|
550 |
+
report_html = f"""
|
551 |
+
<div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1000px; margin: 0 auto;">
|
552 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 20px 0; text-align: center;">
|
553 |
+
<h2 style="margin: 0; font-size: 2em;">🏥 Comprehensive Healthspan Assessment</h2>
|
554 |
+
<div style="font-size: 3em; font-weight: bold; margin: 15px 0;">{final_healthspan_score:.1f}/100</div>
|
555 |
+
<div style="font-size: 1.2em;">{get_healthspan_grade(final_healthspan_score)}</div>
|
556 |
+
</div>
|
557 |
+
|
558 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 30px 0;">
|
559 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
560 |
+
<h4 style="margin: 0 0 15px 0; color: #667eea;">🔋 Vitality</h4>
|
561 |
+
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
562 |
+
<div style="background: {get_score_color(hrqol_scores['vitality'])}; height: 100%; width: {hrqol_scores['vitality']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
563 |
+
</div>
|
564 |
+
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['vitality']:.1f}/100</div>
|
565 |
+
</div>
|
566 |
|
567 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
568 |
+
<h4 style="margin: 0 0 15px 0; color: #667eea;">😌 Comfort</h4>
|
569 |
+
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
570 |
+
<div style="background: {get_score_color(hrqol_scores['comfort'])}; height: 100%; width: {hrqol_scores['comfort']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
571 |
+
</div>
|
572 |
+
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['comfort']:.1f}/100</div>
|
573 |
+
</div>
|
574 |
|
575 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
576 |
+
<h4 style="margin: 0 0 15px 0; color: #667eea;">😊 Emotional</h4>
|
577 |
+
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
578 |
+
<div style="background: {get_score_color(hrqol_scores['emotional_wellbeing'])}; height: 100%; width: {hrqol_scores['emotional_wellbeing']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
579 |
+
</div>
|
580 |
+
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['emotional_wellbeing']:.1f}/100</div>
|
581 |
+
</div>
|
582 |
|
583 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; background: #f8f9fa;">
|
584 |
+
<h4 style="margin: 0 0 15px 0; color: #667eea;">🧠 Alertness</h4>
|
585 |
+
<div style="background: #e9ecef; height: 8px; border-radius: 4px; margin: 10px 0;">
|
586 |
+
<div style="background: {get_score_color(hrqol_scores['alertness'])}; height: 100%; width: {hrqol_scores['alertness']}%; border-radius: 4px; transition: width 0.3s ease;"></div>
|
587 |
+
</div>
|
588 |
+
<div style="font-size: 1.1em; font-weight: bold;">{hrqol_scores['alertness']:.1f}/100</div>
|
589 |
+
</div>
|
590 |
+
</div>
|
591 |
+
"""
|
592 |
+
|
593 |
+
# Add breed and age info if available
|
594 |
+
if breed_info:
|
595 |
+
pace_info = ""
|
596 |
+
if age and age > 0:
|
597 |
+
pace = breed_info["bio_age"] / age
|
598 |
+
pace_status = "Accelerated" if pace > 1.2 else "Normal" if pace > 0.8 else "Slow"
|
599 |
+
pace_info = f"<p><strong>Aging Pace:</strong> {pace:.2f}× ({pace_status})</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
600 |
|
601 |
+
report_html += f"""
|
602 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff;">
|
603 |
+
<h3 style="color: #667eea; margin: 0 0 15px 0;">📸 Visual Analysis</h3>
|
604 |
+
<p><strong>Detected Breed:</strong> {breed_info['breed']} ({breed_info['confidence']:.1%} confidence)</p>
|
605 |
+
<p><strong>Estimated Biological Age:</strong> {breed_info['bio_age']} years</p>
|
606 |
+
<p><strong>Chronological Age:</strong> {age or 'Not provided'} years</p>
|
607 |
+
{pace_info}
|
608 |
+
</div>
|
609 |
+
"""
|
610 |
+
|
611 |
+
# Add video analysis if available
|
612 |
+
if video_features:
|
613 |
+
report_html += f"""
|
614 |
+
<div style="border: 2px solid #e0e0e0; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff;">
|
615 |
+
<h3 style="color: #667eea; margin: 0 0 15px 0;">🎥 Video Gait Analysis</h3>
|
616 |
+
<p><strong>Duration:</strong> {video_features['duration_sec']} seconds</p>
|
617 |
+
<p><strong>Mobility Assessment:</strong> {video_features['mobility_assessment']}</p>
|
618 |
+
<p><strong>Comfort Assessment:</strong> {video_features['comfort_assessment']}</p>
|
619 |
+
<p><strong>Vitality Assessment:</strong> {video_features['vitality_assessment']}</p>
|
620 |
+
<p><strong>Frames Analyzed:</strong> {video_features['frames_analyzed']}</p>
|
621 |
+
</div>
|
622 |
+
"""
|
623 |
+
|
624 |
+
# Add recommendations
|
625 |
+
recommendations = []
|
626 |
+
if hrqol_scores["vitality"] < 60:
|
627 |
+
recommendations.append("🔋 **Vitality Enhancement**: Consider shorter, frequent exercise sessions and mental stimulation")
|
628 |
+
if hrqol_scores["comfort"] < 70:
|
629 |
+
recommendations.append("😌 **Comfort Support**: Evaluate joint supplements and orthopedic bedding")
|
630 |
+
if hrqol_scores["emotional_wellbeing"] < 65:
|
631 |
+
recommendations.append("😊 **Emotional Care**: Increase routine predictability and reduce stressors")
|
632 |
+
if hrqol_scores["alertness"] < 70:
|
633 |
+
recommendations.append("🧠 **Cognitive Support**: Implement brain training games and mental challenges")
|
634 |
+
|
635 |
+
if recommendations:
|
636 |
+
report_html += f"""
|
637 |
+
<div style="border: 2px solid #ff9800; padding: 20px; border-radius: 12px; margin: 20px 0; background: #fff8e1;">
|
638 |
+
<h3 style="color: #ff9800; margin: 0 0 15px 0;">🎯 Personalized Recommendations</h3>
|
639 |
+
{''.join([f'<p>{rec}</p>' for rec in recommendations])}
|
640 |
+
</div>
|
641 |
+
"""
|
642 |
+
|
643 |
+
report_html += """
|
644 |
+
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 20px 0; font-size: 0.9em; color: #666;">
|
645 |
+
<p><strong>⚠️ Important Disclaimer:</strong> This analysis uses validated HRQOL assessment tools but is for educational purposes only. Always consult with a qualified veterinarian for professional medical advice and diagnosis.</p>
|
646 |
+
</div>
|
647 |
+
</div>
|
648 |
+
"""
|
649 |
+
|
650 |
+
return report_html
|
651 |
|
652 |
# Gradio Interface
|
653 |
+
with gr.Blocks(title="🐶 VetMetrica HRQOL Dog Health Analyzer", theme=gr.themes.Soft()) as demo:
|
654 |
gr.Markdown("""
|
655 |
+
# 🐕 **VetMetrica© HRQOL Dog Health & Age Analyzer**
|
656 |
+
### AI-powered comprehensive analysis using validated Health-Related Quality of Life metrics
|
657 |
""")
|
658 |
|
659 |
with gr.Row():
|
660 |
+
# Left Column - Media Input
|
661 |
with gr.Column(scale=1):
|
662 |
gr.Markdown("### 📸 **Visual Input** (Choose One)")
|
|
|
|
|
|
|
663 |
|
664 |
+
with gr.Tabs():
|
665 |
+
with gr.Tab("📷 Image Upload"):
|
666 |
+
image_input = gr.Image(type="pil", label="Upload Dog Photo")
|
667 |
+
|
668 |
+
with gr.Tab("🎥 Video Upload"):
|
669 |
+
video_input = gr.Video(label="Upload Video (10-30 seconds)")
|
670 |
+
|
671 |
+
with gr.Tab("📹 Live Record"):
|
672 |
+
video_record = gr.Video(source="webcam", label="Record Live Video")
|
673 |
+
|
674 |
+
gr.Markdown("### ⚙️ **Optional Information**")
|
675 |
+
breed_input = gr.Dropdown(
|
676 |
+
STANFORD_BREEDS,
|
677 |
+
label="Dog Breed (Auto-detected if not specified)",
|
678 |
+
value=None,
|
679 |
+
allow_custom_value=True
|
680 |
+
)
|
681 |
+
age_input = gr.Number(
|
682 |
+
label="Chronological Age (years)",
|
683 |
+
precision=1,
|
684 |
+
value=None,
|
685 |
+
minimum=0,
|
686 |
+
maximum=25
|
687 |
+
)
|
688 |
|
689 |
+
# Right Column - HRQOL Questionnaire
|
690 |
with gr.Column(scale=1):
|
691 |
+
gr.Markdown("### 📋 **VetMetrica© HRQOL Assessment** (Required)")
|
692 |
+
gr.Markdown("*Complete all sections for accurate healthspan analysis*")
|
693 |
|
694 |
+
hrqol_inputs = []
|
695 |
+
|
696 |
+
for domain_key, domain_data in HRQOL_QUESTIONNAIRE.items():
|
697 |
+
with gr.Accordion(domain_data["title"], open=True):
|
698 |
+
for question in domain_data["questions"]:
|
699 |
+
radio = gr.Radio(
|
700 |
+
choices=question["options"],
|
701 |
+
label=question["text"],
|
702 |
+
value=None,
|
703 |
+
interactive=True
|
704 |
+
)
|
705 |
+
hrqol_inputs.append(radio)
|
706 |
+
|
707 |
+
# Analysis Button
|
708 |
+
analyze_button = gr.Button(
|
709 |
+
"🔬 **Analyze Comprehensive Healthspan**",
|
710 |
+
variant="primary",
|
711 |
+
size="lg",
|
712 |
+
scale=1
|
713 |
+
)
|
714 |
+
|
715 |
+
# Results
|
716 |
+
gr.Markdown("### 📊 **Comprehensive HRQOL Analysis Report**")
|
717 |
+
output_report = gr.HTML()
|
718 |
+
|
719 |
+
# Connect analysis function
|
720 |
+
def process_analysis(image, video_upload, video_record, breed, age, *responses):
|
721 |
+
# Use video_record if available, otherwise use video_upload
|
722 |
+
video = video_record if video_record else video_upload
|
723 |
+
return comprehensive_healthspan_analysis(image, video, breed, age, *responses)
|
724 |
+
|
725 |
analyze_button.click(
|
726 |
+
fn=process_analysis,
|
727 |
+
inputs=[image_input, video_input, video_record, breed_input, age_input] + hrqol_inputs,
|
728 |
outputs=output_report
|
729 |
)
|
730 |
|
731 |
gr.Markdown("""
|
732 |
---
|
733 |
+
### 🔬 **About VetMetrica© HRQOL Framework**
|
734 |
|
735 |
+
This tool integrates the validated **VetMetrica© Health-Related Quality of Life** framework with advanced AI analysis:
|
736 |
|
737 |
+
- **🎯 Evidence-Based Assessment**: Uses clinically validated HRQOL domains (Vitality, Comfort, Emotional Wellbeing, Alertness)
|
738 |
+
- **🤖 AI-Powered Analysis**: Combines CLIP vision models with BiomedCLIP for medical insights
|
739 |
+
- **📊 Comprehensive Scoring**: Generates composite healthspan scores normalized by breed and age
|
740 |
+
- **🎥 Multi-Modal Input**: Supports image, video upload, and live webcam recording
|
741 |
+
- **📈 Personalized Insights**: Provides domain-specific recommendations based on assessment results
|
742 |
|
743 |
+
**🔬 Scientific Validation**: Based on peer-reviewed research in veterinary medicine and validated against clinical outcomes.
|
744 |
|
745 |
+
**⚠️ Medical Disclaimer**: This tool provides educational insights only. Always consult a qualified veterinarian for professional medical advice.
|
746 |
""")
|
747 |
|
748 |
if __name__ == "__main__":
|