EngReem85 commited on
Commit
18b1375
·
verified ·
1 Parent(s): fb93dd1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -204
app.py CHANGED
@@ -2,17 +2,19 @@ import gradio as gr
2
  import cv2
3
  import numpy as np
4
  import mediapipe as mp
5
- import tempfile, os, math
 
 
6
 
7
- # =========================
8
  # إعداد Mediapipe Pose
9
- # =========================
10
  mp_pose = mp.solutions.pose
11
  pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.75, model_complexity=2)
12
 
13
- # =========================
14
  # دوال مساعدة
15
- # =========================
16
  def _dist(p1, p2):
17
  return math.hypot(p1[0]-p2[0], p1[1]-p2[1])
18
 
@@ -26,291 +28,208 @@ def _angle(a, b, c):
26
  except:
27
  return 0.0
28
 
29
- def _safe_mean(x): return float(np.mean(x)) if len(x) else 0.0
30
- def _safe_std(x): return float(np.std(x)) if len(x) else 0.0
31
-
32
- def _symmetry_index(a_mean, b_mean, eps=1e-6):
33
- # Robinson/SI مبسّط: 2*(R-L)/(R+L) كنسبة مئوية
34
- return 100.0 * (2.0 * (a_mean - b_mean) / (a_mean + b_mean + eps))
35
-
36
- def _symmetry_angle(a_mean, b_mean, eps=1e-6):
37
- # Zifchock/SA مبسّطة: تحويل تماثل لنطاق زاوي
38
- r = (a_mean + eps) / (b_mean + eps)
39
- return abs(45.0 * (r - 1) / (r + 1))
40
-
41
- def _norm01(x, lo, hi):
42
- return max(0.0, min(1.0, (x - lo) / (hi - lo + 1e-6)))
43
 
44
  def _gauge_html(norm_score):
45
  pct = int(max(0, min(1, norm_score)) * 100)
46
  color = "#4caf50" if pct < 35 else "#fbc02d" if pct < 65 else "#c62828"
47
- return f"""
48
- <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ccc;">
49
- <div style="width:{pct}%;height:100%;background:{color};transition:width .8s;"></div>
50
  </div>
51
- <div style="font-size:12px;color:#555;margin-top:6px">درجة الخطورة: {pct}%</div>
52
  """
 
53
 
54
- # =========================
55
- # التحليل الرئيسي
56
- # =========================
57
  def analyze_gait(video_file):
58
  if video_file is None:
59
- return "<div>❌ يرجى رفع فيديو أولًا.</div>", "<div></div>"
60
 
61
  # حفظ الفيديو مؤقتًا
62
  if hasattr(video_file, "name"):
63
  video_path = video_file.name
64
  else:
65
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
66
- with open(tmp.name, "wb") as f: f.write(video_file)
 
67
  video_path = tmp.name
68
 
69
  cap = cv2.VideoCapture(video_path)
70
  if not cap.isOpened():
71
- return "<div>❌ لا يمكن فتح الفيديو.</div>", "<div></div>"
72
 
 
 
73
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
74
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
75
- fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
76
- px2m = 1.7 / (H * 0.8) # تطبيع تقريبي للطول
77
- ground_y = H * 0.92
78
 
79
- # سلاسل زمنية
80
  L_clear, R_clear = [], []
81
- L_ang, R_ang = [], []
82
- base_px_seq, torso_tilt_seq, torso_side_seq = [], [], []
83
-
84
- # طاقة الحركة (للاستقرار العام)
85
- motion_energy = []
86
- prev_gray = None
87
-
88
- frames = 0
89
- detected = False
90
 
91
- while cap.isOpened() and frames < 1400:
92
  ret, frame = cap.read()
93
  if not ret: break
94
- frames += 1
95
-
96
- g = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
97
- if prev_gray is not None:
98
- diff = cv2.absdiff(g, prev_gray)
99
- motion_energy.append(float(np.mean(diff)))
100
- prev_gray = g
101
 
