Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import mediapipe as mp | |
| import tempfile | |
| import os | |
| import math | |
| # إعداد Mediapipe Pose | |
| mp_pose = mp.solutions.pose | |
| pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.8, model_complexity=2) | |
| def _dist(p1, p2): | |
| return math.hypot(p1[0]-p2[0], p1[1]-p2[1]) | |
| def _angle(a, b, c): | |
| try: | |
| a, b, c = np.array(a), np.array(b), np.array(c) | |
| ang = abs(math.degrees( | |
| math.atan2(c[1]-b[1], c[0]-b[0]) - math.atan2(a[1]-b[1], a[0]-b[0]) | |
| )) | |
| return ang if ang <= 180 else 360 - ang | |
| except: | |
| return 0.0 | |
| def _safe_mean(x): return float(np.mean(x)) if len(x) else 0.0 | |
| def _safe_std(x): return float(np.std(x)) if len(x) else 0.0 | |
| def _gauge_html(norm_score): | |
| pct = int(max(0, min(1, norm_score)) * 100) | |
| color = "#4caf50" if pct < 35 else "#fbc02d" if pct < 65 else "#c62828" | |
| return f""" | |
| <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ccc;"> | |
| <div style="width:{pct}%;height:100%;background:{color};transition:width .8s;"></div> | |
| </div> | |
| <div style="font-size:12px;color:#555;margin-top:6px;direction:rtl;text-align:right">درجة الخطورة: {pct}%</div> | |
| """ | |
| def analyze_gait(video_file): | |
| if video_file is None: | |
| return "<div style='direction:rtl;text-align:right'>❌ يرجى رفع فيديو أولًا.</div>", "<div></div>" | |
| if hasattr(video_file, "name"): | |
| video_path = video_file.name | |
| else: | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") | |
| with open(tmp.name, "wb") as f: f.write(video_file) | |
| video_path = tmp.name | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| return "<div style='direction:rtl;text-align:right'>❌ لا يمكن فتح الفيديو.</div>", "<div></div>" | |
| W, H = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| px2m = 1.7 / (H * 0.8) | |
| ground_y = H * 0.92 | |
| L_clear, R_clear, L_angle, R_angle = [], [], [], [] | |
| base_seq, tilt_seq, side_lean_seq = [], [], [] | |
| frames, person_detected = 0, False | |
| while cap.isOpened() and frames < 1000: | |
| ret, frame = cap.read() | |
| if not ret: break | |
| frames += 1 | |
| res = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) | |
| if not res.pose_landmarks: continue | |
| person_detected = True | |
| lm = res.pose_landmarks.landmark | |
| def xy(i): return [lm[i].x * W, lm[i].y * H] | |
| L_ank, R_ank = xy(27), xy(28) | |
| L_knee, R_knee = xy(25), xy(26) | |
| L_foot, R_foot = xy(31), xy(32) | |
| L_hip, R_hip = xy(23), xy(24) | |
| L_sh, R_sh = xy(11), xy(12) | |
| Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100) | |
| Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100) | |
| L_clear.append(Lc); R_clear.append(Rc) | |
| L_angle.append(_angle(L_knee, L_ank, L_foot)) | |
| R_angle.append(_angle(R_knee, R_ank, R_foot)) | |
| base_seq.append(abs(L_ank[0]-R_ank[0])) | |
| mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2] | |
| mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2] | |
| vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]]) | |
| tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6)))) | |
| tilt_seq.append(tilt) | |
| side_lean_seq.append(mid_sh[0]-mid_hip[0]) | |
| cap.release() | |
| try: os.unlink(video_path) | |
| except: pass | |
| if not person_detected: | |
| return "<div style='direction:rtl;text-align:right'>❌ لم يتم اكتشاف شخص في الفيديو. يُرجى إعادة التصوير بزاوية أمامية واضحة.</div>", "<div></div>" | |
| avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear) | |
| std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear) | |
| avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle) | |
| avg_tilt = _safe_mean(tilt_seq) | |
| avg_lean = _safe_mean(side_lean_seq) | |
| base_ratio = _safe_mean(base_seq)/W | |
| diff_clear = abs(avg_Lc - avg_Rc) | |
| diff_angle = abs(avg_La - avg_Ra) | |
| var_clear = max(std_Lc, std_Rc) | |
| sym_clear = abs((avg_Lc - avg_Rc) / (avg_Lc + avg_Rc + 1e-6)) * 100 | |
| sym_angle = abs((avg_La - avg_Ra) / (avg_La + avg_Ra + 1e-6)) * 100 | |
| low_ratio_L = np.mean(np.array(L_clear) < 4) | |
| low_ratio_R = np.mean(np.array(R_clear) < 4) | |
| score = 0 | |
| red_flags = 0 | |
| if min(avg_Lc, avg_Rc) < 4 or low_ratio_L > 0.4 or low_ratio_R > 0.4: | |
| score += 3.5; red_flags += 1 | |
| if var_clear > 8 or diff_angle > 15 or sym_angle > 18: | |
| score += 3; red_flags += 1 | |
| if base_ratio > 0.24 and avg_tilt > 8: | |
| score += 3.5; red_flags += 1 | |
| if sym_clear > 25: score += 1.5 | |
| if abs(avg_lean) > W*0.025: score += 1.5 | |
| if diff_clear > 5 and diff_angle > 10: score += 1.0 | |
| score = min(score, 10) | |
| norm_score = score / 10.0 | |
| # الجهة المتضررة | |
| if avg_Lc < avg_Rc - 2 and avg_La < avg_Ra: | |
| side = "الجهة اليسرى" | |
| elif avg_Rc < avg_Lc - 2 and avg_Ra < avg_La: | |
| side = "الجهة اليمنى" | |
| else: | |
| side = "غير محددة بوضوح" | |
| # الحالة المحتملة | |
| if red_flags >= 3 and base_ratio > 0.25: | |
| condition = "قدم شاركوت (Charcot Foot)" | |
| elif min(avg_Lc, avg_Rc) < 4 or low_ratio_L > 0.4 or low_ratio_R > 0.4: | |
| condition = "ضعف العضلة الظنبوبية (Foot Drop)" | |
| elif var_clear > 8 or sym_angle > 15: | |
| condition = "اعتلال الأعصاب المحيطية أو السكري" | |
| else: | |
| condition = "مشية طبيعية" | |
| # مستوى الخطورة + زر الحجز عند الحاجة | |
| if red_flags >= 2 or norm_score >= 0.7: | |
| level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات متعددة لخلل واضح في المشية." | |
| booking = """ | |
| <div style='margin-top:10px;direction:rtl;text-align:right'> | |
| <a href="https://example.com/book" target="_blank" | |
| style="background:#2e7d32;color:#fff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;"> | |
| احجز موعد مباشر (حضوري أو أونلاين) | |
| </a> | |
| </div>""" | |
| elif norm_score >= 0.45: | |
| level, color, desc = "🟡 متوسطة الخطورة", "#fbc02d", "مؤشرات تستدعي متابعة طبية دقيقة." | |
| booking = """ | |
| <div style='margin-top:10px;direction:rtl;text-align:right'> | |
| <a href="https://example.com/book" target="_blank" | |
| style="background:#2e7d32;color:#fff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;"> | |
| احجز استشارة طبية | |
| </a> | |
| </div>""" | |
| else: | |
| level, color, desc = "🟢 منخفضة الخطورة", "#2e7d32", "المشية ضمن النطاق السليم." | |
| booking = "" | |
| html = f""" | |
| <div style='direction:rtl;text-align:right;color:{color};font-weight:700;font-size:18px'>{level}</div> | |
| <div style='direction:rtl;text-align:right'> الجانب المتأثر: <b>{side}</b></div> | |
| <div style='direction:rtl;text-align:right'> الحالة المحتملة: <b>{condition}</b></div> | |
| <div style='direction:rtl;text-align:right'> درجة الخطورة: <b>{score:.1f}/10</b></div> | |
| <div style='direction:rtl;text-align:right'>{desc}</div> | |
| {booking} | |
| <div style='font-size:13px;color:#555;margin-top:8px;direction:rtl;text-align:right'> | |
| ⚠️ التحليل يعتمد على مؤشرات متعددة ولا يُغني عن الفحص الطبي.</div> | |
| """ | |
| return html, _gauge_html(norm_score) | |
| instructions = """ | |
| <div style='direction:rtl;text-align:right'> | |
| <h3> تعليمات التصوير لضمان دقة التحليل:</h3> | |
| <ol> | |
| <li>ضع الكاميرا على <b>بعد 2 إلى 3 أمتار</b> وارتفاع الركبة تقريبًا.</li> | |
| <li>استخدم <b>إضاءة أمامية قوية</b> وخلفية بسيطة.</li> | |
| <li>صوّر <b>من الأمام أو الجانب</b> مع ظهور الساقين بالكامل.</li> | |
| <li>اطلب من الشخص أن <b>يمشي بشكل طبيعي</b> لمسافة 3–5 أمتار لمدة 15–30 ثانية.</li> | |
| <li>تجنّب الملابس التي <b>تغطي الركبة أو الكاحل</b>.</li> | |
| <li>ثبّت الهاتف أثناء التصوير لتقليل الاهتزاز.</li> | |
| </ol> | |
| </div> | |
| """ | |
| # ✅ ثيم بلون أخضر كامل | |
| green_theme = gr.themes.Soft( | |
| primary_hue="green", | |
| secondary_hue="green", | |
| neutral_hue="gray" | |
| ) | |
| with gr.Blocks(title="تحليل المشية ", theme=green_theme) as demo: | |
| gr.HTML(instructions) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| video_in = gr.File(label="📂 اختر فيديو المشي", file_types=[".mp4", ".avi", ".mov"], type="binary") | |
| analyze_btn = gr.Button(" بدء التحليل", elem_id="green_btn") | |
| with gr.Column(scale=1): | |
| gauge = gr.HTML("<div></div>") | |
| out_html = gr.HTML("<i style='direction:rtl;text-align:right'>النتيجة ستظهر هنا بعد التحليل...</i>") | |
| analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge]) | |
| if __name__ == "__main__": | |
| demo.launch() | |