EngReem85 commited on
Commit
8b15264
·
verified ·
1 Parent(s): f4bfc2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -145
app.py CHANGED
@@ -6,9 +6,6 @@ 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.7, model_complexity=1)
14
 
@@ -32,16 +29,13 @@ 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
- # norm_score بين 0 و 1
36
  pct = int(max(0, min(1, norm_score)) * 100)
37
- # تدرّج أخضر→أصفر→أحمر
38
  bar = f"""
39
  <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ddd;">
40
- <div style="width:{pct}%;height:100%;
41
- background: linear-gradient(90deg,#4caf50,#ffeb3b,#f44336);
42
- filter: saturate(1.2);"></div>
43
  </div>
44
- <div style="font-size:12px;color:#555;margin-top:6px">Risk: {pct}%</div>
45
  """
46
  return bar
47
 
@@ -69,48 +63,32 @@ def analyze_gait(video_file):
69
  fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
70
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
71
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
72
-
73
- # تقدير تحويل بيكسل→متر (تقريبي حسب الطول 1.7م و 80% من الارتفاع إطار)
74
  px2m = 1.7 / (H * 0.8)
75
 
76
- # قوائم القياسات عبر الإطارات
77
  L_clear, R_clear = [], []
78
  L_angle, R_angle = [], []
79
- base_px_seq, base_m_seq = [], []
80
- torso_tilt_seq = [] # ميل الجذع (درجات) بالنسبة للمحور الرأسي
81
- step_L, step_R = [], []
82
 
83
- # كواشف زمنية (نِسَب)
84
- low_clear_flags_L, low_clear_flags_R = 0, 0
85
- asym_clear_flags = 0
86
- lean_flags = 0
87
-
88
- # مرجعية أرض
89
  ground_y = H * 0.92
90
-
91
- # لحساب طول الخطوة تقريبياً
92
- prev_L_ank, prev_R_ank = None, None
93
-
94
- frames_processed, person_detected = 0, False
95
 
96
  while cap.isOpened() and frames_processed < min(1200, total_frames or 1200):
97
  ret, frame = cap.read()
98
- if not ret:
99
- break
100
  frames_processed += 1
101
 
102
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
103
  res = pose.process(frame_rgb)
104
- if not res.pose_landmarks:
105
- continue
106
 
107
  person_detected = True
108
  lm = res.pose_landmarks.landmark
109
 
110
- def xy(idx):
111
- return [lm[idx].x * W, lm[idx].y * H]
112
 
113
- # نقاط مهمة
114
  L_ank = xy(mp_pose.PoseLandmark.LEFT_ANKLE.value)
115
  R_ank = xy(mp_pose.PoseLandmark.RIGHT_ANKLE.value)
116
  L_knee = xy(mp_pose.PoseLandmark.LEFT_KNEE.value)
@@ -122,155 +100,134 @@ def analyze_gait(video_file):
122
  L_sh = xy(mp_pose.PoseLandmark.LEFT_SHOULDER.value)
123
  R_sh = xy(mp_pose.PoseLandmark.RIGHT_SHOULDER.value)
124
 
125
- # ارتفاع القدم بالسنتيمتر (أقرب نقطة من الكاحل/مقدمة القدم للأرض)
126
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
127
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
128
- L_clear.append(Lc); R_clear.append(Rc)
 
129
 
130
  # زوايا الكاحل
131
  La = _angle(L_knee, L_ank, L_foot)
132
  Ra = _angle(R_knee, R_ank, R_foot)
133
- L_angle.append(La); R_angle.append(Ra)
 
134
 
135
- # مسافة تباعد الكاحلين (بالبكسل والمتر)
136
- base_px = abs(L_ank[0] - R_ank[0])
137
- base_px_seq.append(base_px)
138
- base_m_seq.append(base_px * px2m)
139
 
140
- # ميل الجذع: زاوية (منتصف الكتفين → منتصف الوركين) مقابل الرأسي
141
  mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2]
142
  mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2]
143
- vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]]) # من الحوض للأكتاف
144
- # زاوية مع المحور الرأسي (0 = عمودي مثالي)
145
  tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6))))
146
  torso_tilt_seq.append(tilt)
147
 
