Marcel0123 commited on
Commit
8594d4d
·
verified ·
1 Parent(s): f1d41b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +9 -276
app.py CHANGED
@@ -1,276 +1,9 @@
1
- import math, random, time
2
- from dataclasses import dataclass, field
3
- from typing import List, Optional, Tuple
4
- import gradio as gr
5
- from PIL import Image, ImageDraw
6
-
7
- W, H = 900, 540
8
- ROOM = (60, 40, W - 60, H - 40)
9
- DT = 0.05
10
- MAX_SPEED_BASE = 65.0
11
- PERSON_RADIUS = 6
12
- NEIGHBOR_RANGE = 55.0
13
- EXIT_W, EXIT_H = 28, 68
14
-
15
- C_BG = (245, 246, 248, 255)
16
- C_ROOM = (250, 250, 250, 255)
17
- C_BORDER = (28, 28, 28, 255)
18
- C_OBS = (175, 178, 185, 255)
19
- C_EXIT = (52, 132, 255, 255)
20
- C_CALM = (44, 184, 78, 255)
21
- C_ALERT = (250, 191, 35, 255)
22
- C_PANIC = (233, 68, 68, 255)
23
- C_INCIDENT = (230, 60, 60, 96)
24
-
25
- RUNNING = False
26
-
27
- @dataclass
28
- class Person:
29
- x: float; y: float; vx: float; vy: float
30
- stress: float = 0.0; state: int = 0; evacuated: bool = False
31
- def color(self): return [C_CALM, C_ALERT, C_PANIC][self.state]
32
-
33
- @dataclass
34
- class ExitZone:
35
- x: int; y: int; w: int = EXIT_W; h: int = EXIT_H
36
- def rect(self): return (self.x, self.y, self.x + self.w, self.y + self.h)
37
-
38
- @dataclass
39
- class World:
40
- width: int = W; height: int = H; room: Tuple[int,int,int,int] = ROOM
41
- people: List[Person] = field(default_factory=list)
42
- exits: List[ExitZone] = field(default_factory=list)
43
- obstacles: List[Tuple[int,int,int,int]] = field(default_factory=list)
44
- incident: Optional[Tuple[int,int,int]] = None
45
- speed_scale: float = 1.0; cohesion: float = 0.3; separation: float = 0.6
46
- panic_spread: float = 2.0; personal_space: float = 22.0
47
- evac_count: int = 0; avg_stress: float = 0.0; collisions_proxy: int = 0
48
-
49
- def reset_metrics(self):
50
- self.evac_count = sum(p.evacuated for p in self.people)
51
- active = [p for p in self.people if not p.evacuated]
52
- self.avg_stress = (sum(p.stress for p in active)/len(active)) if active else 0.0
53
- close = 0
54
- for i in range(len(active)):
55
- for j in range(i+1, len(active)):
56
- dx = active[i].x - active[j].x
57
- dy = active[i].y - active[j].y
58
- if dx*dx + dy*dy < (0.8*self.personal_space)**2:
59
- close += 1
60
- self.collisions_proxy = close
61
-
62
- def step(self, dt: float):
63
- def nearest_exit_vec(px, py):
64
- best=None; best_d2=1e12
65
- for ex in self.exits:
66
- cx, cy = ex.x + ex.w/2, ex.y + ex.h/2
67
- dx, dy = cx - px, cy - py
68
- d2 = dx*dx + dy*dy
69
- if d2 < best_d2: best_d2, best = d2, (dx, dy)
70
- return best or (0.0, 0.0)
71
-
72
- # state updates
73
- for p in self.people:
74
- if p.evacuated: continue
75
- if self.incident:
76
- cx, cy, rad = self.incident
77
- d = math.hypot(p.x - cx, p.y - cy)
78
- if d < rad:
79
- p.stress += (1 - d/rad) * self.panic_spread * 0.18 * dt
80
- for _ in range(6):
81
- q = random.choice(self.people)
82
- if q is p or q.evacuated: continue
83
- dx, dy = q.x - p.x, q.y - p.y
84
- if dx*dx + dy*dy < NEIGHBOR_RANGE**2 and q.state == 2:
85
- p.stress += self.panic_spread * 0.04 * dt
86
- p.stress = max(0.0, min(1.0, p.stress))
87
- p.state = 2 if p.stress > 0.65 else (1 if p.stress > 0.35 else 0)
88
-
89
- # motion
90
- for p in self.people:
91
- if p.evacuated: continue
92
- for ex in self.exits:
93
- x0, y0, x1, y1 = ex.rect()
94
- if x0 <= p.x <= x1 and y0 <= p.y <= y1:
95
- p.evacuated = True
96
- if p.evacuated: continue
97
-
98
- neighbors = random.sample(self.people, k=min(20, len(self.people)))
99
- fx = fy = 0.0
100
- for q in neighbors:
101
- if q is p or q.evacuated: continue
102
- dx, dy = p.x - q.x, p.y - q.y
103
- d2 = dx*dx + dy*dy
104
- if d2 and d2 < (self.personal_space**2):
105
- inv = 1.0 / max(1.0, d2)
106
- fx += dx*inv; fy += dy*inv
107
- cx = cy = cvx = cvy = cnt = 0.0
108
- for q in neighbors:
109
- if q is p or q.evacuated: continue
110
- dx, dy = q.x - p.x, q.y - p.y
111
- if dx*dx + dy*dy < NEIGHBOR_RANGE**2:
112
- cx += q.x; cy += q.y; cvx += q.vx; cvy += q.vy; cnt += 1
113
- if cnt:
114
- cx/=cnt; cy/=cnt; cvx/=cnt; cvy/=cnt
115
- fx += (cx - p.x) * self.cohesion * 0.15
116
- fy += (cy - p.y) * self.cohesion * 0.15
117
- fx += (cvx - p.vx) * 0.05
118
- fy += (cvy - p.vy) * 0.05
119
-
120
- gx, gy = nearest_exit_vec(p.x, p.y)
121
- goal_k = 0.6 if p.state==0 else (0.8 if p.state==1 else 1.1)
122
- fx += gx * goal_k / 200.0
123
- fy += gy * goal_k / 200.0
124
-
125
- for (ox0, oy0, ox1, oy1) in self.obstacles:
126
- margin = self.personal_space*0.7
127
- if (ox0 - margin) < p.x < (ox1 + margin) and (oy0 - margin) < p.y < (oy1 + margin):
128
- dx_left, dx_right = p.x - ox0, ox1 - p.x
129
- dy_top, dy_bot = p.y - oy0, oy1 - p.y
130
- m = min(dx_left, dx_right, dy_top, dy_bot)
131
- if m == dx_left: fx -= 0.8
132
- elif m == dx_right: fx += 0.8
133
- elif m == dy_top: fy -= 0.8
134
- else: fy += 0.8
135
-
136
- x0, y0, x1, y1 = self.room
137
- wm = 8
138
- if p.x < x0+wm: fx += 1.2
139
- if p.x > x1-wm: fx -= 1.2
140
- if p.y < y0+wm: fy += 1.2
141
- if p.y > y1-wm: fy -= 1.2
142
-
143
- p.vx += fx; p.vy += fy
144
- vmax = MAX_SPEED_BASE * self.speed_scale * [1.0,1.2,1.6][p.state]
145
- v = math.hypot(p.vx, p.vy)
146
- if v > vmax:
147
- p.vx, p.vy = p.vx/v*vmax, p.vy/v*vmax
148
- p.x += p.vx*dt; p.y += p.vy*dt
149
- p.x = min(max(p.x, x0+2), x1-2)
150
- p.y = min(max(p.y, y0+2), y1-2)
151
-
152
- self.reset_metrics()
153
-
154
- def render(self):
155
- img = Image.new("RGBA", (self.width, self.height), C_BG)
156
- d = ImageDraw.Draw(img, "RGBA")
157
- d.rectangle(self.room, fill=C_ROOM, outline=C_BORDER, width=3)
158
- if self.incident:
159
- cx, cy, rad = self.incident
160
- d.ellipse((cx-rad, cy-rad, cx+rad, cy+rad), fill=C_INCIDENT)
161
- for (x0,y0,x1,y1) in self.obstacles:
162
- d.rectangle((x0,y0,x1,y1), fill=C_OBS)
163
- for ex in self.exits:
164
- d.rectangle(ex.rect(), fill=C_EXIT)
165
- d.text((ex.x+6, ex.y+ex.h/2-7), "Exit", fill=(255,255,255,255))
166
- for p in self.people:
167
- if p.evacuated: continue
168
- c = p.color()
169
- d.ellipse((p.x-PERSON_RADIUS, p.y-PERSON_RADIUS,
170
- p.x+PERSON_RADIUS, p.y+PERSON_RADIUS), fill=c)
171
- return img
172
-
173
- def make_world(n_people=200, speed=1.2, cohesion=0.3, separation=0.6, panic_spread=2.4):
174
- random.seed(7)
175
- w = World()
176
- w.speed_scale = float(speed); w.cohesion = float(cohesion)
177
- w.separation = float(separation); w.panic_spread = float(panic_spread)
178
- w.personal_space = 22 + 10*separation
179
- ex1 = ExitZone(x=ROOM[2]-8-EXIT_W, y=int(ROOM[1]+70))
180
- ex2 = ExitZone(x=ROOM[2]-8-EXIT_W, y=int(ROOM[3]-70-EXIT_H))
181
- w.exits = [ex1, ex2]
182
- w.obstacles = [
183
- (int(ROOM[0]+220), int(ROOM[1]+110), int(ROOM[0]+360), int(ROOM[1]+150)),
184
- (int(ROOM[0]+280), int(ROOM[3]-150), int(ROOM[0]+420), int(ROOM[3]-110)),
185
- (int(ROOM[2]-140), int(ROOM[1]+160), int(ROOM[2]-110), int(ROOM[1]+260)),
186
- ]
187
- l,t,r,b = ROOM
188
- for _ in range(n_people):
189
- x = min(max(random.gauss((l+r)/2 - 120, 120), l+20), r-40)
190
- y = min(max(random.gauss((t+b)/2, 90), t+20), b-20)
191
- ang = random.random()*2*math.pi
192
- speed0 = random.uniform(5,25)
193
- w.people.append(Person(x=x, y=y, vx=math.cos(ang)*speed0, vy=math.sin(ang)*speed0))
194
- w.reset_metrics()
195
- return w
196
-
197
- def trigger_incident(world: World):
198
- cx = int(ROOM[2]-220); cy = int(ROOM[1]+110); rad = 150
199
- world.incident = (cx, cy, rad)
200
- for p in world.people:
201
- if p.evacuated: continue
202
- d = math.hypot(p.x - cx, p.y - cy)
203
- if d < rad*0.85: p.stress = max(p.stress, 0.7)
204
-
205
- def clear_incident(world: World): world.incident = None
206
-
207
- def reset_fn(n_people, speed, cohesion, separation, panic_spread):
208
- global RUNNING; RUNNING = False
209
- world = make_world(int(n_people), float(speed), float(cohesion), float(separation), float(panic_spread))
210
- frame = world.render()
211
- metrics = {"% Evacuated": f"{(world.evac_count/max(1,len(world.people)))*100:.0f}",
212
- "Avg. Stress": f"{world.avg_stress:.2f}",
213
- "Collisions": f"{world.collisions_proxy}"}
214
- return frame, metrics, world
215
-
216
- def start_incident_fn(world):
217
- world = world or make_world()
218
- trigger_incident(world)
219
- return world.render(), {"% Evacuated": f"{(world.evac_count/max(1,len(world.people)))*100:.0f}",
220
- "Avg. Stress": f"{world.avg_stress:.2f}",
221
- "Collisions": f"{world.collisions_proxy}"}, world
222
-
223
- def clear_incident_fn(world):
224
- world = world or make_world()
225
- clear_incident(world)
226
- return world.render(), {"% Evacuated": f"{(world.evac_count/max(1,len(world.people)))*100:.0f}",
227
- "Avg. Stress": f"{world.avg_stress:.2f}",
228
- "Collisions": f"{world.collisions_proxy}"}, world
229
-
230
- def start_fn(world, n_steps=999999):
231
- global RUNNING; RUNNING = True
232
- world = world or make_world()
233
- steps = 0
234
- while RUNNING and steps < n_steps:
235
- world.step(DT)
236
- frame = world.render()
237
- metrics = {"% Evacuated": f"{(world.evac_count/max(1,len(world.people)))*100:.0f}",
238
- "Avg. Stress": f"{world.avg_stress:.2f}",
239
- "Collisions": f"{world.collisions_proxy}"}
240
- yield frame, metrics, world
241
- steps += 1
242
- time.sleep(DT)
243
-
244
- def pause_fn():
245
- global RUNNING; RUNNING = False
246
- return gr.update(), gr.update(), gr.update()
247
-
248
- with gr.Blocks(title="Behavioral ML in Virtual Crowds") as demo:
249
- gr.Markdown("# Behavioral ML in Virtual Crowds · Evacuatie-simulatie")
250
- with gr.Row():
251
- canvas = gr.Image(label="Simulatie", interactive=False, type="pil")
252
- metrics = gr.Label(label="Live metrics")
253
- with gr.Row():
254
- with gr.Column(scale=2):
255
- n_people = gr.Slider(20, 600, value=200, step=10, label="Aantal mensen")
256
- speed = gr.Slider(0.5, 2.0, value=1.2, step=0.05, label="Snelheid (schaal)")
257
- cohesion = gr.Slider(0.0, 1.0, value=0.3, step=0.05, label="Samenhang")
258
- separation = gr.Slider(0.0, 1.0, value=0.6, step=0.05, label="Persoonlijke ruimte (schaal)")
259
- panic_spread = gr.Slider(0.0, 4.0, value=2.4, step=0.1, label="Paniekverspreiding")
260
- with gr.Column(scale=1):
261
- start_btn = gr.Button("▶️ Start")
262
- pause_btn = gr.Button("⏸️ Pauze")
263
- reset_btn = gr.Button("🔁 Reset")
264
- inc_btn = gr.Button("🚨 Start Incident")
265
- clear_inc_btn = gr.Button("🧯 Clear Incident")
266
- state = gr.State()
267
- reset_btn.click(reset_fn, [n_people, speed, cohesion, separation, panic_spread], [canvas, metrics, state])
268
- init_frame, init_metrics, init_world = reset_fn(200, 1.2, 0.3, 0.6, 2.4)
269
- canvas.value = init_frame; metrics.value = init_metrics; state.value = init_world
270
- start_btn.click(start_fn, [state], [canvas, metrics, state], show_progress=False)
271
- pause_btn.click(pause_fn, outputs=[canvas, metrics, state])
272
- inc_btn.click(start_incident_fn, [state], [canvas, metrics, state])
273
- clear_inc_btn.click(clear_incident_fn, [state], [canvas, metrics, state])
274
-
275
- if __name__ == "__main__":
276
- demo.launch()
 
1
+ ---
2
+ title: Behavioral ML in Virtual Crowds (Smoke Test)
3
+ emoji: 🧪
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---