102
- res = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
103
- if not res.pose_landmarks:
104
- continue
105
- detected = True
106
  lm = res.pose_landmarks.landmark
107
-
108
- def xy(i): return [lm[i].x*W, lm[i].y*H]
109
-
110
- # نقاط حرجة (وفق Mediapipe Pose Landmarks)
111
- L_ank, R_ank = xy( mp_pose.PoseLandmark.LEFT_ANKLE.value ), xy( mp_pose.PoseLandmark.RIGHT_ANKLE.value )
112
- L_foot,R_foot= xy( mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value),xy( mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value)
113
- L_knee,R_knee= xy( mp_pose.PoseLandmark.LEFT_KNEE.value ), xy( mp_pose.PoseLandmark.RIGHT_KNEE.value )
114
- L_hip, R_hip = xy( mp_pose.PoseLandmark.LEFT_HIP.value ), xy( mp_pose.PoseLandmark.RIGHT_HIP.value )
115
- L_sh, R_sh = xy( mp_pose.PoseLandmark.LEFT_SHOULDER.value ),xy( mp_pose.PoseLandmark.RIGHT_SHOULDER.value )
116
-
117
- # ارتفاع القدم (سم): أقرب نقطة (كاحل/مقدمة القدم) إلى الأرض
 
 
118
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
119
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
120
- L_clear.append(Lc); R_clear.append(Rc)
121
-
122
- # زاوية الكاحل
123
  La = _angle(L_knee, L_ank, L_foot)
124
  Ra = _angle(R_knee, R_ank, R_foot)
125
- L_ang.append(La); R_ang.append(Ra)
126
-
127
- # قاعدة القدمين (بكسل) + تقدير الميل
128
  base_px_seq.append(abs(L_ank[0]-R_ank[0]))
 
 
129
  mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2]
130
  mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2]
131
  vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]])
132
  tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6))))
133
  torso_tilt_seq.append(tilt)
134
- torso_side_seq.append(mid_sh[0]-mid_hip[0])
135
 
136
  cap.release()
137
  try: os.unlink(video_path)
138
  except: pass
139
 
140
- if not detected or frames < 30:
141
- return "<div>❌ لم يتم التقاط معالم كافية. يرجى إعادة التصوير وفق التعليمات أدناه.</div>", "<div></div>"
142
 
143
- # =========================
144
- # إحصاءات أساسية
145
- # =========================
146
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
147
- std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
148
- avg_La, avg_Ra = _safe_mean(L_ang), _safe_mean(R_ang)
 
 
 
149
  var_clear = max(std_Lc, std_Rc)
