1use crate::{GpuError, Result};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone)]
12pub struct FrameProcessConfig {
13 pub width: u32,
15 pub height: u32,
17 pub channels: u8,
19}
20
21#[derive(Debug, Clone)]
23pub struct FrameProcessResult {
24 pub data: Vec<u8>,
26 pub width: u32,
28 pub height: u32,
30 pub processing_time_us: u64,
32}
33
34pub struct VideoFrameProcessor {
39 config: FrameProcessConfig,
40}
41
42impl VideoFrameProcessor {
43 #[must_use]
45 pub fn new(config: FrameProcessConfig) -> Self {
46 Self { config }
47 }
48
49 fn timestamp_us() -> u64 {
51 SystemTime::now()
52 .duration_since(UNIX_EPOCH)
53 .unwrap_or_default()
54 .subsec_micros()
55 .into()
56 }
57
58 fn validate_frame(&self, frame: &[u8]) -> Result<()> {
60 let expected = self.config.width as usize
61 * self.config.height as usize
62 * self.config.channels as usize;
63 if frame.len() != expected {
64 return Err(GpuError::InvalidBufferSize {
65 expected,
66 actual: frame.len(),
67 });
68 }
69 Ok(())
70 }
71
72 pub fn compute_histogram(&self, frame: &[u8]) -> Result<Vec<u32>> {
81 self.validate_frame(frame)?;
82
83 let channels = self.config.channels as usize;
84 let mut histogram = vec![0u32; 256 * channels];
85
86 for (i, &pixel) in frame.iter().enumerate() {
87 let ch = i % channels;
88 histogram[ch * 256 + pixel as usize] += 1;
89 }
90
91 Ok(histogram)
92 }
93
94 pub fn adjust_brightness(&self, frame: &[u8], delta: i16) -> Result<Vec<u8>> {
102 self.validate_frame(frame)?;
103
104 let result = frame
105 .iter()
106 .map(|&p| (i16::from(p) + delta).clamp(0, 255) as u8)
107 .collect();
108
109 Ok(result)
110 }
111
112 pub fn adjust_contrast(&self, frame: &[u8], factor: f32) -> Result<Vec<u8>> {
120 self.validate_frame(frame)?;
121
122 let result = frame
123 .iter()
124 .map(|&p| {
125 let adjusted = (f32::from(p) - 128.0) * factor + 128.0;
126 adjusted.clamp(0.0, 255.0) as u8
127 })
128 .collect();
129
130 Ok(result)
131 }
132
133 pub fn adjust_saturation(&self, frame: &[u8], factor: f32) -> Result<Vec<u8>> {
142 self.validate_frame(frame)?;
143
144 if self.config.channels != 3 {
145 return Ok(frame.to_vec());
147 }
148
149 let mut result = Vec::with_capacity(frame.len());
150 for chunk in frame.chunks(3) {
151 let (r, g, b) = (
152 f32::from(chunk[0]) / 255.0,
153 f32::from(chunk[1]) / 255.0,
154 f32::from(chunk[2]) / 255.0,
155 );
156
157 let (h, s, l) = rgb_to_hsl(r, g, b);
158 let new_s = (s * factor).clamp(0.0, 1.0);
159 let (nr, ng, nb) = hsl_to_rgb(h, new_s, l);
160
161 result.push((nr * 255.0).clamp(0.0, 255.0) as u8);
162 result.push((ng * 255.0).clamp(0.0, 255.0) as u8);
163 result.push((nb * 255.0).clamp(0.0, 255.0) as u8);
164 }
165
166 Ok(result)
167 }
168
169 pub fn frame_difference(&self, frame_a: &[u8], frame_b: &[u8]) -> Result<Vec<u8>> {
175 self.validate_frame(frame_a)?;
176 self.validate_frame(frame_b)?;
177
178 let result = frame_a
179 .iter()
180 .zip(frame_b.iter())
181 .map(|(&a, &b)| a.abs_diff(b))
182 .collect();
183
184 Ok(result)
185 }
186
187 pub fn mean_absolute_error(&self, frame_a: &[u8], frame_b: &[u8]) -> Result<f64> {
193 self.validate_frame(frame_a)?;
194 self.validate_frame(frame_b)?;
195
196 if frame_a.is_empty() {
197 return Ok(0.0);
198 }
199
200 let sum: u64 = frame_a
201 .iter()
202 .zip(frame_b.iter())
203 .map(|(&a, &b)| u64::from(a.abs_diff(b)))
204 .sum();
205
206 Ok(sum as f64 / frame_a.len() as f64)
207 }
208
209 #[must_use]
211 pub fn config(&self) -> &FrameProcessConfig {
212 &self.config
213 }
214
215 pub fn process_frame(&self, frame: &[u8], brightness_delta: i16) -> Result<FrameProcessResult> {
224 let start = Self::timestamp_us();
225 let data = self.adjust_brightness(frame, brightness_delta)?;
226 let end = Self::timestamp_us();
227
228 Ok(FrameProcessResult {
229 data,
230 width: self.config.width,
231 height: self.config.height,
232 processing_time_us: end.saturating_sub(start),
233 })
234 }
235}
236
237fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
243 let max = r.max(g).max(b);
244 let min = r.min(g).min(b);
245 let delta = max - min;
246 let l = (max + min) / 2.0;
247
248 if delta < f32::EPSILON {
249 return (0.0, 0.0, l);
250 }
251
252 let s = if l < 0.5 {
253 delta / (max + min)
254 } else {
255 delta / (2.0 - max - min)
256 };
257
258 let h = if (max - r).abs() < f32::EPSILON {
259 ((g - b) / delta).rem_euclid(6.0) / 6.0
260 } else if (max - g).abs() < f32::EPSILON {
261 ((b - r) / delta + 2.0) / 6.0
262 } else {
263 ((r - g) / delta + 4.0) / 6.0
264 };
265
266 (h, s, l)
267}
268
269fn hsl_hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
271 if t < 0.0 {
272 t += 1.0;
273 }
274 if t > 1.0 {
275 t -= 1.0;
276 }
277 if t < 1.0 / 6.0 {
278 return p + (q - p) * 6.0 * t;
279 }
280 if t < 1.0 / 2.0 {
281 return q;
282 }
283 if t < 2.0 / 3.0 {
284 return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
285 }
286 p
287}
288
289fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
291 if s < f32::EPSILON {
292 return (l, l, l);
293 }
294
295 let q = if l < 0.5 {
296 l * (1.0 + s)
297 } else {
298 l + s - l * s
299 };
300 let p = 2.0 * l - q;
301
302 let r = hsl_hue_to_rgb(p, q, h + 1.0 / 3.0);
303 let g = hsl_hue_to_rgb(p, q, h);
304 let b = hsl_hue_to_rgb(p, q, h - 1.0 / 3.0);
305
306 (r, g, b)
307}
308
309#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn make_processor(w: u32, h: u32, ch: u8) -> VideoFrameProcessor {
318 VideoFrameProcessor::new(FrameProcessConfig {
319 width: w,
320 height: h,
321 channels: ch,
322 })
323 }
324
325 #[test]
326 fn test_histogram_uniform_frame() {
327 let proc = make_processor(4, 4, 1);
329 let frame = vec![128u8; 16];
330 let hist = proc
331 .compute_histogram(&frame)
332 .expect("histogram computation should succeed");
333
334 assert_eq!(hist.len(), 256);
335 assert_eq!(hist[128], 16, "All 16 pixels should be at bin 128");
336 for i in 0..256 {
337 if i != 128 {
338 assert_eq!(hist[i], 0);
339 }
340 }
341 }
342
343 #[test]
344 fn test_histogram_rgb_frame() {
345 let proc = make_processor(2, 2, 3);
347 let frame: Vec<u8> = (0..4).flat_map(|_| vec![255u8, 0u8, 128u8]).collect();
348 let hist = proc
349 .compute_histogram(&frame)
350 .expect("histogram computation should succeed");
351
352 assert_eq!(hist.len(), 768); assert_eq!(hist[0 * 256 + 255], 4);
355 assert_eq!(hist[1 * 256 + 0], 4);
357 assert_eq!(hist[2 * 256 + 128], 4);
359 }
360
361 #[test]
362 fn test_adjust_brightness_clamp_up() {
363 let proc = make_processor(2, 2, 1);
364 let frame = vec![200u8, 100u8, 50u8, 10u8];
365 let result = proc
366 .adjust_brightness(&frame, 100)
367 .expect("brightness adjustment should succeed");
368 assert_eq!(result, vec![255, 200, 150, 110]);
369 }
370
371 #[test]
372 fn test_adjust_brightness_clamp_down() {
373 let proc = make_processor(2, 2, 1);
374 let frame = vec![200u8, 100u8, 50u8, 10u8];
375 let result = proc
376 .adjust_brightness(&frame, -100)
377 .expect("brightness adjustment should succeed");
378 assert_eq!(result, vec![100, 0, 0, 0]);
379 }
380
381 #[test]
382 fn test_adjust_contrast() {
383 let proc = make_processor(1, 1, 1);
384 let frame = vec![128u8];
386 let result = proc
387 .adjust_contrast(&frame, 1.0)
388 .expect("contrast adjustment should succeed");
389 assert_eq!(result[0], 128);
390 }
391
392 #[test]
393 fn test_adjust_contrast_increase() {
394 let proc = make_processor(1, 1, 1);
395 let frame = vec![200u8];
397 let result = proc
398 .adjust_contrast(&frame, 2.0)
399 .expect("contrast adjustment should succeed");
400 assert_eq!(result[0], 255);
401 }
402
403 #[test]
404 fn test_adjust_saturation_no_change_at_one() {
405 let proc = make_processor(1, 1, 3);
406 let frame = vec![255u8, 0u8, 0u8]; let result = proc
408 .adjust_saturation(&frame, 1.0)
409 .expect("saturation adjustment should succeed");
410 assert_eq!(result[0], 255);
412 assert_eq!(result[1], 0);
413 assert_eq!(result[2], 0);
414 }
415
416 #[test]
417 fn test_adjust_saturation_zero_desaturates() {
418 let proc = make_processor(1, 1, 3);
419 let frame = vec![255u8, 0u8, 0u8]; let result = proc
421 .adjust_saturation(&frame, 0.0)
422 .expect("saturation adjustment should succeed");
423 assert_eq!(result[0], result[1]);
425 assert_eq!(result[1], result[2]);
426 }
427
428 #[test]
429 fn test_frame_difference() {
430 let proc = make_processor(2, 2, 1);
431 let a = vec![100u8, 200u8, 50u8, 0u8];
432 let b = vec![80u8, 210u8, 50u8, 255u8];
433 let diff = proc
434 .frame_difference(&a, &b)
435 .expect("frame difference should succeed");
436 assert_eq!(diff, vec![20, 10, 0, 255]);
437 }
438
439 #[test]
440 fn test_mean_absolute_error() {
441 let proc = make_processor(2, 2, 1);
442 let a = vec![100u8, 100u8, 100u8, 100u8];
443 let b = vec![110u8, 90u8, 100u8, 120u8];
444 let mae = proc
446 .mean_absolute_error(&a, &b)
447 .expect("MAE computation should succeed");
448 assert!((mae - 10.0).abs() < 1e-9);
449 }
450
451 #[test]
452 fn test_invalid_frame_size() {
453 let proc = make_processor(4, 4, 1);
454 let frame = vec![0u8; 10]; assert!(proc.compute_histogram(&frame).is_err());
456 assert!(proc.adjust_brightness(&frame, 0).is_err());
457 assert!(proc.adjust_contrast(&frame, 1.0).is_err());
458 }
459
460 #[test]
461 fn test_config_accessor() {
462 let config = FrameProcessConfig {
463 width: 1920,
464 height: 1080,
465 channels: 4,
466 };
467 let proc = VideoFrameProcessor::new(config.clone());
468 assert_eq!(proc.config().width, 1920);
469 assert_eq!(proc.config().height, 1080);
470 assert_eq!(proc.config().channels, 4);
471 }
472}