148
- # تقدير طول الخطوة (تقريبي) من حركة الكاحل إطارياً
149
- if prev_L_ank is not None:
150
- step_L.append(_dist(L_ank, prev_L_ank) * px2m)
151
- if prev_R_ank is not None:
152
- step_R.append(_dist(R_ank, prev_R_ank) * px2m)
153
- prev_L_ank, prev_R_ank = L_ank, R_ank
154
-
155
- # كواشف زمنية لكل إطار:
156
- if Lc < 3.5: low_clear_flags_L += 1
157
- if Rc < 3.5: low_clear_flags_R += 1
158
- if abs(Lc - Rc) > 6.0: asym_clear_flags += 1
159
- if tilt > 8.0: lean_flags += 1
160
-
161
  cap.release()
162
- try:
163
- os.unlink(video_path)
164
- except:
165
- pass
166
 
167
- if not person_detected or frames_processed == 0:
168
- return "<div>❌ لم يتم اكتشاف شخص في الفيديو. يُفضّل تصوير جانبي أو أمامي واضح وإضاءة جيدة.</div>", "<div></div>"
169
 
170
  # ===========================
171
- # إحصاءات أساسية
172
  # ===========================
173
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
174
  std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
175
  avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle)
176
- avg_base_m = _safe_mean(base_m_seq)
177
  avg_tilt = _safe_mean(torso_tilt_seq)
 
 
 
178
 
179
- # تقدير العرض النسبي لتحديد زاوية التصوير (أمامي/جانبي)
180
- # إذا كانت المسافة الأفقية بين الكاحلين نسبةً إلى عرض الإطار كبيرة → غالبًا أمامي
181
- view_ratio = _safe_mean(base_px_seq) / max(1, W)
182
- view = "frontal" if view_ratio > 0.18 else "side"
183
-
184
- # نسب زمنية (قوة دليل عبر الزمن)
185
- n = max(1, len(L_clear))
186
- ratio_low_L = low_clear_flags_L / n
187
- ratio_low_R = low_clear_flags_R / n
188
- ratio_asym = asym_clear_flags / n
189
- ratio_lean = lean_flags / n
190
 
191
  # ===========================
192
- # منطق متعدد الشروط (متوازن)
193
  # ===========================
194
- score = 0.0
195
- strong_votes = 0
196
-
197
- # 1) Foot Drop-like: انخفاض مستمر + نسب زمنية عالية
198
- foot_drop_evidence = (min(avg_Lc, avg_Rc) < 3.5) or (ratio_low_L > 0.35 or ratio_low_R > 0.35)
199
- if view == "side":
200
- if foot_drop_evidence and abs(avg_La - avg_Ra) < 35:
201
- score += 3.0; strong_votes += 1
202
- elif (ratio_low_L > 0.2 or ratio_low_R > 0.2):
203
- score += 1.5
204
-
205
- # 2) Neuropathy-like: تذبذب ارتفاع واضح + لا تماثل زاوٍي
206
- var_clear = max(std_Lc, std_Rc)
207
- diff_angle = abs(avg_La - avg_Ra)
208
- if (var_clear > 9 and diff_angle > 16) or (ratio_asym > 0.25 and var_clear > 7):
209
- score += 2.5; strong_votes += 1
210
- elif (var_clear > 7 and diff_angle > 12):
 
 
 
211
  score += 1.5
212
 
213
- # 3) Charcot-like: قاعدة عريضة + ميل جذع/عدم اتزان (أكثر منطقية في الأمامي)
214
  if view == "frontal":
215
- if (avg_base_m > 0.28 and ratio_lean > 0.25) or (avg_base_m > 0.30):
216
- score += 3.0; strong_votes += 1
217
- elif avg_base_m > 0.25 and ratio_lean > 0.15:
218
- score += 1.5
219
- else:
220
- # من الجانب: نعتمد أكثر على عدم التماثل الزمني
221
- if ratio_asym > 0.35 and var_clear > 8:
222
- score += 2.5; strong_votes += 1
223
-
224
- # 4) ميل الجذع المستمر (واضح سريريًا)
225
- if ratio_lean > 0.35 or avg_tilt > 10:
226
- score += 1.5
227
 
228
- # 5) اختلاف الارتفاع بين القدمين (متوسط)
229
- if abs(avg_Lc - avg_Rc) > 6:
230
- score += 1.0
231
-
232
- # 6) فلاتر السماح بالمشي الطليعي المنتظم (Toe-walking) دون تصنيف مرضي
233
- if (avg_Lc > 10 and avg_Rc > 10) and (var_clear < 5) and (ratio_asym < 0.15) and (ratio_lean < 0.15):
234
- html = (
235
- "<div style='color:#2e7d32;font-weight:600'>✅ المشية طليعية منتظمة.</div>"
236
- "<div>تم التعرف على نمط مشي طليعي مستقر دون مؤشرات عصبية مقلقة.</div>"
237
- "<div style='font-size:13px;color:#555;margin-top:8px'>⚠️ يُوصى بالمراجعة فقط إذا وُجد ألم أو عدم اتزان.</div>"
238
- )
239
- return html, _gauge_html(0.1)
240
 