150
- diff_clear= abs(avg_Lc - avg_Rc)
151
- diff_angle= abs(avg_La - avg_Ra)
152
- base_ratio= _safe_mean(base_px_seq)/max(1,W)
153
- avg_tilt = _safe_mean(torso_tilt_seq)
154
- side_lean = _safe_mean(torso_side_seq)
155
- motion_cv = (np.std(motion_energy)/ (np.mean(motion_energy)+1e-6)) if len(motion_energy) else 0.0
156
-
157
- # عرض أمامي/جانبي (مع ترجيح للأمامي كما طلبتِ)
158
- view = "frontal" if base_ratio > 0.15 else "side"
159
-
160
- # =========================
161
- # مؤشرات زمنية متقدمة
162
- # =========================
163
- Lc_arr = np.array(L_clear, dtype=float)
164
- Rc_arr = np.array(R_clear, dtype=float)
165
-
166
- # 1) نسبة زمن انخفاض الارتفاع لكل قدم
167
- low_thr = 3.5 # سم
168
- ratio_low_L = float(np.mean(Lc_arr < low_thr))
169
- ratio_low_R = float(np.mean(Rc_arr < low_thr))
170
-
171
- # 2) تأخر زمني بين القدمين (cross-correlation) كدليل Foot Drop
172
- # نستخدم الإزاحة التي تعظم الارتباط بين L_clear و R_clear
173
- def lag_cc(a, b, max_lag=15):
174
- if len(a) < 5 or len(b) < 5: return 0
175
- a = (a - np.mean(a)) / (np.std(a)+1e-6)
176
- b = (b - np.mean(b)) / (np.std(b)+1e-6)
177
- best_lag, best_cc = 0, -1
178
- for lag in range(-max_lag, max_lag+1):
179
- if lag < 0:
180
- cc = np.mean(a[:lag] * b[-lag:])
181
- elif lag > 0:
182
- cc = np.mean(a[lag:] * b[:-lag])
183
- else:
184
- cc = np.mean(a * b)
185
- if cc > best_cc:
186
- best_cc, best_lag = cc, lag
187
- return best_lag
188
- lag = lag_cc(Lc_arr, Rc_arr, max_lag=round(0.3*fps)) # ~0.3 ثانية كحد أقصى
189
-
190
- # 3) مؤشرات تماثل مبسّطة (SI/SA)
191
- si_clear = abs(_symmetry_index(avg_Lc, avg_Rc)) # كلما ارتفع كانت لا تماثل أكبر
192
- sa_angle = _symmetry_angle(avg_La, avg_Ra)
193
-
194
- # 4) عدم استقرار الجذع (تذبذب)
195
- torso_sway = _safe_std(torso_side_seq) / max(1.0, W) # نسبي لعرض الإطار
196
-
197
- # =========================
198
- # تصنيف صارم ومتوازن
199
- # =========================
200
- score = 0.0
201
- strong = 0
202
-
203
- # Foot Drop: انخفاض مستمر + تأخر زمني + زاوية منخفضة نسبياً
204
- fd_evidence = (min(avg_Lc, avg_Rc) < 3.5) or (ratio_low_L > 0.45 or ratio_low_R > 0.45)
205
- fd_delay = abs(lag)/max(1.0,fps) > 0.08 # تأخر > 80ms
206
- if (fd_evidence and fd_delay) or (fd_evidence and diff_angle < 25):
207
- score += 3.5; strong += 1
208
-
209
- # Neuropathy: تذبذب ارتفاع واضح + لا تماثل زاوي/زمني
210
- if (var_clear > 9 and sa_angle > 6) or (si_clear > 18 and motion_cv > 0.22):
211
- score += 3.0; strong += 1
212
-
213
- # Charcot: قاعدة أوسع + ميل جذعي ملحوظ (مفيد أكثر للأمامي)
214
- base_m = base_ratio * W * px2m
215
- if (view == "frontal" and base_m > 0.27 and (avg_tilt > 9 or torso_sway > 0.02)) or (base_m > 0.30):
216
- score += 3.5; strong += 1
217
-
218
- # عوامل داعمة
219
- if diff_clear > 6: score += 1.0
220
- if abs(side_lean) > W*0.03: score += 1.0
221
- if sa_angle > 8: score += 0.5
222
-
223
- # ترجيح للأمامي لأنك ذكرت أنه الأكثر شيوعاً
224
- if view == "frontal": score *= 1.12
225
-
226
- score = min(score, 10.0)
227
- norm_score = score / 10.0
228
-
229
- # تحديد الجانب المتضرر (تجميعي)
230
- # يعتمد على: انخفاض المتوسط + طول زمن الانخفاض + إشارة التأخر الزمني
231
- left_weight = (avg_Rc - avg_Lc) + 20*(ratio_low_L - ratio_low_R) + (1 if lag > 0 else 0) # lag>0 يعني L يتأخر
232
- right_weight = (avg_Lc - avg_Rc) + 20*(ratio_low_R - ratio_low_L) + (1 if lag < 0 else 0)
233
-
234
- if left_weight > right_weight + 3:
235
  side = "اليسار"
236
- elif right_weight > left_weight + 3:
237
  side = "اليمين"
238
  else:
239
- side = "غير محدد بوضوح"
240
 
