Spaces:
Running
Running
altawil
commited on
Update app.py
Browse files
app.py
CHANGED
@@ -1,594 +1,596 @@
|
|
1 |
# app.py - InterFuser Self-Driving API Server
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
import uuid
|
4 |
import base64
|
5 |
import cv2
|
6 |
import torch
|
7 |
import numpy as np
|
|
|
8 |
from fastapi import FastAPI, HTTPException
|
9 |
from fastapi.responses import HTMLResponse
|
10 |
-
from pydantic import BaseModel
|
11 |
-
from
|
12 |
-
from typing import List, Dict, Any, Optional
|
13 |
-
import logging
|
14 |
|
15 |
-
#
|
|
|
|
|
|
|
|
|
16 |
from model_definition import InterfuserModel, load_and_prepare_model, create_model_config
|
17 |
-
from simulation_modules import (
|
18 |
-
InterfuserController, ControllerConfig, Tracker, DisplayInterface,
|
19 |
-
render, render_waypoints, render_self_car, WAYPOINT_SCALE_FACTOR,
|
20 |
-
T1_FUTURE_TIME, T2_FUTURE_TIME
|
21 |
-
)
|
22 |
|
23 |
-
#
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
-
#
|
28 |
app = FastAPI(
|
29 |
title="Baseer Self-Driving API",
|
30 |
-
description="API
|
31 |
-
version="1.
|
32 |
)
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
#
|
38 |
-
try:
|
39 |
-
# إنشاء إعدادات النموذج باستخدام الإعدادات الصحيحة من التدريب
|
40 |
-
model_config = create_model_config(
|
41 |
-
model_path="model/best_model.pth"
|
42 |
-
# الإعدادات الصحيحة من التدريب ستطبق تلقائياً:
|
43 |
-
# embed_dim=256, rgb_backbone_name='r50', waypoints_pred_head='gru'
|
44 |
-
# with_lidar=False, with_right_left_sensors=False, with_center_sensor=False
|
45 |
-
)
|
46 |
-
|
47 |
-
# تحميل النموذج مع الأوزان
|
48 |
-
model = load_and_prepare_model(model_config, device)
|
49 |
-
logger.info("✅ تم تحميل النموذج بنجاح")
|
50 |
-
|
51 |
-
except Exception as e:
|
52 |
-
logger.error(f"❌ خطأ في تحميل النموذج: {e}")
|
53 |
-
logger.info("🔄 محاولة إنشاء نموذج بأوزان عشوائية...")
|
54 |
-
try:
|
55 |
-
model = InterfuserModel()
|
56 |
-
model.to(device)
|
57 |
-
model.eval()
|
58 |
-
logger.warning("⚠️ تم إنشاء النموذج بأوزان عشوائية")
|
59 |
-
except Exception as e2:
|
60 |
-
logger.error(f"❌ فشل في إنشاء النموذج: {e2}")
|
61 |
-
model = None
|
62 |
-
|
63 |
-
# تهيئة واجهة العرض
|
64 |
-
display = DisplayInterface()
|
65 |
-
|
66 |
-
# قاموس لتخزين جلسات المستخدمين
|
67 |
-
SESSIONS: Dict[str, Dict] = {}
|
68 |
|
69 |
-
#
|
|
|
|
|
70 |
class Measurements(BaseModel):
|
71 |
-
|
72 |
-
theta: float = 0.0
|
73 |
-
speed: float = 0.0
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
class ModelOutputs(BaseModel):
|
81 |
-
traffic: List[List[List[float]]] # 20x20x7 grid
|
82 |
-
waypoints: List[List[float]] # Nx2 waypoints
|
83 |
-
is_junction: float
|
84 |
-
traffic_light_state: float
|
85 |
-
stop_sign: float
|
86 |
|
87 |
class ControlCommands(BaseModel):
|
88 |
steer: float
|
89 |
throttle: float
|
90 |
brake: bool
|
91 |
|
92 |
-
class
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
|
97 |
-
class
|
98 |
-
model_outputs: ModelOutputs
|
99 |
control_commands: ControlCommands
|
100 |
-
|
|
|
|
|
|
|
101 |
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
110 |
transforms.ToTensor(),
|
111 |
transforms.Resize((224, 224), antialias=True),
|
112 |
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
113 |
])
|
|
|
|
|
114 |
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
تحاكي ما يفعله PDMDataset.__getitem__ لإنشاء دفعة (batch) واحدة.
|
121 |
-
"""
|
122 |
-
# 1. معالجة الصورة الرئيسية
|
123 |
-
from PIL import Image
|
124 |
-
if isinstance(frame_rgb, np.ndarray):
|
125 |
-
frame_rgb = Image.fromarray(frame_rgb)
|
126 |
|
127 |
-
|
128 |
|
129 |
-
|
130 |
-
batch = {
|
131 |
'rgb': image_tensor,
|
132 |
-
'rgb_left': image_tensor.clone(),
|
133 |
-
'
|
134 |
-
'
|
|
|
135 |
}
|
136 |
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
batch['target_point'] = torch.tensor([m.target_point], dtype=torch.float32).to(device)
|
151 |
-
|
152 |
-
# لا نحتاج إلى قيم ground truth (gt_*) أثناء التنبؤ
|
153 |
-
return batch
|
154 |
-
|
155 |
-
def decode_base64_image(image_b64: str) -> np.ndarray:
|
156 |
-
"""
|
157 |
-
فك تشفير صورة Base64
|
158 |
-
"""
|
159 |
-
try:
|
160 |
-
image_bytes = base64.b64decode(image_b64)
|
161 |
-
nparr = np.frombuffer(image_bytes, np.uint8)
|
162 |
-
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
163 |
-
return image
|
164 |
-
except Exception as e:
|
165 |
-
raise HTTPException(status_code=400, detail=f"Invalid image format: {str(e)}")
|
166 |
-
|
167 |
-
def encode_image_to_base64(image: np.ndarray) -> str:
|
168 |
-
"""
|
169 |
-
تشفير صورة إلى Base64
|
170 |
-
"""
|
171 |
-
_, buffer = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
172 |
-
return base64.b64encode(buffer).decode('utf-8')
|
173 |
|
174 |
-
#
|
175 |
-
|
|
|
|
|
176 |
async def root():
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
<title>🚗 Baseer Self-Driving API</title>
|
187 |
-
<style>
|
188 |
-
* {{
|
189 |
-
margin: 0;
|
190 |
-
padding: 0;
|
191 |
-
box-sizing: border-box;
|
192 |
-
}}
|
193 |
-
body {{
|
194 |
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
195 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
196 |
-
min-height: 100vh;
|
197 |
-
display: flex;
|
198 |
-
align-items: center;
|
199 |
-
justify-content: center;
|
200 |
-
padding: 20px;
|
201 |
-
}}
|
202 |
-
.container {{
|
203 |
-
background: rgba(255, 255, 255, 0.95);
|
204 |
-
backdrop-filter: blur(10px);
|
205 |
-
border-radius: 20px;
|
206 |
-
padding: 40px;
|
207 |
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
208 |
-
text-align: center;
|
209 |
-
max-width: 600px;
|
210 |
-
width: 100%;
|
211 |
-
}}
|
212 |
-
.logo {{
|
213 |
-
font-size: 4rem;
|
214 |
-
margin-bottom: 20px;
|
215 |
-
animation: bounce 2s infinite;
|
216 |
-
}}
|
217 |
-
@keyframes bounce {{
|
218 |
-
0%, 20%, 50%, 80%, 100% {{ transform: translateY(0); }}
|
219 |
-
40% {{ transform: translateY(-10px); }}
|
220 |
-
60% {{ transform: translateY(-5px); }}
|
221 |
-
}}
|
222 |
-
h1 {{
|
223 |
-
color: #333;
|
224 |
-
margin-bottom: 10px;
|
225 |
-
font-size: 2.5rem;
|
226 |
-
}}
|
227 |
-
.subtitle {{
|
228 |
-
color: #666;
|
229 |
-
margin-bottom: 30px;
|
230 |
-
font-size: 1.2rem;
|
231 |
-
}}
|
232 |
-
.status {{
|
233 |
-
display: inline-block;
|
234 |
-
background: #4CAF50;
|
235 |
-
color: white;
|
236 |
-
padding: 8px 16px;
|
237 |
-
border-radius: 20px;
|
238 |
-
margin: 10px 0;
|
239 |
-
font-weight: bold;
|
240 |
-
}}
|
241 |
-
.stats {{
|
242 |
-
display: grid;
|
243 |
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
244 |
-
gap: 20px;
|
245 |
-
margin: 30px 0;
|
246 |
-
}}
|
247 |
-
.stat-card {{
|
248 |
-
background: #f8f9fa;
|
249 |
-
padding: 20px;
|
250 |
-
border-radius: 15px;
|
251 |
-
border-left: 4px solid #667eea;
|
252 |
-
}}
|
253 |
-
.stat-number {{
|
254 |
-
font-size: 2rem;
|
255 |
-
font-weight: bold;
|
256 |
-
color: #667eea;
|
257 |
-
}}
|
258 |
-
.stat-label {{
|
259 |
-
color: #666;
|
260 |
-
margin-top: 5px;
|
261 |
-
}}
|
262 |
-
.buttons {{
|
263 |
-
display: flex;
|
264 |
-
gap: 15px;
|
265 |
-
justify-content: center;
|
266 |
-
flex-wrap: wrap;
|
267 |
-
margin-top: 30px;
|
268 |
-
}}
|
269 |
-
.btn {{
|
270 |
-
display: inline-block;
|
271 |
-
padding: 12px 24px;
|
272 |
-
border-radius: 25px;
|
273 |
-
text-decoration: none;
|
274 |
-
font-weight: bold;
|
275 |
-
transition: all 0.3s ease;
|
276 |
-
border: none;
|
277 |
-
cursor: pointer;
|
278 |
-
}}
|
279 |
-
.btn-primary {{
|
280 |
-
background: #667eea;
|
281 |
-
color: white;
|
282 |
-
}}
|
283 |
-
.btn-secondary {{
|
284 |
-
background: #6c757d;
|
285 |
-
color: white;
|
286 |
-
}}
|
287 |
-
.btn:hover {{
|
288 |
-
transform: translateY(-2px);
|
289 |
-
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
290 |
-
}}
|
291 |
-
.features {{
|
292 |
-
text-align: right;
|
293 |
-
margin-top: 30px;
|
294 |
-
padding: 20px;
|
295 |
-
background: #f8f9fa;
|
296 |
-
border-radius: 15px;
|
297 |
-
}}
|
298 |
-
.features h3 {{
|
299 |
-
color: #333;
|
300 |
-
margin-bottom: 15px;
|
301 |
-
}}
|
302 |
-
.features ul {{
|
303 |
-
list-style: none;
|
304 |
-
padding: 0;
|
305 |
-
}}
|
306 |
-
.features li {{
|
307 |
-
padding: 5px 0;
|
308 |
-
color: #666;
|
309 |
-
}}
|
310 |
-
.features li:before {{
|
311 |
-
content: "✅ ";
|
312 |
-
margin-left: 10px;
|
313 |
-
}}
|
314 |
-
</style>
|
315 |
-
</head>
|
316 |
-
<body>
|
317 |
-
<div class="container">
|
318 |
-
<div class="logo">🚗</div>
|
319 |
-
<h1>Baseer Self-Driving API</h1>
|
320 |
-
<p class="subtitle">نظام القيادة الذاتية المتقدم</p>
|
321 |
-
|
322 |
-
<div class="status">🟢 يعمل بنجاح</div>
|
323 |
-
|
324 |
-
<div class="stats">
|
325 |
-
<div class="stat-card">
|
326 |
-
<div class="stat-number">{len(SESSIONS)}</div>
|
327 |
-
<div class="stat-label">الجلسات النشطة</div>
|
328 |
-
</div>
|
329 |
-
<div class="stat-card">
|
330 |
-
<div class="stat-number">v1.0</div>
|
331 |
-
<div class="stat-label">الإصدار</div>
|
332 |
-
</div>
|
333 |
-
<div class="stat-card">
|
334 |
-
<div class="stat-number">FastAPI</div>
|
335 |
-
<div class="stat-label">التقنية</div>
|
336 |
-
</div>
|
337 |
-
</div>
|
338 |
-
|
339 |
-
<div class="buttons">
|
340 |
-
<a href="/docs" class="btn btn-primary">📚 توثيق API</a>
|
341 |
-
<a href="/sessions" class="btn btn-secondary">📊 الجلسات</a>
|
342 |
-
</div>
|
343 |
-
|
344 |
-
<div class="features">
|
345 |
-
<h3>🌟 الميزات الرئيسية</h3>
|
346 |
-
<ul>
|
347 |
-
<li>نموذج InterFuser للقيادة الذاتية</li>
|
348 |
-
<li>معالجة الصور في الوقت الفعلي</li>
|
349 |
-
<li>اكتشاف الكائنات المرورية</li>
|
350 |
-
<li>تحديد المسارات الذكية</li>
|
351 |
-
<li>واجهة RESTful سهلة الاستخدام</li>
|
352 |
-
<li>إدارة جلسات متعددة</li>
|
353 |
-
</ul>
|
354 |
-
</div>
|
355 |
-
</div>
|
356 |
-
</body>
|
357 |
</html>
|
358 |
"""
|
359 |
-
return html_content
|
360 |
|
361 |
-
@app.post("/start_session",
|
362 |
-
|
363 |
-
"""
|
364 |
-
بدء جلسة جديدة للمحاكاة
|
365 |
-
"""
|
366 |
session_id = str(uuid.uuid4())
|
|
|
|
|
|
|
367 |
|
368 |
-
# إنشاء جلسة جديدة
|
369 |
SESSIONS[session_id] = {
|
370 |
-
'tracker': Tracker(
|
371 |
-
'controller': InterfuserController(
|
372 |
-
'frame_num': 0
|
373 |
-
'created_at': np.datetime64('now'),
|
374 |
-
'last_activity': np.datetime64('now')
|
375 |
}
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
return SessionResponse(
|
380 |
-
session_id=session_id,
|
381 |
-
message="Session started successfully"
|
382 |
-
)
|
383 |
|
384 |
-
@app.post("/run_step", response_model=
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
# التحقق من وجود الجلسة
|
390 |
-
if data.session_id not in SESSIONS:
|
391 |
-
raise HTTPException(status_code=404, detail="Session not found")
|
392 |
-
|
393 |
-
session = SESSIONS[data.session_id]
|
394 |
-
tracker = session['tracker']
|
395 |
-
controller = session['controller']
|
396 |
-
|
397 |
-
# تحديث وقت النشاط
|
398 |
-
session['last_activity'] = np.datetime64('now')
|
399 |
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
traffic_np = traffic.cpu().numpy()[0] # أخذ أول عنصر من الـ batch
|
417 |
-
waypoints_np = waypoints.cpu().numpy()[0]
|
418 |
-
is_junction_prob = torch.sigmoid(is_junction)[0, 1].item()
|
419 |
-
traffic_light_prob = torch.sigmoid(traffic_light)[0, 0].item()
|
420 |
-
stop_sign_prob = torch.sigmoid(stop_sign)[0, 1].item()
|
421 |
-
|
422 |
-
# 5. تحديث التتبع
|
423 |
-
# تحويل traffic grid إلى detections للتتبع
|
424 |
-
detections = []
|
425 |
-
h, w, c = traffic_np.shape
|
426 |
-
for y in range(h):
|
427 |
-
for x in range(w):
|
428 |
-
for ch in range(c):
|
429 |
-
if traffic_np[y, x, ch] > 0.2: # عتبة الكشف
|
430 |
-
world_x = (x / w - 0.5) * 64 # تحويل إلى إحداثيات العالم
|
431 |
-
world_y = (y / h - 0.5) * 64
|
432 |
-
detections.append({
|
433 |
-
'position': [world_x, world_y],
|
434 |
-
'feature': traffic_np[y, x, ch]
|
435 |
-
})
|
436 |
-
|
437 |
-
updated_traffic = tracker.update_and_predict(detections, session['frame_num'])
|
438 |
-
|
439 |
-
# 6. تشغيل المتحكم
|
440 |
-
steer, throttle, brake, metadata = controller.run_step(
|
441 |
-
current_speed=data.measurements.speed,
|
442 |
-
waypoints=waypoints_np,
|
443 |
-
junction=is_junction_prob,
|
444 |
-
traffic_light_state=traffic_light_prob,
|
445 |
-
stop_sign=stop_sign_prob,
|
446 |
-
meta_data={'frame': session['frame_num']}
|
447 |
-
)
|
448 |
-
|
449 |
-
# 7. إنشاء خرائط العرض
|
450 |
-
surround_t0, counts_t0 = render(updated_traffic, t=0)
|
451 |
-
surround_t1, counts_t1 = render(updated_traffic, t=T1_FUTURE_TIME)
|
452 |
-
surround_t2, counts_t2 = render(updated_traffic, t=T2_FUTURE_TIME)
|
453 |
-
|
454 |
-
# إضافة المسار المقترح
|
455 |
-
wp_map = render_waypoints(waypoints_np)
|
456 |
-
map_t0 = cv2.add(surround_t0, wp_map)
|
457 |
-
|
458 |
-
# إضافة السيارة الذاتية
|
459 |
-
map_t0 = render_self_car(map_t0)
|
460 |
-
map_t1 = render_self_car(surround_t1)
|
461 |
-
map_t2 = render_self_car(surround_t2)
|
462 |
-
|
463 |
-
# 8. إنشاء لوحة العرض النهائية
|
464 |
-
interface_data = {
|
465 |
-
'camera_view': frame_bgr,
|
466 |
-
'map_t0': map_t0,
|
467 |
-
'map_t1': map_t1,
|
468 |
-
'map_t2': map_t2,
|
469 |
-
'text_info': {
|
470 |
-
'Frame': f"Frame: {session['frame_num']}",
|
471 |
-
'Control': f"Steer: {steer:.2f}, Throttle: {throttle:.2f}, Brake: {brake}",
|
472 |
-
'Speed': f"Speed: {data.measurements.speed:.1f} km/h",
|
473 |
-
'Junction': f"Junction: {is_junction_prob:.2f}",
|
474 |
-
'Traffic Light': f"Red Light: {traffic_light_prob:.2f}",
|
475 |
-
'Stop Sign': f"Stop Sign: {stop_sign_prob:.2f}",
|
476 |
-
'Metadata': metadata
|
477 |
-
},
|
478 |
-
'object_counts': {
|
479 |
-
't0': counts_t0,
|
480 |
-
't1': counts_t1,
|
481 |
-
't2': counts_t2
|
482 |
-
}
|
483 |
-
}
|
484 |
-
|
485 |
-
dashboard_image = display.run_interface(interface_data)
|
486 |
-
dashboard_b64 = encode_image_to_base64(dashboard_image)
|
487 |
-
|
488 |
-
# 9. تجميع المخرجات النهائية
|
489 |
-
response = RunStepOutput(
|
490 |
-
model_outputs=ModelOutputs(
|
491 |
-
traffic=traffic_np.tolist(),
|
492 |
-
waypoints=waypoints_np.tolist(),
|
493 |
-
is_junction=is_junction_prob,
|
494 |
-
traffic_light_state=traffic_light_prob,
|
495 |
-
stop_sign=stop_sign_prob
|
496 |
-
),
|
497 |
-
control_commands=ControlCommands(
|
498 |
-
steer=float(steer),
|
499 |
-
throttle=float(throttle),
|
500 |
-
brake=bool(brake)
|
501 |
-
),
|
502 |
-
dashboard_image_b64=dashboard_b64
|
503 |
-
)
|
504 |
-
|
505 |
-
# تحديث رقم الإطار
|
506 |
-
session['frame_num'] += 1
|
507 |
-
|
508 |
-
logger.info(f"Step completed for session {data.session_id}, frame {session['frame_num']}")
|
509 |
-
|
510 |
-
return response
|
511 |
-
|
512 |
-
except Exception as e:
|
513 |
-
logger.error(f"Error in run_step: {str(e)}")
|
514 |
-
raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
|
515 |
-
|
516 |
-
@app.post("/end_session", response_model=SessionResponse)
|
517 |
-
async def end_session(session_id: str):
|
518 |
-
"""
|
519 |
-
إنهاء جلسة المحاكاة
|
520 |
-
"""
|
521 |
-
if session_id not in SESSIONS:
|
522 |
-
raise HTTPException(status_code=404, detail="Session not found")
|
523 |
-
|
524 |
-
# حذف الجلسة
|
525 |
-
del SESSIONS[session_id]
|
526 |
|
527 |
-
|
|
|
|
|
|
|
528 |
|
529 |
-
|
530 |
-
|
531 |
-
|
|
|
|
|
532 |
)
|
533 |
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
active_sessions.append({
|
545 |
-
'session_id': session_id,
|
546 |
-
'frame_count': session_data['frame_num'],
|
547 |
-
'created_at': str(session_data['created_at']),
|
548 |
-
'last_activity': str(session_data['last_activity']),
|
549 |
-
'inactive_minutes': float(time_diff / np.timedelta64(1, 'm'))
|
550 |
-
})
|
551 |
-
|
552 |
-
return {
|
553 |
-
'total_sessions': len(active_sessions),
|
554 |
-
'sessions': active_sessions
|
555 |
}
|
|
|
556 |
|
557 |
-
|
558 |
-
|
559 |
-
"""
|
560 |
-
تنظيف الجلسات غير النشطة
|
561 |
-
"""
|
562 |
-
current_time = np.datetime64('now')
|
563 |
-
cleaned_sessions = []
|
564 |
-
|
565 |
-
for session_id in list(SESSIONS.keys()):
|
566 |
-
session = SESSIONS[session_id]
|
567 |
-
time_diff = current_time - session['last_activity']
|
568 |
-
inactive_minutes = float(time_diff / np.timedelta64(1, 'm'))
|
569 |
-
|
570 |
-
if inactive_minutes > max_inactive_minutes:
|
571 |
-
del SESSIONS[session_id]
|
572 |
-
cleaned_sessions.append(session_id)
|
573 |
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
'
|
580 |
-
|
581 |
-
|
582 |
-
# ================== معالج الأخطاء ==================
|
583 |
-
@app.exception_handler(Exception)
|
584 |
-
async def global_exception_handler(request, exc):
|
585 |
-
logger.error(f"Global exception: {str(exc)}")
|
586 |
-
return {
|
587 |
-
"error": "Internal server error",
|
588 |
-
"detail": str(exc)
|
589 |
-
}
|
590 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
591 |
# ================== تشغيل الخادم ==================
|
592 |
-
if __name__ == "__main__":
|
593 |
-
|
594 |
-
|
|
|
1 |
# app.py - InterFuser Self-Driving API Server
|
2 |
|
3 |
+
import uuid
|
4 |
+
import base64
|
5 |
+
import cv2
|
6 |
+
# import torch
|
7 |
+
# import numpy as np
|
8 |
+
# from fastapi import FastAPI, HTTPException
|
9 |
+
# from fastapi.responses import HTMLResponse
|
10 |
+
# from pydantic import BaseModel
|
11 |
+
# from torchvision import transforms
|
12 |
+
# from typing import List, Dict, Any, Optional
|
13 |
+
# import logging
|
14 |
+
|
15 |
+
# # استيراد من ملفاتنا المحلية
|
16 |
+
# from model_definition import InterfuserModel, load_and_prepare_model, create_model_config
|
17 |
+
# from simulation_modules import (
|
18 |
+
# InterfuserController, ControllerConfig, Tracker, DisplayInterface,
|
19 |
+
# render, render_waypoints, render_self_car, WAYPOINT_SCALE_FACTOR,
|
20 |
+
# T1_FUTURE_TIME, T2_FUTURE_TIME
|
21 |
+
# )
|
22 |
+
|
23 |
+
# # إعداد التسجيل
|
24 |
+
# logging.basicConfig(level=logging.INFO)
|
25 |
+
# logger = logging.getLogger(__name__)
|
26 |
+
|
27 |
+
# # ================== إعدادات عامة وتحميل النموذج ==================
|
28 |
+
# app = FastAPI(
|
29 |
+
# title="Baseer Self-Driving API",
|
30 |
+
# description="API للقيادة الذاتية باستخدام نموذج InterFuser",
|
31 |
+
# version="1.0.0"
|
32 |
+
# )
|
33 |
+
|
34 |
+
# device = torch.device("cpu")
|
35 |
+
# logger.info(f"Using device: {device}")
|
36 |
+
|
37 |
+
# # تحميل النموذج باستخدام الدالة المحسنة
|
38 |
+
# try:
|
39 |
+
# # إنشاء إعدادات النموذج باستخدام الإعدادات الصحيحة من التدريب
|
40 |
+
# model_config = create_model_config(
|
41 |
+
# model_path="model/best_model.pth"
|
42 |
+
# # الإعدادات الصحيحة من التدريب ستطبق تلقائياً:
|
43 |
+
# # embed_dim=256, rgb_backbone_name='r50', waypoints_pred_head='gru'
|
44 |
+
# # with_lidar=False, with_right_left_sensors=False, with_center_sensor=False
|
45 |
+
# )
|
46 |
+
|
47 |
+
# # تحميل النموذج مع الأوزان
|
48 |
+
# model = load_and_prepare_model(model_config, device)
|
49 |
+
# logger.info("✅ تم تحميل النموذج بنجاح")
|
50 |
+
|
51 |
+
# except Exception as e:
|
52 |
+
# logger.error(f"❌ خطأ في تحميل النموذج: {e}")
|
53 |
+
# logger.info("🔄 محاولة إنشاء نموذج بأوزان عشوائية...")
|
54 |
+
# try:
|
55 |
+
# model = InterfuserModel()
|
56 |
+
# model.to(device)
|
57 |
+
# model.eval()
|
58 |
+
# logger.warning("⚠️ تم إنشاء النموذج بأوزان عشوائية")
|
59 |
+
# except Exception as e2:
|
60 |
+
# logger.error(f"❌ فشل في إنشاء النموذج: {e2}")
|
61 |
+
# model = None
|
62 |
+
|
63 |
+
# # تهيئة واجهة العرض
|
64 |
+
# display = DisplayInterface()
|
65 |
+
|
66 |
+
# # قاموس لتخزين جلسات المستخدمين
|
67 |
+
# SESSIONS: Dict[str, Dict] = {}
|
68 |
+
|
69 |
+
# # ================== هياكل بيانات Pydantic ==================
|
70 |
+
# class Measurements(BaseModel):
|
71 |
+
# pos: List[float] = [0.0, 0.0] # [x, y] position
|
72 |
+
# theta: float = 0.0 # orientation angle
|
73 |
+
# speed: float = 0.0 # current speed
|
74 |
+
# steer: float = 0.0 # current steering
|
75 |
+
# throttle: float = 0.0 # current throttle
|
76 |
+
# brake: bool = False # brake status
|
77 |
+
# command: int = 4 # driving command (4 = FollowLane)
|
78 |
+
# target_point: List[float] = [0.0, 0.0] # target point [x, y]
|
79 |
+
|
80 |
+
# class ModelOutputs(BaseModel):
|
81 |
+
# traffic: List[List[List[float]]] # 20x20x7 grid
|
82 |
+
# waypoints: List[List[float]] # Nx2 waypoints
|
83 |
+
# is_junction: float
|
84 |
+
# traffic_light_state: float
|
85 |
+
# stop_sign: float
|
86 |
+
|
87 |
+
# class ControlCommands(BaseModel):
|
88 |
+
# steer: float
|
89 |
+
# throttle: float
|
90 |
+
# brake: bool
|
91 |
+
|
92 |
+
# class RunStepInput(BaseModel):
|
93 |
+
# session_id: str
|
94 |
+
# image_b64: str
|
95 |
+
# measurements: Measurements
|
96 |
+
|
97 |
+
# class RunStepOutput(BaseModel):
|
98 |
+
# model_outputs: ModelOutputs
|
99 |
+
# control_commands: ControlCommands
|
100 |
+
# dashboard_image_b64: str
|
101 |
+
|
102 |
+
# class SessionResponse(BaseModel):
|
103 |
+
# session_id: str
|
104 |
+
# message: str
|
105 |
+
|
106 |
+
# # ================== دوال المساعدة ==================
|
107 |
+
# def get_image_transform():
|
108 |
+
# """إنشاء تحويلات الصورة كما في PDMDataset"""
|
109 |
+
# return transforms.Compose([
|
110 |
+
# transforms.ToTensor(),
|
111 |
+
# transforms.Resize((224, 224), antialias=True),
|
112 |
+
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
113 |
+
# ])
|
114 |
+
|
115 |
+
# # إنشاء كائن التحويل مرة واحدة
|
116 |
+
# image_transform = get_image_transform()
|
117 |
+
|
118 |
+
# def preprocess_input(frame_rgb: np.ndarray, measurements: Measurements, device: torch.device) -> Dict[str, torch.Tensor]:
|
119 |
+
# """
|
120 |
+
# تحاكي ما يفعله PDMDataset.__getitem__ لإنشاء دفعة (batch) واحدة.
|
121 |
+
# """
|
122 |
+
# # 1. معالجة الصورة الرئيسية
|
123 |
+
# from PIL import Image
|
124 |
+
# if isinstance(frame_rgb, np.ndarray):
|
125 |
+
# frame_rgb = Image.fromarray(frame_rgb)
|
126 |
+
|
127 |
+
# image_tensor = image_transform(frame_rgb).unsqueeze(0).to(device) # إضافة بُعد الدفعة
|
128 |
+
|
129 |
+
# # 2. إنشاء مدخلات الكاميرات الأخرى عن طريق الاستنساخ
|
130 |
+
# batch = {
|
131 |
+
# 'rgb': image_tensor,
|
132 |
+
# 'rgb_left': image_tensor.clone(),
|
133 |
+
# 'rgb_right': image_tensor.clone(),
|
134 |
+
# 'rgb_center': image_tensor.clone(),
|
135 |
+
# }
|
136 |
+
|
137 |
+
# # 3. إنشاء مدخل ليدار وهمي (أصفار)
|
138 |
+
# batch['lidar'] = torch.zeros(1, 3, 224, 224, dtype=torch.float32).to(device)
|
139 |
+
|
140 |
+
# # 4. تجميع القياسات بنفس ترتيب PDMDataset
|
141 |
+
# m = measurements
|
142 |
+
# measurements_tensor = torch.tensor([[
|
143 |
+
# m.pos[0], m.pos[1], m.theta,
|
144 |
+
# m.steer, m.throttle, float(m.brake),
|
145 |
+
# m.speed, float(m.command)
|
146 |
+
# ]], dtype=torch.float32).to(device)
|
147 |
+
# batch['measurements'] = measurements_tensor
|
148 |
+
|
149 |
+
# # 5. إنشاء نقطة هدف
|
150 |
+
# batch['target_point'] = torch.tensor([m.target_point], dtype=torch.float32).to(device)
|
151 |
+
|
152 |
+
# # لا نحتاج إلى قيم ground truth (gt_*) أثناء التنبؤ
|
153 |
+
# return batch
|
154 |
+
|
155 |
+
# def decode_base64_image(image_b64: str) -> np.ndarray:
|
156 |
+
# """
|
157 |
+
# فك تشفير صورة Base64
|
158 |
+
# """
|
159 |
+
# try:
|
160 |
+
# image_bytes = base64.b64decode(image_b64)
|
161 |
+
# nparr = np.frombuffer(image_bytes, np.uint8)
|
162 |
+
# image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
163 |
+
# return image
|
164 |
+
# except Exception as e:
|
165 |
+
# raise HTTPException(status_code=400, detail=f"Invalid image format: {str(e)}")
|
166 |
+
|
167 |
+
# def encode_image_to_base64(image: np.ndarray) -> str:
|
168 |
+
# """
|
169 |
+
# تشفير صورة إلى Base64
|
170 |
+
# """
|
171 |
+
# _, buffer = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
172 |
+
# return base64.b64encode(buffer).decode('utf-8')
|
173 |
+
|
174 |
+
# # ================== نقاط نهاية الـ API ==================
|
175 |
+
# @app.get("/", response_class=HTMLResponse)
|
176 |
+
# async def root():
|
177 |
+
# """
|
178 |
+
# الصفحة الرئيسية للـ API
|
179 |
+
# """
|
180 |
+
# html_content = f"""
|
181 |
+
# <!DOCTYPE html>
|
182 |
+
# <html dir="rtl" lang="ar">
|
183 |
+
# <head>
|
184 |
+
# <meta charset="UTF-8">
|
185 |
+
# <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
186 |
+
# <title>🚗 Baseer Self-Driving API</title>
|
187 |
+
# <style>
|
188 |
+
# * {{
|
189 |
+
# margin: 0;
|
190 |
+
# padding: 0;
|
191 |
+
# box-sizing: border-box;
|
192 |
+
# }}
|
193 |
+
# body {{
|
194 |
+
# font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
195 |
+
# background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
196 |
+
# min-height: 100vh;
|
197 |
+
# display: flex;
|
198 |
+
# align-items: center;
|
199 |
+
# justify-content: center;
|
200 |
+
# padding: 20px;
|
201 |
+
# }}
|
202 |
+
# .container {{
|
203 |
+
# background: rgba(255, 255, 255, 0.95);
|
204 |
+
# backdrop-filter: blur(10px);
|
205 |
+
# border-radius: 20px;
|
206 |
+
# padding: 40px;
|
207 |
+
# box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
208 |
+
# text-align: center;
|
209 |
+
# max-width: 600px;
|
210 |
+
# width: 100%;
|
211 |
+
# }}
|
212 |
+
# .logo {{
|
213 |
+
# font-size: 4rem;
|
214 |
+
# margin-bottom: 20px;
|
215 |
+
# animation: bounce 2s infinite;
|
216 |
+
# }}
|
217 |
+
# @keyframes bounce {{
|
218 |
+
# 0%, 20%, 50%, 80%, 100% {{ transform: translateY(0); }}
|
219 |
+
# 40% {{ transform: translateY(-10px); }}
|
220 |
+
# 60% {{ transform: translateY(-5px); }}
|
221 |
+
# }}
|
222 |
+
# h1 {{
|
223 |
+
# color: #333;
|
224 |
+
# margin-bottom: 10px;
|
225 |
+
# font-size: 2.5rem;
|
226 |
+
# }}
|
227 |
+
# .subtitle {{
|
228 |
+
# color: #666;
|
229 |
+
# margin-bottom: 30px;
|
230 |
+
# font-size: 1.2rem;
|
231 |
+
# }}
|
232 |
+
# .status {{
|
233 |
+
# display: inline-block;
|
234 |
+
# background: #4CAF50;
|
235 |
+
# color: white;
|
236 |
+
# padding: 8px 16px;
|
237 |
+
# border-radius: 20px;
|
238 |
+
# margin: 10px 0;
|
239 |
+
# font-weight: bold;
|
240 |
+
# }}
|
241 |
+
# .stats {{
|
242 |
+
# display: grid;
|
243 |
+
# grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
244 |
+
# gap: 20px;
|
245 |
+
# margin: 30px 0;
|
246 |
+
# }}
|
247 |
+
# .stat-card {{
|
248 |
+
# background: #f8f9fa;
|
249 |
+
# padding: 20px;
|
250 |
+
# border-radius: 15px;
|
251 |
+
# border-left: 4px solid #667eea;
|
252 |
+
# }}
|
253 |
+
# .stat-number {{
|
254 |
+
# font-size: 2rem;
|
255 |
+
# font-weight: bold;
|
256 |
+
# color: #667eea;
|
257 |
+
# }}
|
258 |
+
# .stat-label {{
|
259 |
+
# color: #666;
|
260 |
+
# margin-top: 5px;
|
261 |
+
# }}
|
262 |
+
# .buttons {{
|
263 |
+
# display: flex;
|
264 |
+
# gap: 15px;
|
265 |
+
# justify-content: center;
|
266 |
+
# flex-wrap: wrap;
|
267 |
+
# margin-top: 30px;
|
268 |
+
# }}
|
269 |
+
# .btn {{
|
270 |
+
# display: inline-block;
|
271 |
+
# padding: 12px 24px;
|
272 |
+
# border-radius: 25px;
|
273 |
+
# text-decoration: none;
|
274 |
+
# font-weight: bold;
|
275 |
+
# transition: all 0.3s ease;
|
276 |
+
# border: none;
|
277 |
+
# cursor: pointer;
|
278 |
+
# }}
|
279 |
+
# .btn-primary {{
|
280 |
+
# background: #667eea;
|
281 |
+
# color: white;
|
282 |
+
# }}
|
283 |
+
# .btn-secondary {{
|
284 |
+
# background: #6c757d;
|
285 |
+
# color: white;
|
286 |
+
# }}
|
287 |
+
# .btn:hover {{
|
288 |
+
# transform: translateY(-2px);
|
289 |
+
# box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
290 |
+
# }}
|
291 |
+
# .features {{
|
292 |
+
# text-align: right;
|
293 |
+
# margin-top: 30px;
|
294 |
+
# padding: 20px;
|
295 |
+
# background: #f8f9fa;
|
296 |
+
# border-radius: 15px;
|
297 |
+
# }}
|
298 |
+
# .features h3 {{
|
299 |
+
# color: #333;
|
300 |
+
# margin-bottom: 15px;
|
301 |
+
# }}
|
302 |
+
# .features ul {{
|
303 |
+
# list-style: none;
|
304 |
+
# padding: 0;
|
305 |
+
# }}
|
306 |
+
# .features li {{
|
307 |
+
# padding: 5px 0;
|
308 |
+
# color: #666;
|
309 |
+
# }}
|
310 |
+
# .features li:before {{
|
311 |
+
# content: "✅ ";
|
312 |
+
# margin-left: 10px;
|
313 |
+
# }}
|
314 |
+
# </style>
|
315 |
+
# </head>
|
316 |
+
# <body>
|
317 |
+
# <div class="container">
|
318 |
+
# <div class="logo">🚗</div>
|
319 |
+
# <h1>Baseer Self-Driving API</h1>
|
320 |
+
# <p class="subtitle">نظام القيادة الذاتية المتقدم</p>
|
321 |
+
|
322 |
+
# <div class="status">🟢 يعمل بنجاح</div>
|
323 |
+
|
324 |
+
# <div class="stats">
|
325 |
+
# <div class="stat-card">
|
326 |
+
# <div class="stat-number">{len(SESSIONS)}</div>
|
327 |
+
# <div class="stat-label">الجلسات النشطة</div>
|
328 |
+
# </div>
|
329 |
+
# <div class="stat-card">
|
330 |
+
# <div class="stat-number">v1.0</div>
|
331 |
+
# <div class="stat-label">الإصدار</div>
|
332 |
+
# </div>
|
333 |
+
# <div class="stat-card">
|
334 |
+
# <div class="stat-number">FastAPI</div>
|
335 |
+
# <div class="stat-label">التقنية</div>
|
336 |
+
# </div>
|
337 |
+
# </div>
|
338 |
+
|
339 |
+
# <div class="buttons">
|
340 |
+
# <a href="/docs" class="btn btn-primary">📚 توثيق API</a>
|
341 |
+
# <a href="/sessions" class="btn btn-secondary">📊 الجلسات</a>
|
342 |
+
# </div>
|
343 |
+
|
344 |
+
# <div class="features">
|
345 |
+
# <h3>🌟 الميزات الرئيسية</h3>
|
346 |
+
# <ul>
|
347 |
+
# <li>نموذج InterFuser للقيادة الذاتية</li>
|
348 |
+
# <li>معالجة الصور في الوقت الفعلي</li>
|
349 |
+
# <li>اكتشاف الكائنات المرورية</li>
|
350 |
+
# <li>تحديد المسارات الذكية</li>
|
351 |
+
# <li>واجهة RESTful سهلة الاستخدام</li>
|
352 |
+
# <li>إدارة جلسات متعددة</li>
|
353 |
+
# </ul>
|
354 |
+
# </div>
|
355 |
+
# </div>
|
356 |
+
# </body>
|
357 |
+
# </html>
|
358 |
+
# """
|
359 |
+
# return html_content
|
360 |
+
|
361 |
import uuid
|
362 |
import base64
|
363 |
import cv2
|
364 |
import torch
|
365 |
import numpy as np
|
366 |
+
import logging
|
367 |
from fastapi import FastAPI, HTTPException
|
368 |
from fastapi.responses import HTMLResponse
|
369 |
+
from pydantic import BaseModel, Field
|
370 |
+
from typing import List, Dict, Tuple
|
|
|
|
|
371 |
|
372 |
+
# ==============================================================================
|
373 |
+
# 1. استيراد كل مكونات المشروع التي قمنا بتطويرها
|
374 |
+
# (تأكد من أن هذه الملفات موجودة في نفس المجلد)
|
375 |
+
# ==============================================================================
|
376 |
+
# من ملف النموذج (يحتوي على كلاس Interfuser والدوال المساعدة)
|
377 |
from model_definition import InterfuserModel, load_and_prepare_model, create_model_config
|
|
|
|
|
|
|
|
|
|
|
378 |
|
379 |
+
# من ملفات التحكم والعرض
|
380 |
+
from simulation_modules import InterfuserController, Tracker
|
381 |
+
from simulation_modules import DisplayInterface, render_bev, unnormalize_image, DisplayConfig
|
382 |
+
# # استيراد من ملفاتنا المحلية
|
383 |
+
# from model_definition import InterfuserModel, load_and_prepare_model, create_model_config
|
384 |
+
# from simulation_modules import (
|
385 |
+
# InterfuserController, ControllerConfig, Tracker, DisplayInterface,
|
386 |
+
# render, render_waypoints, render_self_car, WAYPOINT_SCALE_FACTOR,
|
387 |
+
# T1_FUTURE_TIME, T2_FUTURE_TIME
|
388 |
+
# )
|
389 |
+
# ==============================================================================
|
390 |
+
# 2. إعدادات عامة وتطبيق FastAPI
|
391 |
+
# ==============================================================================
|
392 |
+
# إعداد التسجيل (Logging)
|
393 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
394 |
|
395 |
+
# تهيئة تطبيق FastAPI
|
396 |
app = FastAPI(
|
397 |
title="Baseer Self-Driving API",
|
398 |
+
description="An advanced API for the InterFuser self-driving model, providing real-time control commands and scene analysis.",
|
399 |
+
version="1.1.0"
|
400 |
)
|
401 |
|
402 |
+
# متغيرات عامة سيتم تهيئتها عند بدء التشغيل
|
403 |
+
MODEL: Interfuser = None
|
404 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
405 |
+
SESSIONS: Dict[str, Dict] = {} # قاموس لتخزين حالة الجلسات النشطة
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
406 |
|
407 |
+
# ==============================================================================
|
408 |
+
# 3. تعريف نماذج البيانات (Pydantic Models) للـ API
|
409 |
+
# ==============================================================================
|
410 |
class Measurements(BaseModel):
|
411 |
+
pos_global: Tuple[float, float] = Field(..., example=(0.0, 0.0), description="Global [X, Y] position of the vehicle.")
|
412 |
+
theta: float = Field(..., example=0.0, description="Global orientation angle of the vehicle in radians.")
|
413 |
+
speed: float = Field(..., example=0.0, description="Current speed in m/s.")
|
414 |
+
target_point: Tuple[float, float] = Field(..., example=(10.0, 0.0), description="Target point relative to the vehicle.")
|
415 |
+
|
416 |
+
class RunStepRequest(BaseModel):
|
417 |
+
session_id: str
|
418 |
+
image_b64: str = Field(..., description="Base64 encoded string of the vehicle's front camera view (BGR format).")
|
419 |
+
measurements: Measurements
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
|
421 |
class ControlCommands(BaseModel):
|
422 |
steer: float
|
423 |
throttle: float
|
424 |
brake: bool
|
425 |
|
426 |
+
class SceneAnalysis(BaseModel):
|
427 |
+
is_junction: float
|
428 |
+
traffic_light_state: float
|
429 |
+
stop_sign: float
|
430 |
|
431 |
+
class RunStepResponse(BaseModel):
|
|
|
432 |
control_commands: ControlCommands
|
433 |
+
scene_analysis: SceneAnalysis
|
434 |
+
predicted_waypoints: List[Tuple[float, float]]
|
435 |
+
dashboard_b64: str = Field(..., description="Base64 encoded string of the comprehensive dashboard view.")
|
436 |
+
reason: str = Field(..., description="The reason for the current control action (e.g., 'Following ID 12', 'Red Light').")
|
437 |
|
438 |
+
# ==============================================================================
|
439 |
+
# 4. دوال مساعدة (Helpers)
|
440 |
+
# ==============================================================================
|
441 |
+
def b64_to_cv2(b64_string: str) -> np.ndarray:
|
442 |
+
try:
|
443 |
+
img_bytes = base64.b64decode(b64_string)
|
444 |
+
img_array = np.frombuffer(img_bytes, dtype=np.uint8)
|
445 |
+
return cv2.imdecode(img_array, cv2.IMREAD_COLOR)
|
446 |
+
except Exception:
|
447 |
+
raise HTTPException(status_code=400, detail="Invalid Base64 image string.")
|
448 |
+
|
449 |
+
def cv2_to_b64(img: np.ndarray) -> str:
|
450 |
+
_, buffer = cv2.imencode('.jpg', img)
|
451 |
+
return base64.b64encode(buffer).decode('utf-8')
|
452 |
|
453 |
+
def prepare_model_input(image: np.ndarray, measurements: Measurements) -> Dict[str, torch.Tensor]:
|
454 |
+
"""
|
455 |
+
إعداد دفعة (batch of 1) لتمريرها إلى النموذج.
|
456 |
+
"""
|
457 |
+
transform = transforms.Compose([
|
458 |
transforms.ToTensor(),
|
459 |
transforms.Resize((224, 224), antialias=True),
|
460 |
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
461 |
])
|
462 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
463 |
+
image_tensor = transform(image_rgb).unsqueeze(0).to(DEVICE)
|
464 |
|
465 |
+
measurements_tensor = torch.tensor([[
|
466 |
+
measurements.pos_global[0], measurements.pos_global[1], measurements.theta,
|
467 |
+
0.0, 0.0, 0.0, # Steer, throttle, brake (not used by model)
|
468 |
+
measurements.speed, 4.0 # Command (default to FollowLane)
|
469 |
+
]], dtype=torch.float32).to(DEVICE)
|
|
|
|
|
|
|
|
|
|
|
|
|
470 |
|
471 |
+
target_point_tensor = torch.tensor([measurements.target_point], dtype=torch.float32).to(DEVICE)
|
472 |
|
473 |
+
return {
|
|
|
474 |
'rgb': image_tensor,
|
475 |
+
'rgb_left': image_tensor.clone(), 'rgb_right': image_tensor.clone(), 'rgb_center': image_tensor.clone(),
|
476 |
+
'measurements': measurements_tensor,
|
477 |
+
'target_point': target_point_tensor,
|
478 |
+
'lidar': torch.zeros_like(image_tensor)
|
479 |
}
|
480 |
|
481 |
+
# ==============================================================================
|
482 |
+
# 5. أحداث دورة حياة التطبيق (Startup/Shutdown)
|
483 |
+
# ==============================================================================
|
484 |
+
@app.on_event("startup")
|
485 |
+
async def startup_event():
|
486 |
+
global MODEL
|
487 |
+
logging.info("🚗 Server starting up...")
|
488 |
+
logging.info(f"Using device: {DEVICE}")
|
489 |
+
MODEL = load_and_prepare_model(DEVICE)
|
490 |
+
if MODEL:
|
491 |
+
logging.info("✅ Model loaded successfully. Server is ready!")
|
492 |
+
else:
|
493 |
+
logging.error("❌ CRITICAL: Model could not be loaded. The API will not function correctly.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
|
495 |
+
# ==============================================================================
|
496 |
+
# 6. نقاط النهاية الرئيسية (API Endpoints)
|
497 |
+
# ==============================================================================
|
498 |
+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
499 |
async def root():
|
500 |
+
# هذا يعرض صفحة رئيسية بسيطة وجميلة للمستخدمين
|
501 |
+
return """
|
502 |
+
<html>
|
503 |
+
<head><title>Baseer API</title></head>
|
504 |
+
<body style='font-family: sans-serif; text-align: center; padding-top: 50px;'>
|
505 |
+
<h1>🚗 Baseer Self-Driving API</h1>
|
506 |
+
<p>Welcome! The API is running.</p>
|
507 |
+
<p>Navigate to <a href="/docs">/docs</a> for the interactive API documentation.</p>
|
508 |
+
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
509 |
</html>
|
510 |
"""
|
|
|
511 |
|
512 |
+
@app.post("/start_session", summary="Start a new driving session", tags=["Session Management"])
|
513 |
+
def start_session():
|
|
|
|
|
|
|
514 |
session_id = str(uuid.uuid4())
|
515 |
+
config = create_model_config()
|
516 |
+
controller_params = config.get('controller_params', {})
|
517 |
+
controller_params.update({'frequency': 10.0}) # Set default frequency
|
518 |
|
|
|
519 |
SESSIONS[session_id] = {
|
520 |
+
'tracker': Tracker(grid_conf=config['grid_conf']),
|
521 |
+
'controller': InterfuserController({'controller_params': controller_params, 'grid_conf': config['grid_conf']}),
|
522 |
+
'frame_num': 0
|
|
|
|
|
523 |
}
|
524 |
+
logging.info(f"New session started: {session_id}")
|
525 |
+
return {"session_id": session_id}
|
|
|
|
|
|
|
|
|
|
|
526 |
|
527 |
+
@app.post("/run_step", response_model=RunStepResponse, summary="Process a single simulation step", tags=["Core"])
|
528 |
+
@torch.no_grad()
|
529 |
+
def run_step(request: RunStepRequest):
|
530 |
+
if MODEL is None:
|
531 |
+
raise HTTPException(status_code=503, detail="Model is not available.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
532 |
|
533 |
+
session = SESSIONS.get(request.session_id)
|
534 |
+
if not session:
|
535 |
+
raise HTTPException(status_code=404, detail="Session ID not found.")
|
536 |
+
|
537 |
+
# --- 1. الإدراك (Perception) ---
|
538 |
+
image = b64_to_cv2(request.image_b64)
|
539 |
+
model_input = prepare_model_input(image, request.measurements)
|
540 |
+
traffic, waypoints, junc, light, stop, _ = MODEL(model_input)
|
541 |
+
|
542 |
+
# --- 2. معالجة مخرجات النموذج ---
|
543 |
+
traffic_processed = torch.cat([torch.sigmoid(traffic[0][:, 0:1]), traffic[0][:, 1:]], dim=1)
|
544 |
+
traffic_np = traffic_processed.cpu().numpy().reshape(20, 20, -1)
|
545 |
+
waypoints_np = waypoints[0].cpu().numpy()
|
546 |
+
junction_prob = torch.softmax(junc, dim=1)[0, 1].item()
|
547 |
+
light_prob = torch.softmax(light, dim=1)[0, 1].item()
|
548 |
+
stop_prob = torch.softmax(stop, dim=1)[0, 1].item()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
549 |
|
550 |
+
# --- 3. التتبع والتحكم ---
|
551 |
+
ego_pos = np.array(request.measurements.pos_global)
|
552 |
+
ego_theta = request.measurements.theta
|
553 |
+
frame_num = session['frame_num']
|
554 |
|
555 |
+
active_tracks = session['tracker'].process_frame(traffic_np, ego_pos, ego_theta, frame_num)
|
556 |
+
steer, throttle, brake, ctrl_info = session['controller'].run_step(
|
557 |
+
speed=request.measurements.speed, waypoints=torch.from_numpy(waypoints_np),
|
558 |
+
junction=junction_prob, traffic_light=light_prob, stop_sign=stop_prob,
|
559 |
+
bev_map=traffic_np, ego_pos=ego_pos, ego_theta=ego_theta, frame_num=frame_num
|
560 |
)
|
561 |
|
562 |
+
# --- 4. إنشاء الواجهة المرئية ---
|
563 |
+
display_iface = DisplayInterface(DisplayConfig(width=1280, height=720))
|
564 |
+
bev_maps = render_bev(active_tracks, waypoints_np, ego_pos, ego_theta)
|
565 |
+
display_data = {
|
566 |
+
'camera_view': image, 'map_t0': bev_maps['t0'], 'map_t1': bev_maps['t1'], 'map_t2': bev_maps['t2'],
|
567 |
+
'frame_num': frame_num, 'speed': request.measurements.speed * 3.6,
|
568 |
+
'target_speed': ctrl_info.get('target_speed', 0) * 3.6,
|
569 |
+
'steer': steer, 'throttle': throttle, 'brake': brake,
|
570 |
+
'light_prob': light_prob, 'stop_prob': stop_prob,
|
571 |
+
'object_counts': {'car': len(active_tracks)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
572 |
}
|
573 |
+
dashboard = display_iface.run_interface(display_data)
|
574 |
|
575 |
+
# --- 5. تحديث الجلسة وإرجاع الرد ---
|
576 |
+
session['frame_num'] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
577 |
|
578 |
+
return RunStepResponse(
|
579 |
+
control_commands=ControlCommands(steer=steer, throttle=throttle, brake=brake),
|
580 |
+
scene_analysis=SceneAnalysis(is_junction=junction_prob, traffic_light_state=light_prob, stop_sign=stop_prob),
|
581 |
+
predicted_waypoints=[tuple(wp) for wp in waypoints_np.tolist()],
|
582 |
+
dashboard_b64=cv2_to_b64(dashboard),
|
583 |
+
reason=ctrl_info.get('brake_reason', "Cruising")
|
584 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
585 |
|
586 |
+
@app.post("/end_session", summary="End and clean up a session", tags=["Session Management"])
|
587 |
+
def end_session(session_id: str):
|
588 |
+
if session_id in SESSIONS:
|
589 |
+
del SESSIONS[session_id]
|
590 |
+
logging.info(f"Session ended: {session_id}")
|
591 |
+
return {"message": f"Session {session_id} ended."}
|
592 |
+
raise HTTPException(status_code=404, detail="Session not found.")
|
593 |
# ================== تشغيل الخادم ==================
|
594 |
+
# if __name__ == "__main__":
|
595 |
+
# import uvicorn
|
596 |
+
# uvicorn.run(app, host="0.0.0.0", port=7860)
|