Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,276 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 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 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|