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"""
درجة الخطورة: {pct}%
"""
def analyze_gait(video_file):
if video_file is None:
return "❌ يرجى رفع فيديو أولًا.
", ""
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 "❌ لا يمكن فتح الفيديو.
", ""
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 "❌ لم يتم اكتشاف شخص في الفيديو. يُرجى إعادة التصوير بزاوية أمامية واضحة.
", ""
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 = """
"""
elif norm_score >= 0.45:
level, color, desc = "🟡 متوسطة الخطورة", "#fbc02d", "مؤشرات تستدعي متابعة طبية دقيقة."
booking = """
"""
else:
level, color, desc = "🟢 منخفضة الخطورة", "#2e7d32", "المشية ضمن النطاق السليم."
booking = ""
html = f"""
{level}
الجانب المتأثر: {side}
الحالة المحتملة: {condition}
درجة الخطورة: {score:.1f}/10
{desc}
{booking}
⚠️ التحليل يعتمد على مؤشرات متعددة ولا يُغني عن الفحص الطبي.
"""
return html, _gauge_html(norm_score)
instructions = """
تعليمات التصوير لضمان دقة التحليل:
- ضع الكاميرا على بعد 2 إلى 3 أمتار وارتفاع الركبة تقريبًا.
- استخدم إضاءة أمامية قوية وخلفية بسيطة.
- صوّر من الأمام أو الجانب مع ظهور الساقين بالكامل.
- اطلب من الشخص أن يمشي بشكل طبيعي لمسافة 3–5 أمتار لمدة 15–30 ثانية.
- تجنّب الملابس التي تغطي الركبة أو الكاحل.
- ثبّت الهاتف أثناء التصوير لتقليل الاهتزاز.
"""
# ✅ ثيم بلون أخضر كامل
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("")
out_html = gr.HTML("النتيجة ستظهر هنا بعد التحليل...")
analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
if __name__ == "__main__":
demo.launch()