-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Expand file tree
/
Copy pathsec_panic_motion.rs
More file actions
366 lines (328 loc) · 13.1 KB
/
sec_panic_motion.rs
File metadata and controls
366 lines (328 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
//! Panic/erratic motion detection — ADR-041 Category 2 Security module.
//!
//! Detects erratic high-energy movement patterns indicative of distress, struggle,
//! or fleeing. Computes two signals:
//!
//! 1. **Jerk** — rate of change of motion energy (d/dt of velocity proxy).
//! High jerk indicates sudden, erratic changes in movement.
//!
//! 2. **Motion entropy** — unpredictability of motion direction changes.
//! A person walking smoothly has low entropy; someone struggling or
//! panicking exhibits rapid, random direction reversals = high entropy.
//!
//! Both jerk and entropy must exceed their respective thresholds simultaneously
//! over a 5-second window (100 frames at 20 Hz) to trigger an alert.
//!
//! Events: PANIC_DETECTED(250), STRUGGLE_PATTERN(251), FLEEING_DETECTED(252).
//! Budget: S (<5 ms).
#[cfg(not(feature = "std"))]
use libm::{fabsf, sqrtf};
#[cfg(feature = "std")]
fn sqrtf(x: f32) -> f32 { x.sqrt() }
#[cfg(feature = "std")]
fn fabsf(x: f32) -> f32 { x.abs() }
const MAX_SC: usize = 32;
/// Window size for jerk/entropy computation (5 seconds at 20 Hz).
const WINDOW: usize = 100;
/// Jerk threshold (rate of change of motion energy per frame).
const JERK_THRESH: f32 = 2.0;
/// Entropy threshold (direction reversal rate in window).
const ENTROPY_THRESH: f32 = 0.35;
/// Minimum motion energy for detection (ignore idle).
const MIN_MOTION: f32 = 1.0;
/// Minimum presence required.
const MIN_PRESENCE: i32 = 1;
/// Fraction of window frames that must exceed both thresholds.
const TRIGGER_FRAC: f32 = 0.3;
/// Cooldown after event emission.
const COOLDOWN: u16 = 100;
/// Fleeing: sustained high energy threshold.
const FLEE_ENERGY_THRESH: f32 = 5.0;
/// Fleeing: minimum jerk threshold (lower than panic — running is rhythmic not chaotic).
/// Just needs to be above noise floor (person must be actively moving, not just present).
const FLEE_JERK_THRESH: f32 = 0.05;
/// Fleeing: maximum entropy (low = consistent direction, running is directional).
const FLEE_MAX_ENTROPY: f32 = 0.25;
/// Struggle detection: high jerk but moderate total energy (not fleeing).
const STRUGGLE_JERK_THRESH: f32 = 1.5;
pub const EVENT_PANIC_DETECTED: i32 = 250;
pub const EVENT_STRUGGLE_PATTERN: i32 = 251;
pub const EVENT_FLEEING_DETECTED: i32 = 252;
/// Panic/erratic motion detector.
pub struct PanicMotionDetector {
/// Circular buffer of motion energy values.
energy_buf: [f32; WINDOW],
/// Circular buffer of phase variance values (for direction estimation).
variance_buf: [f32; WINDOW],
buf_idx: usize,
buf_filled: bool,
/// Previous motion energy (for jerk computation).
prev_energy: f32,
prev_energy_init: bool,
/// Cooldowns.
cd_panic: u16,
cd_struggle: u16,
cd_fleeing: u16,
frame_count: u32,
/// Total panic events.
panic_count: u32,
}
impl PanicMotionDetector {
pub const fn new() -> Self {
Self {
energy_buf: [0.0; WINDOW],
variance_buf: [0.0; WINDOW],
buf_idx: 0,
buf_filled: false,
prev_energy: 0.0,
prev_energy_init: false,
cd_panic: 0,
cd_struggle: 0,
cd_fleeing: 0,
frame_count: 0,
panic_count: 0,
}
}
/// Process one frame. Returns `(event_id, value)` pairs.
pub fn process_frame(
&mut self,
motion_energy: f32,
variance_mean: f32,
_phase_mean: f32,
presence: i32,
) -> &[(i32, f32)] {
self.frame_count += 1;
self.cd_panic = self.cd_panic.saturating_sub(1);
self.cd_struggle = self.cd_struggle.saturating_sub(1);
self.cd_fleeing = self.cd_fleeing.saturating_sub(1);
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
let mut ne = 0usize;
// Store in circular buffer.
self.energy_buf[self.buf_idx] = motion_energy;
self.variance_buf[self.buf_idx] = variance_mean;
self.buf_idx = (self.buf_idx + 1) % WINDOW;
if self.buf_idx == 0 {
self.buf_filled = true;
}
// Need full window before analysis.
if !self.buf_filled {
self.prev_energy = motion_energy;
self.prev_energy_init = true;
return unsafe { &EVENTS[..0] };
}
// Require presence.
if presence < MIN_PRESENCE {
self.prev_energy = motion_energy;
return unsafe { &EVENTS[..0] };
}
// Compute jerk (absolute rate of change of motion energy).
let _jerk = if self.prev_energy_init {
fabsf(motion_energy - self.prev_energy)
} else {
0.0
};
// Compute window statistics.
let (mean_jerk, mean_energy, entropy, high_jerk_frac) =
self.compute_window_stats();
self.prev_energy = motion_energy;
self.prev_energy_init = true;
// Skip if not enough motion.
if mean_energy < MIN_MOTION {
return unsafe { &EVENTS[..0] };
}
// Panic detection: high jerk AND high entropy over threshold fraction of window.
let is_panic = mean_jerk > JERK_THRESH
&& entropy > ENTROPY_THRESH
&& high_jerk_frac > TRIGGER_FRAC;
if is_panic && self.cd_panic == 0 && ne < 3 {
let severity = (mean_jerk / JERK_THRESH) * (entropy / ENTROPY_THRESH);
unsafe { EVENTS[ne] = (EVENT_PANIC_DETECTED, severity.min(10.0)); }
ne += 1;
self.cd_panic = COOLDOWN;
self.panic_count += 1;
}
// Struggle pattern: elevated jerk, moderate energy (person not displacing far).
// Does not require high_jerk_frac (individual jerks may be below JERK_THRESH
// but the *mean* jerk is still elevated from constant direction reversals).
let is_struggle = mean_jerk > STRUGGLE_JERK_THRESH
&& mean_energy < FLEE_ENERGY_THRESH
&& mean_energy > MIN_MOTION
&& entropy > ENTROPY_THRESH * 0.5;
if is_struggle && !is_panic && self.cd_struggle == 0 && ne < 3 {
unsafe { EVENTS[ne] = (EVENT_STRUGGLE_PATTERN, mean_jerk); }
ne += 1;
self.cd_struggle = COOLDOWN;
}
// Fleeing detection: sustained high energy with low entropy (running in one direction).
// Running produces rhythmic jerk but consistent direction (low entropy).
let is_fleeing = mean_energy > FLEE_ENERGY_THRESH
&& mean_jerk > FLEE_JERK_THRESH
&& entropy < FLEE_MAX_ENTROPY;
if is_fleeing && !is_panic && self.cd_fleeing == 0 && ne < 3 {
unsafe { EVENTS[ne] = (EVENT_FLEEING_DETECTED, mean_energy); }
ne += 1;
self.cd_fleeing = COOLDOWN;
}
unsafe { &EVENTS[..ne] }
}
/// Compute window-level statistics.
fn compute_window_stats(&self) -> (f32, f32, f32, f32) {
let mut sum_jerk = 0.0f32;
let mut sum_energy = 0.0f32;
let mut direction_changes = 0u32;
let mut high_jerk_count = 0u32;
let mut prev_e = self.energy_buf[0];
let mut prev_sign = 0i8; // +1 increasing, -1 decreasing, 0 unknown.
for k in 1..WINDOW {
let e = self.energy_buf[k];
let j = fabsf(e - prev_e);
sum_jerk += j;
sum_energy += e;
if j > JERK_THRESH {
high_jerk_count += 1;
}
// Track direction reversals for entropy.
let sign: i8 = if e > prev_e + 0.1 {
1
} else if e < prev_e - 0.1 {
-1
} else {
prev_sign // Unchanged.
};
if prev_sign != 0 && sign != 0 && sign != prev_sign {
direction_changes += 1;
}
prev_sign = sign;
prev_e = e;
}
let n = (WINDOW - 1) as f32;
let mean_jerk = sum_jerk / n;
let mean_energy = sum_energy / n;
// Entropy proxy: fraction of frames with direction reversals.
let entropy = direction_changes as f32 / n;
let high_jerk_frac = high_jerk_count as f32 / n;
(mean_jerk, mean_energy, entropy, high_jerk_frac)
}
pub fn frame_count(&self) -> u32 { self.frame_count }
pub fn panic_count(&self) -> u32 { self.panic_count }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init() {
let det = PanicMotionDetector::new();
assert_eq!(det.frame_count(), 0);
assert_eq!(det.panic_count(), 0);
}
#[test]
fn test_no_events_before_window_filled() {
let mut det = PanicMotionDetector::new();
for i in 0..(WINDOW - 1) {
let ev = det.process_frame(5.0 + (i as f32) * 0.1, 1.0, 0.5, 1);
assert!(ev.is_empty(), "no events before window is filled");
}
}
#[test]
fn test_calm_motion_no_panic() {
let mut det = PanicMotionDetector::new();
// Fill window with smooth, consistent motion.
for i in 0..200u32 {
let energy = 2.0 + (i as f32) * 0.01; // Slowly increasing.
let ev = det.process_frame(energy, 0.1, 0.5, 1);
for &(et, _) in ev {
assert_ne!(et, EVENT_PANIC_DETECTED, "calm motion should not trigger panic");
}
}
}
#[test]
fn test_panic_detection() {
let mut det = PanicMotionDetector::new();
// Fill buffer with erratic, high-jerk motion.
let mut found_panic = false;
for i in 0..300u32 {
// Alternating high and low energy = high jerk + high entropy.
let energy = if i % 2 == 0 { 8.0 } else { 1.5 };
let ev = det.process_frame(energy, 1.0, 0.5, 1);
for &(et, _) in ev {
if et == EVENT_PANIC_DETECTED {
found_panic = true;
}
}
}
assert!(found_panic, "erratic alternating motion should trigger panic");
assert!(det.panic_count() >= 1);
}
#[test]
fn test_no_panic_without_presence() {
let mut det = PanicMotionDetector::new();
for i in 0..300u32 {
let energy = if i % 2 == 0 { 8.0 } else { 1.5 };
let ev = det.process_frame(energy, 1.0, 0.5, 0); // No presence.
for &(et, _) in ev {
assert_ne!(et, EVENT_PANIC_DETECTED, "no panic without presence");
}
}
}
#[test]
fn test_fleeing_detection() {
let mut det = PanicMotionDetector::new();
// Simulate fleeing: sustained high energy, mostly monotonic (low entropy).
// Person is running in one direction: energy steadily rises with small jitter.
let mut found_fleeing = false;
for i in 0..300u32 {
// Steadily increasing energy: 6.0 up to ~12.0 over 300 frames.
// Jitter of +/- 0.05 does not reverse direction often => low entropy.
// Mean energy ~ 9.0 > FLEE_ENERGY_THRESH (5.0).
// Mean jerk ~ 0.02/frame + occasional 0.1 jitter = ~0.05.
// But FLEE_JERK_THRESH = 0.3, so we need slightly more jerk.
// Add a small step every 10 frames.
let base = 6.0 + (i as f32) * 0.02;
let step = if i % 10 == 0 { 0.5 } else { 0.0 };
let energy = base + step;
let ev = det.process_frame(energy, 0.5, 0.5, 1);
for &(et, _) in ev {
if et == EVENT_FLEEING_DETECTED {
found_fleeing = true;
}
}
}
assert!(found_fleeing, "sustained high energy should trigger fleeing");
}
#[test]
fn test_struggle_pattern() {
let mut det = PanicMotionDetector::new();
// Simulate struggle: moderate jerk (above STRUGGLE_JERK_THRESH=1.5 but
// below JERK_THRESH=2.0 or with insufficient high_jerk_frac for panic),
// moderate energy (below FLEE_ENERGY_THRESH=5.0), some direction changes.
// Pattern: 3.0, 1.2, 3.0, 1.2, ... => jerk = 1.8 per transition.
// Mean jerk = 1.8 > 1.5 (struggle threshold).
// Mean jerk = 1.8 < 2.0 (panic threshold), so panic won't fire.
// Mean energy = 2.1 > MIN_MOTION=1.0 and < FLEE_ENERGY_THRESH=5.0.
// Entropy: alternates every frame => ~0.5 > ENTROPY_THRESH*0.5=0.175.
let mut found_struggle = false;
for i in 0..300u32 {
let energy = if i % 2 == 0 { 3.0 } else { 1.2 };
let ev = det.process_frame(energy, 0.5, 0.5, 1);
for &(et, _) in ev {
if et == EVENT_STRUGGLE_PATTERN {
found_struggle = true;
}
}
}
assert!(found_struggle, "moderate energy with high jerk should trigger struggle");
}
#[test]
fn test_low_motion_ignored() {
let mut det = PanicMotionDetector::new();
// Very low motion energy — below MIN_MOTION.
for _ in 0..300 {
let ev = det.process_frame(0.2, 0.01, 0.1, 1);
for &(et, _) in ev {
assert_ne!(et, EVENT_PANIC_DETECTED);
assert_ne!(et, EVENT_STRUGGLE_PATTERN);
assert_ne!(et, EVENT_FLEEING_DETECTED);
}
}
}
}