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.compute_histogram(&frame).unwrap();
331
332 assert_eq!(hist.len(), 256);
333 assert_eq!(hist[128], 16, "All 16 pixels should be at bin 128");
334 for i in 0..256 {
335 if i != 128 {
336 assert_eq!(hist[i], 0);
337 }
338 }
339 }
340
341 #[test]
342 fn test_histogram_rgb_frame() {
343 let proc = make_processor(2, 2, 3);
345 let frame: Vec<u8> = (0..4).flat_map(|_| vec![255u8, 0u8, 128u8]).collect();
346 let hist = proc.compute_histogram(&frame).unwrap();
347
348 assert_eq!(hist.len(), 768); assert_eq!(hist[0 * 256 + 255], 4);
351 assert_eq!(hist[1 * 256 + 0], 4);
353 assert_eq!(hist[2 * 256 + 128], 4);
355 }
356
357 #[test]
358 fn test_adjust_brightness_clamp_up() {
359 let proc = make_processor(2, 2, 1);
360 let frame = vec![200u8, 100u8, 50u8, 10u8];
361 let result = proc.adjust_brightness(&frame, 100).unwrap();
362 assert_eq!(result, vec![255, 200, 150, 110]);
363 }
364
365 #[test]
366 fn test_adjust_brightness_clamp_down() {
367 let proc = make_processor(2, 2, 1);
368 let frame = vec![200u8, 100u8, 50u8, 10u8];
369 let result = proc.adjust_brightness(&frame, -100).unwrap();
370 assert_eq!(result, vec![100, 0, 0, 0]);
371 }
372
373 #[test]
374 fn test_adjust_contrast() {
375 let proc = make_processor(1, 1, 1);
376 let frame = vec![128u8];
378 let result = proc.adjust_contrast(&frame, 1.0).unwrap();
379 assert_eq!(result[0], 128);
380 }
381
382 #[test]
383 fn test_adjust_contrast_increase() {
384 let proc = make_processor(1, 1, 1);
385 let frame = vec![200u8];
387 let result = proc.adjust_contrast(&frame, 2.0).unwrap();
388 assert_eq!(result[0], 255);
389 }
390
391 #[test]
392 fn test_adjust_saturation_no_change_at_one() {
393 let proc = make_processor(1, 1, 3);
394 let frame = vec![255u8, 0u8, 0u8]; let result = proc.adjust_saturation(&frame, 1.0).unwrap();
396 assert_eq!(result[0], 255);
398 assert_eq!(result[1], 0);
399 assert_eq!(result[2], 0);
400 }
401
402 #[test]
403 fn test_adjust_saturation_zero_desaturates() {
404 let proc = make_processor(1, 1, 3);
405 let frame = vec![255u8, 0u8, 0u8]; let result = proc.adjust_saturation(&frame, 0.0).unwrap();
407 assert_eq!(result[0], result[1]);
409 assert_eq!(result[1], result[2]);
410 }
411
412 #[test]
413 fn test_frame_difference() {
414 let proc = make_processor(2, 2, 1);
415 let a = vec![100u8, 200u8, 50u8, 0u8];
416 let b = vec![80u8, 210u8, 50u8, 255u8];
417 let diff = proc.frame_difference(&a, &b).unwrap();
418 assert_eq!(diff, vec![20, 10, 0, 255]);
419 }
420
421 #[test]
422 fn test_mean_absolute_error() {
423 let proc = make_processor(2, 2, 1);
424 let a = vec![100u8, 100u8, 100u8, 100u8];
425 let b = vec![110u8, 90u8, 100u8, 120u8];
426 let mae = proc.mean_absolute_error(&a, &b).unwrap();
428 assert!((mae - 10.0).abs() < 1e-9);
429 }
430
431 #[test]
432 fn test_invalid_frame_size() {
433 let proc = make_processor(4, 4, 1);
434 let frame = vec![0u8; 10]; assert!(proc.compute_histogram(&frame).is_err());
436 assert!(proc.adjust_brightness(&frame, 0).is_err());
437 assert!(proc.adjust_contrast(&frame, 1.0).is_err());
438 }
439
440 #[test]
441 fn test_config_accessor() {
442 let config = FrameProcessConfig {
443 width: 1920,
444 height: 1080,
445 channels: 4,
446 };
447 let proc = VideoFrameProcessor::new(config.clone());
448 assert_eq!(proc.config().width, 1920);
449 assert_eq!(proc.config().height, 1080);
450 assert_eq!(proc.config().channels, 4);
451 }
452}