241
- # قرار نهائي (أكثر صرامة)
242
- if norm_score >= 0.68 or strong >= 2:
243
- level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات زمنية ومكانية قوية لخلل في المشية."
244
  booking_html = """
245
- <div style="margin-top:10px">
246
  <a href="https://example.com/book" target="_blank"
247
- style="background:#007bff;color:#fff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;">
248
  احجز موعد مباشر (حضوري أو أونلاين)
249
  </a>
250
  </div>
251
  """
252
- elif norm_score >= 0.48:
253
- level, color, desc = "🟡 متوسطة الخطورة", "#f9a825", "مؤشرات ملحوظة تستدعي متابعة طبية وقائية."
254
  booking_html = """
255
- <div style="margin-top:10px">
256
  <a href="https://example.com/book" target="_blank"
257
- style="background:#fbc02d;color:#000;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;">
258
  احجز استشارة طبية
259
  </a>
260
  </div>
261
  """
262
  else:
263
- level, color, desc = "🟢 طبيعية", "#2e7d32", "المشية ضمن الحدود الطبيعية بحسب المعطيات الزمنية والمكانية."
264
  booking_html = ""
265
 
266
- # ترجيح الحالة
267
- if strong >= 2 and base_m > 0.27:
268
  condition = "قدم شاركوت (Charcot Foot)"
269
- elif fd_evidence:
270
  condition = "ضعف العضلة الظنبوبية (Foot Drop)"
271
- elif (var_clear > 9 and sa_angle > 6) or (si_clear > 18 and motion_cv > 0.22):
272
  condition = "اعتلال الأعصاب المحيطية / السكري"
273
  else:
274
  condition = "خلل بسيط غير محدد"
275
 
276
  html = f"""
277
- <div style="color:{color};font-weight:700;font-size:18px">{level}</div>
278
- <div>👁️ زاوية التصوير: <b>{'أمامية' if view=='frontal' else 'جانبية'}</b></div>
279
- <div>📍 الجانب المتأثر: <b>{side}</b></div>
280
- <div>🩺 الحالة المحتملة: <b>{condition}</b></div>
281
- <div>📊 درجة المؤشرات: <b>{score:.1f}/10</b></div>
282
- <div>{desc}</div>
283
  {booking_html}
284
- <div style='font-size:13px;color:#555;margin-top:8px'>⚠️ التحليل تقديري يعتمد على معالم المشي والزمن ولا يُغني عن التشخيص السريري.</div>
285
  """
286
  return html, _gauge_html(norm_score)
287
 
288
- # =========================
289
- # تعليمات تصوير داخل الواجهة
290
- # =========================
291
  instructions = """
292
- ### 🎥 تعليمات تصوير دقيقة (لتحليل أوثق):
293
- 1) ضع الكاميرا على **بعد 2–3 م** من الشخص وعلى **ارتفاع الركبة تقريبًا**.
294
- 2) **إضاءة أمامية جيدة**، وخلفية بسيطة، وتجنّب الظلال القوية.
295
- 3) صوّر **زاوية أمامية واضحة** (أو جانبية إن تعذّر)، مع إظهار الجسم من **الورك حتى القدم** كاملين.
296
- 4) اطلب من الشخص **المشي بشكل طبيعي 3–5 أمتار ذهابًا وإيابًا** لمدة **15–30 ثانية**.
297
- 5) تجنّب الملابس الطويلة/الفضفاضة التي **تحجب الركبة والكاحل**.
298
- 6) ثبّت الهاتف (على حامل إن أمكن) لتقليل اهتزاز الكاميرا.
 
 
 
 
299
  """
300
 
301
- # =========================
302
- # واجهة Gradio
303
- # =========================
304
- with gr.Blocks(title="تحليل المشية العصبية - v10 (زمني/مكاني صارم)") as demo:
305
- gr.Markdown("## 🩺 نظام تحليل المشية العصبية – الإصدار v10")
306
- gr.Markdown(instructions)
307
  with gr.Row():
308
  with gr.Column(scale=1):
309
  video_in = gr.File(label="📂 اختر فيديو المشي", file_types=[".mp4", ".avi", ".mov"], type="binary")
310
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
311
  with gr.Column(scale=1):