241
  # ===========================
242
- # تسوية متوازنة للحكم النهائي
243
- # - إذا وُجدت مؤشرين قويين على الأقل → نرفع التصنيف
244
- # - وإلا نستخدم الدرجة المعيارية
245
  # ===========================
246
- max_score = 10.0
247
- norm_score = min(score / max_score, 1.0)
248
-
249
- # عتبات متوازنة (لا مبالغة في التشدد ولا التساهل)
250
- if (strong_votes >= 2) or (norm_score >= 0.7):
251
- level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات قوية ومتعددة لخلل في المشية."
 
 
 
 
 
252
  elif norm_score >= 0.45:
253
- level, color, desc = "🟡 متوسطة الخطورة", "#f9a825", "تم رصد مؤشرات ملحوظة تحتاج متابعة."
 
 
 
 
 
 
 
 
 
254
  else:
255
- level, color, desc = "🟢 طبيعي", "#2e7d32", "المشية ضمن الحدود الطبيعية."
256
-
257
- # ترجيح نوع الحالة
258
- if (view == "frontal" and avg_base_m > 0.28 and ratio_lean > 0.25) or (avg_base_m > 0.30):
259
- condition = "قدم شاركوت (Charcot Foot)"
260
- elif foot_drop_evidence and view == "side":
261
- condition = "ضعف العضلة الظنبوبية (Foot Drop)"
262
- elif (var_clear > 9 and diff_angle > 16) or (ratio_asym > 0.25 and var_clear > 7):
263
- condition = "اعتلال الأعصاب المحيطية / السكري"
 
 
 
 
264
  else:
265
- condition = "غير محددة بدقة"
266
 
267
  html = f"""
268
  <div style='color:{color};font-weight:700;font-size:18px'>{level}</div>
269
- <div>👁️ زاوية التصوير المُكتشفة: <b>{'أمامية' if view=='frontal' else 'جانبية'}</b></div>
270
  <div>🩺 الحالة المحتملة: <b>{condition}</b></div>
271
- <div>📊 درجة المؤشرات: <b>{score:.1f}/{max_score:.0f}</b></div>
272
  <div>{desc}</div>
273
- <div style='font-size:13px;color:#555;margin-top:8px'>⚠️ تحليل تقديري يعتمد على أنماط الحركة؛ لا يُغني عن التقييم الطبي.</div>
 
274
  """
275
 
276
  return html, _gauge_html(norm_score)
@@ -278,9 +235,9 @@ def analyze_gait(video_file):
278
  # ===========================
279
  # واجهة Gradio
280
  # ===========================
281
- with gr.Blocks(title="تحليل المشية العصبية - v6 (متوازن مع كشف الزاوية)") as demo:
282
- gr.Markdown("## 🩺 تحليل المشية العصبية الإصدار v6")
283
- gr.Markdown("يرجى رفع فيديو جانبي أو أمامي (15–30 ثانية). النظام يحدد زاوية التصوير تلقائيًا ويعرض تقييمًا متوازنًا مع شريط ألوان.")
284
 
285
  with gr.Row():
286
  with gr.Column(scale=1):
@@ -288,7 +245,7 @@ with gr.Blocks(title="تحليل المشية العصبية - v6 (متوازن
288
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
289
  with gr.Column(scale=1):
290
  gauge = gr.HTML("<div></div>")
291
- out_html = gr.HTML("<i>النتيجة ستظهر هنا...</i>")
292
 
293
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
294
 
 
6
  import os
7
  import math
8
 
 
 
 
9
  mp_pose = mp.solutions.pose
10
  pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.7, model_complexity=1)
11
 
 
29
  def _safe_std(x): return float(np.std(x)) if x else 0.0
30
 
31
  def _gauge_html(norm_score):
 
32
  pct = int(max(0, min(1, norm_score)) * 100)
33
+ color = "#4caf50" if pct < 35 else "#fbc02d" if pct < 65 else "#c62828"
34
  bar = f"""
35
  <div style="width:100%;background:#eee;border-radius:10px;height:16px;overflow:hidden;border:1px solid #ddd;">
36
+ <div style="width:{pct}%;height:100%;background:{color};transition:width 1s;"></div>
 
 
37
  </div>
38
+ <div style="font-size:12px;color:#555;margin-top:6px">درجة الخطورة: {pct}%</div>
39
  """
