1use super::config::PerformanceConfig;
10use super::heatmap::Rgb;
11use super::metrics::{CieDe2000Metric, Lab, SsimMetric};
12
13#[derive(Debug, Clone, Default)]
15pub struct ParallelContext {
16 config: PerformanceConfig,
18}
19
20impl ParallelContext {
21 #[must_use]
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 #[must_use]
29 pub fn with_config(config: PerformanceConfig) -> Self {
30 Self { config }
31 }
32
33 #[must_use]
35 pub fn is_parallel(&self) -> bool {
36 self.config.parallel
37 }
38
39 #[must_use]
41 pub fn thread_count(&self) -> usize {
42 if self.config.threads == 0 {
43 num_cpus()
44 } else {
45 self.config.threads
46 }
47 }
48}
49
50#[must_use]
52pub fn num_cpus() -> usize {
53 std::thread::available_parallelism()
55 .map(|n| n.get())
56 .unwrap_or(1)
57}
58
59pub fn parallel_map<T, U, F>(items: &[T], f: F) -> Vec<U>
64where
65 T: Sync,
66 U: Send,
67 F: Fn(&T) -> U + Sync,
68{
69 items.iter().map(f).collect()
71}
72
73#[allow(dead_code)]
78pub fn parallel_reduce<T, U, M, R>(items: &[T], identity: U, map_fn: M, reduce_fn: R) -> U
79where
80 T: Sync,
81 U: Send + Clone,
82 M: Fn(&T) -> U + Sync,
83 R: Fn(U, U) -> U + Sync,
84{
85 items.iter().map(map_fn).fold(identity, reduce_fn)
87}
88
89#[allow(dead_code)]
91pub fn parallel_sum<T, F>(items: &[T], f: F) -> f32
92where
93 T: Sync,
94 F: Fn(&T) -> f32 + Sync,
95{
96 parallel_reduce(items, 0.0, f, |a, b| a + b)
97}
98
99#[derive(Debug)]
101pub struct BatchProcessor {
102 #[allow(dead_code)]
104 batch_size: usize,
105 ctx: ParallelContext,
107}
108
109impl Default for BatchProcessor {
110 fn default() -> Self {
111 Self {
112 batch_size: 1024,
113 ctx: ParallelContext::default(),
114 }
115 }
116}
117
118impl BatchProcessor {
119 #[must_use]
121 pub fn new(batch_size: usize) -> Self {
122 Self {
123 batch_size,
124 ..Default::default()
125 }
126 }
127
128 #[must_use]
130 pub fn with_context(mut self, ctx: ParallelContext) -> Self {
131 self.ctx = ctx;
132 self
133 }
134
135 #[must_use]
137 pub fn compute_delta_e_batch(
138 &self,
139 reference: &[Rgb],
140 generated: &[Rgb],
141 metric: &CieDe2000Metric,
142 ) -> DeltaEBatchResult {
143 if reference.len() != generated.len() {
144 return DeltaEBatchResult::default();
145 }
146
147 let pairs: Vec<_> = reference.iter().zip(generated.iter()).collect();
148
149 let delta_es: Vec<f32> = parallel_map(&pairs, |(r, g)| {
151 let lab1 = Lab::from_rgb(r);
152 let lab2 = Lab::from_rgb(g);
153 metric.delta_e(&lab1, &lab2)
154 });
155
156 let sum: f32 = delta_es.iter().sum();
158 let max = delta_es.iter().cloned().fold(0.0f32, f32::max);
159 let count = delta_es.len();
160
161 let imperceptible = delta_es
162 .iter()
163 .filter(|&&de| de < metric.jnd_threshold)
164 .count();
165 let acceptable = delta_es
166 .iter()
167 .filter(|&&de| de < metric.accept_threshold)
168 .count();
169
170 DeltaEBatchResult {
171 mean: if count > 0 { sum / count as f32 } else { 0.0 },
172 max,
173 count,
174 imperceptible_count: imperceptible,
175 acceptable_count: acceptable,
176 }
177 }
178
179 #[must_use]
181 pub fn compute_ssim_batched(
182 &self,
183 reference: &[Rgb],
184 generated: &[Rgb],
185 width: u32,
186 height: u32,
187 metric: &SsimMetric,
188 ) -> SsimBatchResult {
189 if reference.len() != generated.len() {
190 return SsimBatchResult::default();
191 }
192
193 let result = metric.compare(reference, generated, width, height);
196
197 SsimBatchResult {
198 score: result.score,
199 channel_scores: result.channel_scores,
200 batches_processed: 1,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Default)]
207pub struct DeltaEBatchResult {
208 pub mean: f32,
210 pub max: f32,
212 pub count: usize,
214 pub imperceptible_count: usize,
216 pub acceptable_count: usize,
218}
219
220#[derive(Debug, Clone, Default)]
222pub struct SsimBatchResult {
223 pub score: f32,
225 pub channel_scores: [f32; 3],
227 pub batches_processed: usize,
229}
230
231#[derive(Debug, Clone)]
233pub struct Downscaler {
234 factor: u32,
236}
237
238impl Default for Downscaler {
239 fn default() -> Self {
240 Self { factor: 2 }
241 }
242}
243
244impl Downscaler {
245 #[must_use]
247 pub fn new(factor: u32) -> Self {
248 Self {
249 factor: factor.max(1),
250 }
251 }
252
253 #[must_use]
255 pub fn downscale(&self, image: &[Rgb], width: u32, height: u32) -> (Vec<Rgb>, u32, u32) {
256 let new_width = width / self.factor;
257 let new_height = height / self.factor;
258
259 if new_width == 0 || new_height == 0 {
260 return (image.to_vec(), width, height);
261 }
262
263 let mut result = Vec::with_capacity((new_width * new_height) as usize);
264
265 for y in 0..new_height {
266 for x in 0..new_width {
267 let mut r_sum = 0u32;
269 let mut g_sum = 0u32;
270 let mut b_sum = 0u32;
271 let mut count = 0u32;
272
273 for dy in 0..self.factor {
274 for dx in 0..self.factor {
275 let src_x = x * self.factor + dx;
276 let src_y = y * self.factor + dy;
277 if src_x < width && src_y < height {
278 let idx = (src_y * width + src_x) as usize;
279 if idx < image.len() {
280 r_sum += image[idx].r as u32;
281 g_sum += image[idx].g as u32;
282 b_sum += image[idx].b as u32;
283 count += 1;
284 }
285 }
286 }
287 }
288
289 if count > 0 {
290 result.push(Rgb::new(
291 (r_sum / count) as u8,
292 (g_sum / count) as u8,
293 (b_sum / count) as u8,
294 ));
295 }
296 }
297 }
298
299 (result, new_width, new_height)
300 }
301}
302
303#[derive(Debug, Default)]
305pub struct HashCache {
306 cache: std::collections::HashMap<u64, u64>,
308}
309
310impl HashCache {
311 #[must_use]
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 #[must_use]
319 pub fn get(&self, image_hash: u64) -> Option<u64> {
320 self.cache.get(&image_hash).copied()
321 }
322
323 pub fn insert(&mut self, image_hash: u64, phash: u64) {
325 self.cache.insert(image_hash, phash);
326 }
327
328 pub fn clear(&mut self) {
330 self.cache.clear();
331 }
332
333 #[must_use]
335 pub fn len(&self) -> usize {
336 self.cache.len()
337 }
338
339 #[must_use]
341 pub fn is_empty(&self) -> bool {
342 self.cache.is_empty()
343 }
344
345 #[must_use]
347 pub fn compute_image_hash(image: &[Rgb]) -> u64 {
348 let mut hash: u64 = 0xcbf29ce484222325;
350 for pixel in image {
351 hash ^= pixel.r as u64;
352 hash = hash.wrapping_mul(0x100000001b3);
353 hash ^= pixel.g as u64;
354 hash = hash.wrapping_mul(0x100000001b3);
355 hash ^= pixel.b as u64;
356 hash = hash.wrapping_mul(0x100000001b3);
357 }
358 hash
359 }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used)]
364mod tests {
365 use super::*;
366
367 fn test_image(size: usize, value: u8) -> Vec<Rgb> {
368 vec![Rgb::new(value, value, value); size]
369 }
370
371 #[test]
376 fn h0_par_01_default_context() {
377 let ctx = ParallelContext::new();
378 assert!(ctx.is_parallel());
379 assert!(ctx.thread_count() >= 1);
380 }
381
382 #[test]
383 fn h0_par_02_custom_threads() {
384 let config = PerformanceConfig {
385 threads: 4,
386 ..Default::default()
387 };
388 let ctx = ParallelContext::with_config(config);
389 assert_eq!(ctx.thread_count(), 4);
390 }
391
392 #[test]
393 fn h0_par_03_parallel_map() {
394 let items = vec![1, 2, 3, 4, 5];
395 let result: Vec<i32> = parallel_map(&items, |x| x * 2);
396 assert_eq!(result, vec![2, 4, 6, 8, 10]);
397 }
398
399 #[test]
400 fn h0_par_04_parallel_sum() {
401 let items = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
402 let result = parallel_sum(&items, |&x| x);
403 assert!((result - 15.0).abs() < f32::EPSILON);
404 }
405
406 #[test]
407 fn h0_par_05_parallel_reduce() {
408 let items = vec![1, 2, 3, 4];
409 let result = parallel_reduce(&items, 0, |&x| x, |a, b| a + b);
410 assert_eq!(result, 10);
411 }
412
413 #[test]
418 fn h0_batch_01_delta_e_same() {
419 let img = test_image(100, 128);
420 let processor = BatchProcessor::default();
421 let metric = CieDe2000Metric::default();
422 let result = processor.compute_delta_e_batch(&img, &img, &metric);
423 assert!(result.mean < f32::EPSILON);
424 assert_eq!(result.count, 100);
425 }
426
427 #[test]
428 fn h0_batch_02_delta_e_different() {
429 let img1 = test_image(100, 100);
430 let img2 = test_image(100, 150);
431 let processor = BatchProcessor::default();
432 let metric = CieDe2000Metric::default();
433 let result = processor.compute_delta_e_batch(&img1, &img2, &metric);
434 assert!(result.mean > 0.0);
435 assert_eq!(result.count, 100);
436 }
437
438 #[test]
439 fn h0_batch_03_ssim_same() {
440 let img = test_image(100, 128);
441 let processor = BatchProcessor::default();
442 let metric = SsimMetric::default();
443 let result = processor.compute_ssim_batched(&img, &img, 10, 10, &metric);
444 assert!(result.score >= 0.99);
445 }
446
447 #[test]
452 fn h0_down_01_downscale_2x() {
453 let img = test_image(100, 128); let downscaler = Downscaler::new(2);
455 let (result, w, h) = downscaler.downscale(&img, 10, 10);
456 assert_eq!(w, 5);
457 assert_eq!(h, 5);
458 assert_eq!(result.len(), 25);
459 }
460
461 #[test]
462 fn h0_down_02_downscale_preserves_color() {
463 let img = test_image(16, 200); let downscaler = Downscaler::new(2);
465 let (result, _, _) = downscaler.downscale(&img, 4, 4);
466 assert_eq!(result[0].r, 200);
468 }
469
470 #[test]
471 fn h0_down_03_downscale_factor_1() {
472 let img = test_image(25, 100);
473 let downscaler = Downscaler::new(1);
474 let (result, w, h) = downscaler.downscale(&img, 5, 5);
475 assert_eq!(w, 5);
476 assert_eq!(h, 5);
477 assert_eq!(result.len(), 25);
478 }
479
480 #[test]
485 fn h0_cache_01_insert_get() {
486 let mut cache = HashCache::new();
487 cache.insert(12345, 67890);
488 assert_eq!(cache.get(12345), Some(67890));
489 assert_eq!(cache.get(99999), None);
490 }
491
492 #[test]
493 fn h0_cache_02_clear() {
494 let mut cache = HashCache::new();
495 cache.insert(1, 1);
496 cache.insert(2, 2);
497 assert_eq!(cache.len(), 2);
498 cache.clear();
499 assert!(cache.is_empty());
500 }
501
502 #[test]
503 fn h0_cache_03_image_hash() {
504 let img1 = test_image(100, 128);
505 let img2 = test_image(100, 128);
506 let img3 = test_image(100, 129);
507
508 let hash1 = HashCache::compute_image_hash(&img1);
509 let hash2 = HashCache::compute_image_hash(&img2);
510 let hash3 = HashCache::compute_image_hash(&img3);
511
512 assert_eq!(hash1, hash2); assert_ne!(hash1, hash3); }
515
516 #[test]
517 fn h0_cache_04_empty() {
518 let cache = HashCache::new();
519 assert!(cache.is_empty());
520 assert_eq!(cache.len(), 0);
521 }
522
523 #[test]
528 fn h0_par_06_num_cpus() {
529 let cpus = num_cpus();
530 assert!(cpus >= 1);
531 }
532}