312
  gauge = gr.HTML("<div></div>")
313
- out_html = gr.HTML("<i>النتيجة ستظهر هنا بعد التحليل...</i>")
314
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
315
 
316
  if __name__ == "__main__":
 
2
  import cv2
3
  import numpy as np
4
  import mediapipe as mp
5
+ import tempfile
6
+ import os
7
+ import math
8
 
9
+ # ===========================
10
  # إعداد Mediapipe Pose
11
+ # ===========================
12
  mp_pose = mp.solutions.pose
13
  pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.75, model_complexity=2)
14
 
15
+ # ===========================
16
  # دوال مساعدة
17
+ # ===========================
18
  def _dist(p1, p2):
19
  return math.hypot(p1[0]-p2[0], p1[1]-p2[1])
20
 
 
28
  except:
29
  return 0.0
30
 
31
+ def _safe_mean(x): return float(np.mean(x)) if x else 0.0
32
+ def _safe_std(x): return float(np.std(x)) if x else 0.0
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  def _gauge_html(norm_score):
35
  pct = int(max(0, min(1, norm_score)) * 100)
36
  color = "#4caf50" if pct < 35 else "#fbc02d" if pct < 65 else "#c62828"
37
+ bar = f"""
38
+ <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ddd;">
39
+ <div style="width:{pct}%;height:100%;background:{color};transition:width 1s;"></div>
40
  </div>
41
+ <div style="font-size:12px;color:#555;margin-top:6px;direction:rtl;text-align:right">درجة الخطورة: {pct}%</div>
42
  """
43
+ return bar
44
 
45
+ # ===========================
46
+ # التحليل الرئيسي للفيديو
47
+ # ===========================
48
  def analyze_gait(video_file):
49
  if video_file is None:
50
+ return "<div style='direction:rtl;text-align:right'>❌ يرجى رفع فيديو أولًا.</div>", "<div></div>"
51
 
52
  # حفظ الفيديو مؤقتًا
53
  if hasattr(video_file, "name"):
54
  video_path = video_file.name
55
  else:
56
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
57
+ with open(tmp.name, "wb") as f:
58
+ f.write(video_file)
59
  video_path = tmp.name
60
 
61
  cap = cv2.VideoCapture(video_path)
62
  if not cap.isOpened():
63
+ return "<div style='direction:rtl;text-align:right'>❌ لا يمكن فتح الفيديو.</div>", "<div></div>"
64
 
65
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
66
+ fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
67
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
68
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
69
+ px2m = 1.7 / (H * 0.8)
 
 
70
 
 
71
  L_clear, R_clear = [], []
72
+ L_angle, R_angle = [], []
73
+ base_px_seq = []
74
+ torso_tilt_seq, torso_side_lean_seq = [], []
75
+ ground_y = H * 0.92
76
+ frames_processed = 0
77
+ person_detected = False
 
 
 
78
 
79
+ while cap.isOpened() and frames_processed < min(1200, total_frames or 1200):
80
  ret, frame = cap.read()
81
  if not ret: break
82
+ frames_processed += 1
83
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
84
+ res = pose.process(frame_rgb)
85
+ if not res.pose_landmarks: continue
 
 
 
86
 
87
+ person_detected = True
 
 
 
88
  lm = res.pose_landmarks.landmark
89
+ def xy(idx): return [lm[idx].x * W, lm[idx].y * H]
90
+ L_ank = xy(mp_pose.PoseLandmark.LEFT_ANKLE.value)
91
+ R_ank = xy(mp_pose.PoseLandmark.RIGHT_ANKLE.value)
92
+ L_knee = xy(mp_pose.PoseLandmark.LEFT_KNEE.value)
93
+ R_knee = xy(mp_pose.PoseLandmark.RIGHT_KNEE.value)
94
+ L_foot = xy(mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value)
95
+ R_foot = xy(mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value)
96
+ L_hip = xy(mp_pose.PoseLandmark.LEFT_HIP.value)
97
+ R_hip = xy(mp_pose.PoseLandmark.RIGHT_HIP.value)
98
+ L_sh = xy(mp_pose.PoseLandmark.LEFT_SHOULDER.value)
99
+ R_sh = xy(mp_pose.PoseLandmark.RIGHT_SHOULDER.value)
100
+
101
+ # ارتفاع القدم بالسنتيمتر
102
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
103
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
104
+ L_clear.append(Lc)
105
+ R_clear.append(Rc)
 