40
  return bar
41
 
 
63
  fps = float(cap.get(cv2.CAP_PROP_FPS) or 30.0)
64
  W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
65
  H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
 
 
66
  px2m = 1.7 / (H * 0.8)
67
 
68
+ # قياسات الحركة
69
  L_clear, R_clear = [], []
70
  L_angle, R_angle = [], []
71
+ base_px_seq = []
72
+ torso_tilt_seq = []
 
73
 
 
 
 
 
 
 
74
  ground_y = H * 0.92
75
+ frames_processed = 0
76
+ person_detected = False
 
 
 
77
 
78
  while cap.isOpened() and frames_processed < min(1200, total_frames or 1200):
79
  ret, frame = cap.read()
80
+ if not ret: break
 
81
  frames_processed += 1
82
 
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
 
90
+ def xy(idx): return [lm[idx].x * W, lm[idx].y * H]
 
91
 
 
92
  L_ank = xy(mp_pose.PoseLandmark.LEFT_ANKLE.value)
93
  R_ank = xy(mp_pose.PoseLandmark.RIGHT_ANKLE.value)
94
  L_knee = xy(mp_pose.PoseLandmark.LEFT_KNEE.value)
 
100
  L_sh = xy(mp_pose.PoseLandmark.LEFT_SHOULDER.value)
101
  R_sh = xy(mp_pose.PoseLandmark.RIGHT_SHOULDER.value)
102
 
103
+ # ارتفاع القدم
104
  Lc = max(0, (ground_y - min(L_ank[1], L_foot[1])) * px2m * 100)
105
  Rc = max(0, (ground_y - min(R_ank[1], R_foot[1])) * px2m * 100)
106
+ L_clear.append(Lc)
107
+ R_clear.append(Rc)
108
 
109
  # زوايا الكاحل
110
  La = _angle(L_knee, L_ank, L_foot)
111
  Ra = _angle(R_knee, R_ank, R_foot)
112
+ L_angle.append(La)
113
+ R_angle.append(Ra)
114
 
115
+ # قاعدة القدمين
116
+ base_px_seq.append(abs(L_ank[0] - R_ank[0]))
 
 
117
 
118
+ # ميل الجذع
119
  mid_sh = [(L_sh[0]+R_sh[0])/2, (L_sh[1]+R_sh[1])/2]
120
  mid_hip= [(L_hip[0]+R_hip[0])/2, (L_hip[1]+R_hip[1])/2]
121
+ vec = np.array([mid_sh[0]-mid_hip[0], mid_sh[1]-mid_hip[1]])
 
122
  tilt = abs(90 - abs(math.degrees(math.atan2(abs(vec[1]), abs(vec[0])+1e-6))))
123
  torso_tilt_seq.append(tilt)
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  cap.release()
126
+ try: os.unlink(video_path)
127
+ except: pass
 
 
128
 
129
+ if not person_detected:
130
+ return "<div>❌ لم يتم اكتشاف شخص في الفيديو.</div>", "<div></div>"
131
 
132
  # ===========================
133
+ # الإحصاءات والتحليل
134
  # ===========================
135
  avg_Lc, avg_Rc = _safe_mean(L_clear), _safe_mean(R_clear)
136
  std_Lc, std_Rc = _safe_std(L_clear), _safe_std(R_clear)
137
  avg_La, avg_Ra = _safe_mean(L_angle), _safe_mean(R_angle)
138
+ avg_base_px = _safe_mean(base_px_seq)
139
  avg_tilt = _safe_mean(torso_tilt_seq)
140
+ var_clear = max(std_Lc, std_Rc)
141
+ diff_clear = abs(avg_Lc - avg_Rc)
142
+ diff_angle = abs(avg_La - avg_Ra)
143
 
144
+ # تحديد زاوية التصوير (أغلبها أمامية)
145
+ view_ratio = avg_base_px / max(1, W)
146
+ view = "frontal" if view_ratio > 0.16 else "side"
 
 
 
 
 
 
 
 
147
 
148
  # ===========================
149
+ # منطق التصنيف الجديد (أعلى دقة)
150
  # ===========================
151
+ score = 0
152
+ strong_flags = 0
153
+
154
+ # Foot Drop
155
+ if min(avg_Lc, avg_Rc) < 3.5 or (var_clear > 9 and diff_angle < 20):
156
+ score += 3
157
+ strong_flags += 1
158
+
159
+ # Neuropathy
160
+ if (var_clear > 8 and diff_angle > 15) or diff_clear > 6:
161
+ score += 2.5
162
+ strong_flags += 1
163
+
164
+ # Charcot
165
+ if avg_base_px * px2m > 0.28 and avg_tilt > 9:
166
+ score += 3
167
+ strong_flags += 1
168
+
169
+ # ميل الجذع الواضح
170
+ if avg_tilt > 12:
171
  score += 1.5
