1use crate::{GpuError, Result};
8
9#[derive(Debug, Clone, Copy)]
18pub enum Sensitivity {
19 Low,
21 Medium,
23 High,
25}
26
27impl Sensitivity {
28 #[must_use]
30 pub fn threshold(&self) -> u8 {
31 match self {
32 Self::Low => 30,
33 Self::Medium => 15,
34 Self::High => 8,
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
45pub struct MotionRegion {
46 pub x: u32,
48 pub y: u32,
50 pub width: u32,
52 pub height: u32,
54 pub magnitude: f32,
56 pub changed_pixels: u32,
58}
59
60#[derive(Debug, Clone)]
66pub struct MotionAnalysis {
67 pub global_magnitude: f32,
69 pub changed_pixel_ratio: f32,
71 pub regions: Vec<MotionRegion>,
73 pub motion_detected: bool,
75}
76
77pub struct MotionDetector {
89 sensitivity: Sensitivity,
90 region_count_x: u32,
92 region_count_y: u32,
94 prev_frame: Option<Vec<u8>>,
95}
96
97impl MotionDetector {
98 #[must_use]
106 pub fn new(sensitivity: Sensitivity, region_count_x: u32, region_count_y: u32) -> Self {
107 Self {
108 sensitivity,
109 region_count_x: region_count_x.max(1),
110 region_count_y: region_count_y.max(1),
111 prev_frame: None,
112 }
113 }
114
115 pub fn analyze(&mut self, frame: &[u8], width: u32, height: u32) -> Result<MotionAnalysis> {
134 if width == 0 || height == 0 {
135 return Err(GpuError::InvalidDimensions { width, height });
136 }
137
138 let Some(prev) = self.prev_frame.take() else {
140 self.prev_frame = Some(frame.to_vec());
141 return Ok(MotionAnalysis {
142 global_magnitude: 0.0,
143 changed_pixel_ratio: 0.0,
144 regions: vec![],
145 motion_detected: false,
146 });
147 };
148
149 if prev.len() != frame.len() {
150 self.prev_frame = Some(frame.to_vec());
152 return Err(GpuError::InvalidBufferSize {
153 expected: prev.len(),
154 actual: frame.len(),
155 });
156 }
157
158 let threshold = self.sensitivity.threshold();
159 let total_pixels = frame.len() as u32;
160
161 let (total_changed, total_diff_sum) =
163 prev.iter()
164 .zip(frame.iter())
165 .fold((0u32, 0u64), |(changed, sum), (&p, &c)| {
166 let diff = p.abs_diff(c);
167 if diff >= threshold {
168 (changed + 1, sum + u64::from(diff))
169 } else {
170 (changed, sum)
171 }
172 });
173
174 let global_magnitude = if total_pixels > 0 {
175 (total_diff_sum as f64 / (f64::from(total_pixels) * 255.0)) as f32
176 } else {
177 0.0
178 };
179 let changed_pixel_ratio = total_changed as f32 / total_pixels as f32;
180 let motion_detected = total_changed > 0;
181
182 let regions = self.compute_regions(frame, &prev, width, height, threshold, total_pixels);
184
185 self.prev_frame = Some(frame.to_vec());
187
188 Ok(MotionAnalysis {
189 global_magnitude,
190 changed_pixel_ratio,
191 regions,
192 motion_detected,
193 })
194 }
195
196 #[allow(clippy::too_many_arguments)]
198 fn compute_regions(
199 &self,
200 frame: &[u8],
201 prev: &[u8],
202 width: u32,
203 height: u32,
204 threshold: u8,
205 total_pixels: u32,
206 ) -> Vec<MotionRegion> {
207 let rx = self.region_count_x;
208 let ry = self.region_count_y;
209
210 let bytes_per_row = frame.len() / height.max(1) as usize;
212
213 let mut regions = Vec::with_capacity((rx * ry) as usize);
214
215 for ry_idx in 0..ry {
216 for rx_idx in 0..rx {
217 let region_x = rx_idx * width / rx;
218 let region_y = ry_idx * height / ry;
219 let region_w = (rx_idx + 1) * width / rx - region_x;
220 let region_h = (ry_idx + 1) * height / ry - region_y;
221
222 let mut changed = 0u32;
223 let mut diff_sum = 0u64;
224 let mut region_pixels = 0u32;
225
226 for row in region_y..(region_y + region_h) {
227 let row_start = row as usize * bytes_per_row;
228 let col_byte_start =
230 row_start + (region_x as usize * bytes_per_row / width.max(1) as usize);
231 let col_bytes = region_w as usize * bytes_per_row / width.max(1) as usize;
232
233 let end = (col_byte_start + col_bytes).min(frame.len());
234 for i in col_byte_start..end {
235 let diff = frame[i].abs_diff(prev[i]);
236 region_pixels += 1;
237 if diff >= threshold {
238 changed += 1;
239 diff_sum += u64::from(diff);
240 }
241 }
242 }
243
244 let magnitude = if total_pixels > 0 && region_pixels > 0 {
245 (diff_sum as f64 / (f64::from(region_pixels) * 255.0)) as f32
246 } else {
247 0.0
248 };
249
250 regions.push(MotionRegion {
251 x: region_x,
252 y: region_y,
253 width: region_w,
254 height: region_h,
255 magnitude,
256 changed_pixels: changed,
257 });
258 }
259 }
260
261 regions
262 }
263
264 pub fn reset(&mut self) {
266 self.prev_frame = None;
267 }
268
269 #[must_use]
271 pub fn region_count(&self) -> u32 {
272 self.region_count_x * self.region_count_y
273 }
274}
275
276#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_sensitivity_thresholds() {
286 assert_eq!(Sensitivity::Low.threshold(), 30);
287 assert_eq!(Sensitivity::Medium.threshold(), 15);
288 assert_eq!(Sensitivity::High.threshold(), 8);
289 }
290
291 #[test]
292 fn test_first_frame_returns_no_motion() {
293 let mut detector = MotionDetector::new(Sensitivity::Medium, 2, 2);
294 let frame = vec![100u8; 16]; let result = detector.analyze(&frame, 4, 4).unwrap();
296
297 assert_eq!(result.global_magnitude, 0.0);
298 assert_eq!(result.changed_pixel_ratio, 0.0);
299 assert!(!result.motion_detected);
300 assert!(result.regions.is_empty());
301 }
302
303 #[test]
304 fn test_identical_frames_returns_no_motion() {
305 let mut detector = MotionDetector::new(Sensitivity::Medium, 2, 2);
306 let frame = vec![100u8; 16];
307
308 detector.analyze(&frame, 4, 4).unwrap();
310
311 let result = detector.analyze(&frame, 4, 4).unwrap();
313 assert_eq!(result.global_magnitude, 0.0);
314 assert_eq!(result.changed_pixel_ratio, 0.0);
315 assert!(!result.motion_detected);
316 }
317
318 #[test]
319 fn test_different_frames_returns_motion() {
320 let mut detector = MotionDetector::new(Sensitivity::High, 1, 1);
321 let frame_a = vec![0u8; 16];
322 let frame_b = vec![255u8; 16];
323
324 detector.analyze(&frame_a, 4, 4).unwrap();
325 let result = detector.analyze(&frame_b, 4, 4).unwrap();
326
327 assert!(result.motion_detected);
328 assert!(result.global_magnitude > 0.0);
329 assert!(result.changed_pixel_ratio > 0.0);
330 assert_eq!(result.changed_pixel_ratio, 1.0);
331 }
332
333 #[test]
334 fn test_region_count() {
335 let detector = MotionDetector::new(Sensitivity::Low, 4, 3);
336 assert_eq!(detector.region_count(), 12);
337 }
338
339 #[test]
340 fn test_reset_clears_previous_frame() {
341 let mut detector = MotionDetector::new(Sensitivity::Medium, 1, 1);
342 let frame_a = vec![0u8; 16];
343 let frame_b = vec![255u8; 16];
344
345 detector.analyze(&frame_a, 4, 4).unwrap();
346 detector.reset();
347
348 let result = detector.analyze(&frame_b, 4, 4).unwrap();
350 assert!(!result.motion_detected);
351 }
352
353 #[test]
354 fn test_below_threshold_not_detected() {
355 let mut detector = MotionDetector::new(Sensitivity::Low, 1, 1);
357 let frame_a = vec![100u8; 16];
358 let frame_b = vec![110u8; 16]; detector.analyze(&frame_a, 4, 4).unwrap();
361 let result = detector.analyze(&frame_b, 4, 4).unwrap();
362
363 assert!(!result.motion_detected);
364 }
365
366 #[test]
367 fn test_invalid_dimensions() {
368 let mut detector = MotionDetector::new(Sensitivity::Medium, 1, 1);
369 let frame = vec![0u8; 16];
370 assert!(detector.analyze(&frame, 0, 4).is_err());
371 assert!(detector.analyze(&frame, 4, 0).is_err());
372 }
373}