106
  La = _angle(L_knee, L_ank, L_foot)
107
  Ra = _angle(R_knee, R_ank, R_foot)
108
+ L_angle.append(La)
109
+ R_angle.append(Ra)
 
110
  base_px_seq.append(abs(L_ank[0]-R_ank[0]))
111
+
112
+ # ميل الجذع
113
  mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2]
114
  mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2]
115
  vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]])
116
  tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6))))
117
  torso_tilt_seq.append(tilt)
118
+ torso_side_lean_seq.append(mid_sh[0]-mid_hip[0])
119
 
120
  cap.release()
121
  try: os.unlink(video_path)
122
  except: pass
123
 
124
+ if not person_detected:
125
+ return "<div style='direction:rtl;text-align:right'>❌ لم يتم اكتشاف شخص في الفيديو. يُفضّل تصوير جانبي أو أمامي واضح وإضاءة جيدة.</div>", "<div></div>"
126
 
 
 
 
127
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
128
+ std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
129
+ avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle)
130
+ avg_base_px = _safe_mean(base_px_seq)
131
+ avg_tilt = _safe_mean(torso_tilt_seq)
132
+ avg_side_lean = _safe_mean(torso_side_lean_seq)
133
  var_clear = max(std_Lc, std_Rc)
134
+ diff_clear = abs(avg_Lc - avg_Rc)
135
+ diff_angle = abs(avg_La - avg_Ra)
136
+ px_base_ratio = avg_base_px / W
137
+ view = "frontal" if px_base_ratio > 0.14 else "side"
138
+ low_ratio_L = sum(np.array(L_clear)<3.5)/len(L_clear)
139
+ low_ratio_R = sum(np.array(R_clear)<3.5)/len(R_clear)
140
+ score = 0
141
+ strong_flags = 0
142
+
143
+ if (min(avg_Lc, avg_Rc)<3.5) or (low_ratio_L>0.4 or low_ratio_R>0.4):
144
+ score += 3.5; strong_flags += 1
145
+ if (var_clear>9 and diff_angle>15) or (diff_clear>6 and var_clear>8):
146
+ score += 3; strong_flags += 1
147
+ if (px_base_ratio>0.25 and avg_tilt>10):
148
+ score += 3.5; strong_flags += 1
149
+ if abs(avg_side_lean) > W*0.03: score += 1.5
150
+ if diff_clear > 7: score += 1.5
151
+ score = min(score, 10)
152
+ norm_score = score / 10
153
+
154
+ if avg_Lc < avg_Rc-2.5 and avg_La < avg_Ra:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  side = "اليسار"
156
+ elif avg_Rc < avg_Lc-2.5 and avg_Ra < avg_La:
157
  side = "اليمين"
158
  else:
159
+ side = "غير محدد"
160
 
161
+ if norm_score >= 0.7 or strong_flags >= 2:
162
+ level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات قوية لخلل في المشية."
 
163
  booking_html = """
164
+ <div style='margin-top:10px;direction:rtl;text-align:right'>
165
  <a href="https://example.com/book" target="_blank"
166
+ style="background:#007bff;color:#fff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;display:inline-block;">
167
  احجز موعد مباشر (حضوري أو أونلاين)
168
  </a>
169
  </div>
170
  """
171
+ elif norm_score >= 0.45:
172
+ level, color, desc = "🟡 متوسطة الخطورة", "#fbc02d", "مؤشرات تستدعي متابعة طبية."
173
  booking_html = """
174
+ <div style='margin-top:10px;direction:rtl;text-align:right'>
175
  <a href="https://example.com/book" target="_blank"
176
+ style="background:#fbc02d;color:#000;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:600;display:inline-block;">
177
  احجز استشارة طبية
178
  </a>
179
  </div>
180
  """