172
 
173
+ # زيادة الوزن للحالات الأمامية لأنها أكثر حساسية للانحراف
174
  if view == "frontal":
175
+ score *= 1.2
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ # تقييد الدرجة ضمن 0-10
178
+ score = min(score, 10)
179
+ norm_score = score / 10
 
 
 
 
 
 
 
 
 
180
 
181
  # ===========================
182
+ # التقييم النهائي + زر الحجز
 
 
183
  # ===========================
184
+ if norm_score >= 0.7 or strong_flags >= 2:
185
+ level, color, desc = "🔴 عالية الخطورة", "#c62828", "تم رصد مؤشرات قوية تستدعي مراجعة طبيب متخصص."
186
+ booking_html = """
187
+ <div style="margin-top:10px">
188
+ <a href="https://example.com/book" target="_blank"
189
+ style="background:#007bff;color:#fff;padding:10px 16px;border-radius:8px;
190
+ text-decoration:none;font-weight:600;display:inline-block;">
191
+ احجز موعد مباشر (حضوري أو أونلاين)
192
+ </a>
193
+ </div>
194
+ """
195
  elif norm_score >= 0.45:
196
+ level, color, desc = "🟡 متوسطة الخطورة", "#fbc02d", "مؤشرات تستدعي متابعة طبية وقائية."
197
+ booking_html = """
198
+ <div style="margin-top:10px">
199
+ <a href="https://example.com/book" target="_blank"
200
+ style="background:#fbc02d;color:#000;padding:10px 16px;border-radius:8px;
201
+ text-decoration:none;font-weight:600;display:inline-block;">
202
+ احجز استشارة طبية
203
+ </a>
204
+ </div>
205
+ """
206
  else:
207
+ level, color, desc = "🟢 طبيعية", "#2e7d32", "المشية ضمن النطاق الطبيعي."
208
+ booking_html = ""
209
+
210
+ # ترجيح الحالة
211
+ if score >= 7:
212
+ if avg_base_px * px2m > 0.28:
213
+ condition = "قدم شاركوت (Charcot Foot)"
214
+ elif diff_angle > 15:
215
+ condition = "اعتلال الأعصاب المحيطية / السكري"
216
+ else:
217
+ condition = "ضعف العضلة الظنبوبية (Foot Drop)"
218
+ elif score >= 4:
219
+ condition = "خلل بسيط غير محدد"
220
  else:
221
+ condition = "لا توجد مؤشرات مرضية واضحة"
222
 
223
  html = f"""
224
  <div style='color:{color};font-weight:700;font-size:18px'>{level}</div>
225
+ <div>👁️ زاوية التصوير: <b>{'أمامية' if view=='frontal' else 'جانبية'}</b></div>
226
  <div>🩺 الحالة المحتملة: <b>{condition}</b></div>
227
+ <div>📊 درجة الخطورة: <b>{score:.1f}/10</b></div>
228
  <div>{desc}</div>
229
+ {booking_html}
230
+ <div style='font-size:13px;color:#555;margin-top:8px'>⚠️ هذا تحليل مبدئي يعتمد على الفيديو فقط ولا يُغني عن الفحص الطبي.</div>
231
  """
232
 
233
  return html, _gauge_html(norm_score)
 
235
  # ===========================
236
  # واجهة Gradio
237
  # ===========================
238
+ with gr.Blocks(title="تحليل المشية العصبية - v7هائي ومحسّن)") as demo:
239
+ gr.Markdown("## 🩺 نظام تحليل المشية العصبية (الإصدار 7)")
240
+ gr.Markdown("🔹 التحليل مخصص للمقاطع الأمامية أو الجانبية (15–30 ثانية).<br>🔹 يظهر زر للحجز عند الحاجة إلى متابعة طبية.")
241
 
242
  with gr.Row():
243
  with gr.Column(scale=1):
 
245
  analyze_btn = gr.Button("🔍 بدء التحليل", variant="primary")
246
  with gr.Column(scale=1):
247
  gauge = gr.HTML("<div></div>")
248
+ out_html = gr.HTML("<i>النتيجة ستظهر هنا بعد التحليل...</i>")
249
 
250
  analyze_btn.click(fn=analyze_gait, inputs=[video_in], outputs=[out_html, gauge])
251