181
  else:
182
+ level, color, desc = "🟢 طبيعية", "#2e7d32", "المشية ضمن الحدود الطبيعية."
183
  booking_html = ""
184
 
185
+ if strong_flags >= 2 and px_base_ratio>0.25:
 
186
  condition = "قدم شاركوت (Charcot Foot)"
187
+ elif low_ratio_L>0.4 or low_ratio_R>0.4:
188
  condition = "ضعف العضلة الظنبوبية (Foot Drop)"
189
+ elif (var_clear>8 and diff_angle>15):
190
  condition = "اعتلال الأعصاب المحيطية / السكري"
191
  else:
192
  condition = "خلل بسيط غير محدد"
193
 
194
  html = f"""
195
+ <div style='direction:rtl;text-align:right;color:{color};font-weight:700;font-size:18px'>{level}</div>
196
+ <div style='direction:rtl;text-align:right'>👁️ زاوية التصوير: <b>{'أمامية' if view=='frontal' else 'جانبية'}</b></div>
197
+ <div style='direction:rtl;text-align:right'>📍 الجانب المتأثر: <b>{side}</b></div>
198
+ <div style='direction:rtl;text-align:right'>🩺 الحالة المحتملة: <b>{condition}</b></div>
199
+ <div style='direction:rtl;text-align:right'>📊 درجة الخطورة: <b>{score:.1f}/10</b></div>
200
+ <div style='direction:rtl;text-align:right'>{desc}</div>
201
  {booking_html}
202
+ <div style='font-size:13px;color:#555;margin-top:8px;direction:rtl;text-align:right'>⚠️ هذا تحليل مبدئي يعتمد على الفيديو ولا يُغني عن الفحص الطبي.</div>
203
  """
204
  return html, _gauge_html(norm_score)
205
 
206
+ # ===========================
207
+ # واجهة Gradio مع تعليمات تصوير
208
+ # ===========================
209
  instructions = """
210
+ <div style='direction:rtl;text-align:right'>
211
+ <h3>🎥 تعليمات التصوير لضمان دقة التحليل:</h3>
212
+ <ol>
213
+ <li>ضع الكاميرا على <b>بعد 2 إلى 3 أمتار</b> من الشخص، بارتفاع الركبة تقريبًا.</li>
214
+ <li>استخدم <b>إضاءة جيدة</b> بدون ظلال قوية.</li>
215
+ <li>صوّر <b>زاوية أمامية واضحة</b> قدر الإمكان، مع إظهار القدمين بالكامل.</li>
216
+ <li>اطلب من الشخص أن <b>يمشي 3–5 أمتار ذهابًا وإيابًا</b> لمدة 15–30 ثانية.</li>
217
+ <li>تجنّب الملابس الطويلة أو الفضفاضة التي تحجب الركبة والكاحل.</li>
218
+ <li>ثبّت الهاتف لتجنّب اهتزاز الفيديو أثناء التصوير.</li>
219
+ </ol>
220
+ </div>
221
  """
222
 
223
+ with gr.Blocks(title="تحليل المشية العصبية - v8.1 (RTL + تعليمات)") as demo:
224
+ gr.Markdown("## 🩺 نظام تحليل المشية العصبية – الإصدار 8.1", elem_id="title", elem_classes="rtl")
225
+ gr.HTML(instructions)
 
 
 
226
  with gr.Row():
227
  with gr.Column(scale=1):
228
  video_in = gr.File(label="📂 اختر فيديو المشي", file_types=[".mp4", ".avi", ".mov"], type="binary")
229
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
230
  with gr.Column(scale=1):
231
  gauge = gr.HTML("<div></div>")
232
+ out_html = gr.HTML("<i style='direction:rtl;text-align:right'>النتيجة ستظهر هنا بعد التحليل...</i>")
233
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
234
 
235
  if __name__ == "__main__":