1use crate::error::{IoError, Result};
11use crate::image::{ColorMode, ImageData, ImageFormat};
12use scirs2_core::ndarray::{Array3, ArrayView1};
13use scirs2_core::simd_ops::SimdUnifiedOps;
14use std::collections::HashMap;
15use std::path::Path;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum CompressionQuality {
20 Lossless,
22 High,
24 Medium,
26 Low,
28 Custom(u8),
30}
31
32impl CompressionQuality {
33 pub fn value(&self) -> u8 {
35 match self {
36 CompressionQuality::Lossless => 100,
37 CompressionQuality::High => 95,
38 CompressionQuality::Medium => 80,
39 CompressionQuality::Low => 60,
40 CompressionQuality::Custom(v) => (*v).min(100),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct CompressionOptions {
48 pub quality: CompressionQuality,
50 pub progressive: bool,
52 pub optimize: bool,
54 pub compression_level: Option<u8>,
56}
57
58impl Default for CompressionOptions {
59 fn default() -> Self {
60 Self {
61 quality: CompressionQuality::High,
62 progressive: false,
63 optimize: true,
64 compression_level: None,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct PyramidConfig {
72 pub levels: usize,
74 pub scale_factor: f64,
76 pub min_size: u32,
78 pub interpolation: InterpolationMethod,
80}
81
82impl Default for PyramidConfig {
83 fn default() -> Self {
84 Self {
85 levels: 4,
86 scale_factor: 0.5,
87 min_size: 32,
88 interpolation: InterpolationMethod::Lanczos,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum InterpolationMethod {
96 Nearest,
98 Linear,
100 Cubic,
102 Lanczos,
104}
105
106#[derive(Debug, Clone)]
108pub struct ImagePyramid {
109 pub original: ImageData,
111 pub levels: Vec<ImageData>,
113 pub config: PyramidConfig,
115}
116
117#[derive(Debug, Clone)]
119pub struct EnhancedImageProcessor {
120 pub compression: CompressionOptions,
122 cache: HashMap<String, ImageData>,
124 max_cache_size: usize,
126}
127
128impl Default for EnhancedImageProcessor {
129 fn default() -> Self {
130 Self {
131 compression: CompressionOptions::default(),
132 cache: HashMap::new(),
133 max_cache_size: 256, }
135 }
136}
137
138impl EnhancedImageProcessor {
139 pub fn new() -> Self {
141 Self::default()
142 }
143
144 pub fn with_compression(mut self, compression: CompressionOptions) -> Self {
146 self.compression = compression;
147 self
148 }
149
150 pub fn with_cache_size(mut self, size_mb: usize) -> Self {
152 self.max_cache_size = size_mb;
153 self
154 }
155
156 pub fn create_pyramid(&self, image: &ImageData, config: PyramidConfig) -> Result<ImagePyramid> {
158 let mut levels = Vec::new();
159 let mut current_image = image.clone();
160
161 for level in 1..=config.levels {
162 let scale = config.scale_factor.powi(level as i32);
163 let new_width =
164 ((image.metadata.width as f64) * scale).max(config.min_size as f64) as u32;
165 let new_height =
166 ((image.metadata.height as f64) * scale).max(config.min_size as f64) as u32;
167
168 if new_width < config.min_size || new_height < config.min_size {
170 break;
171 }
172
173 current_image = self.resize_with_interpolation(
174 ¤t_image,
175 new_width,
176 new_height,
177 config.interpolation,
178 )?;
179 levels.push(current_image.clone());
180 }
181
182 Ok(ImagePyramid {
183 original: image.clone(),
184 levels,
185 config,
186 })
187 }
188
189 pub fn resize_with_interpolation(
191 &self,
192 image: &ImageData,
193 new_width: u32,
194 new_height: u32,
195 method: InterpolationMethod,
196 ) -> Result<ImageData> {
197 let (_height, width, channels) = image.data.dim();
198 let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
199
200 let img_buffer = if channels == 3 {
201 image::RgbImage::from_raw(width as u32, _height as u32, raw_data)
202 .ok_or_else(|| IoError::FormatError("Invalid RGB image dimensions".to_string()))?
203 } else {
204 return Err(IoError::FormatError(
205 "Unsupported number of channels".to_string(),
206 ));
207 };
208
209 let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
210
211 let filter = match method {
212 InterpolationMethod::Nearest => image::imageops::FilterType::Nearest,
213 InterpolationMethod::Linear => image::imageops::FilterType::Triangle,
214 InterpolationMethod::Cubic => image::imageops::FilterType::CatmullRom,
215 InterpolationMethod::Lanczos => image::imageops::FilterType::Lanczos3,
216 };
217
218 let resized_img = dynamic_img.resize(new_width, new_height, filter);
219 let rgb_img = resized_img.to_rgb8();
220 let resized_raw = rgb_img.into_raw();
221
222 let resized_data = Array3::from_shape_vec(
223 (new_height as usize, new_width as usize, channels),
224 resized_raw,
225 )
226 .map_err(|e| IoError::FormatError(e.to_string()))?;
227
228 let mut new_metadata = image.metadata.clone();
229 new_metadata.width = new_width;
230 new_metadata.height = new_height;
231
232 Ok(ImageData {
233 data: resized_data,
234 metadata: new_metadata,
235 })
236 }
237
238 pub fn save_with_compression<P: AsRef<Path>>(
240 &self,
241 image: &ImageData,
242 path: P,
243 format: ImageFormat,
244 compression: Option<CompressionOptions>,
245 ) -> Result<()> {
246 let path = path.as_ref();
247 let compression = compression.unwrap_or(self.compression.clone());
248
249 let (height, width_, _) = image.data.dim();
250 let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
251
252 let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
253 .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
254
255 let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
256
257 match format {
258 ImageFormat::PNG => {
259 dynamic_img
261 .save_with_format(path, image::ImageFormat::Png)
262 .map_err(|e| IoError::FileError(e.to_string()))?;
263 }
264 ImageFormat::JPEG => {
265 let file =
267 std::fs::File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
268 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
269 file,
270 compression.quality.value(),
271 );
272 if compression.progressive {
273 }
276 encoder
277 .encode(
278 dynamic_img.as_bytes(),
279 width_ as u32,
280 height as u32,
281 image::ColorType::Rgb8.into(),
282 )
283 .map_err(|e| IoError::FileError(e.to_string()))?;
284 }
285 ImageFormat::WEBP => {
286 if compression.quality == CompressionQuality::Lossless {
288 dynamic_img
290 .save_with_format(path, image::ImageFormat::WebP)
291 .map_err(|e| IoError::FileError(e.to_string()))?;
292 } else {
293 dynamic_img
295 .save_with_format(path, image::ImageFormat::WebP)
296 .map_err(|e| IoError::FileError(e.to_string()))?;
297 }
298 }
299 _ => {
300 dynamic_img
302 .save_with_format(path, format.into())
303 .map_err(|e| IoError::FileError(e.to_string()))?;
304 }
305 }
306
307 Ok(())
308 }
309
310 pub fn to_grayscale(&self, image: &ImageData) -> Result<ImageData> {
312 let (height, width, channels) = image.data.dim();
313
314 if channels != 3 {
315 return Err(IoError::FormatError("Expected RGB image".to_string()));
316 }
317
318 let mut gray_data = Array3::zeros((height, width, 3));
319
320 for y in 0..height {
322 let _row_size = width * 3;
324 let mut r_values = vec![0f32; width];
325 let mut g_values = vec![0f32; width];
326 let mut b_values = vec![0f32; width];
327
328 for x in 0..width {
330 r_values[x] = image.data[[y, x, 0]] as f32;
331 g_values[x] = image.data[[y, x, 1]] as f32;
332 b_values[x] = image.data[[y, x, 2]] as f32;
333 }
334
335 let r_coeff = vec![0.299f32; width];
337 let g_coeff = vec![0.587f32; width];
338 let b_coeff = vec![0.114f32; width];
339
340 let _r_weighted = vec![0f32; width];
343 let _g_weighted = vec![0f32; width];
344 let _b_weighted = vec![0f32; width];
345
346 let r_values_view = ArrayView1::from(&r_values);
347 let g_values_view = ArrayView1::from(&g_values);
348 let b_values_view = ArrayView1::from(&b_values);
349 let r_coeff_view = ArrayView1::from(&r_coeff);
350 let g_coeff_view = ArrayView1::from(&g_coeff);
351 let b_coeff_view = ArrayView1::from(&b_coeff);
352
353 let r_weighted = f32::simd_mul(&r_values_view, &r_coeff_view);
354 let g_weighted = f32::simd_mul(&g_values_view, &g_coeff_view);
355 let b_weighted = f32::simd_mul(&b_values_view, &b_coeff_view);
356
357 let r_weighted_view = ArrayView1::from(&r_weighted);
359 let g_weighted_view = ArrayView1::from(&g_weighted);
360 let b_weighted_view = ArrayView1::from(&b_weighted);
361
362 let gray_values = f32::simd_add(&r_weighted_view, &g_weighted_view);
363 let gray_values_view = ArrayView1::from(&gray_values);
364 let gray_values_final = f32::simd_add(&gray_values_view, &b_weighted_view);
365
366 for x in 0..width {
368 let gray = gray_values_final[x].clamp(0.0, 255.0) as u8;
369 gray_data[[y, x, 0]] = gray;
370 gray_data[[y, x, 1]] = gray;
371 gray_data[[y, x, 2]] = gray;
372 }
373 }
374
375 let mut new_metadata = image.metadata.clone();
376 new_metadata.color_mode = ColorMode::Grayscale;
377
378 Ok(ImageData {
379 data: gray_data,
380 metadata: new_metadata,
381 })
382 }
383
384 pub fn histogram_equalization(&self, image: &ImageData) -> Result<ImageData> {
386 let (height, width, channels) = image.data.dim();
387 let mut enhanced_data = image.data.clone();
388
389 for c in 0..channels {
390 let mut histogram = [0u32; 256];
392 for y in 0..height {
393 for x in 0..width {
394 let pixel = image.data[[y, x, c]] as usize;
395 histogram[pixel] += 1;
396 }
397 }
398
399 let mut cdf = [0u32; 256];
401 cdf[0] = histogram[0];
402 for i in 1..256 {
403 cdf[i] = cdf[i - 1] + histogram[i];
404 }
405
406 let total_pixels = (height * width) as f32;
408 let mut lookup = [0u8; 256];
409 for i in 0..256 {
410 lookup[i] = ((cdf[i] as f32 / total_pixels) * 255.0) as u8;
411 }
412
413 for y in 0..height {
415 for x in 0..width {
416 let pixel = image.data[[y, x, c]] as usize;
417 enhanced_data[[y, x, c]] = lookup[pixel];
418 }
419 }
420 }
421
422 Ok(ImageData {
423 data: enhanced_data,
424 metadata: image.metadata.clone(),
425 })
426 }
427
428 pub fn gaussian_blur(&self, image: &ImageData, radius: f32) -> Result<ImageData> {
430 let (height, width_, _) = image.data.dim();
431 let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
432
433 let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
434 .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
435
436 let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
437 let blurred = dynamic_img.blur(radius);
438 let rgb_blurred = blurred.to_rgb8();
439 let blurred_raw = rgb_blurred.into_raw();
440
441 let blurred_data = Array3::from_shape_vec((height, width_, 3), blurred_raw)
442 .map_err(|e| IoError::FormatError(e.to_string()))?;
443
444 Ok(ImageData {
445 data: blurred_data,
446 metadata: image.metadata.clone(),
447 })
448 }
449
450 pub fn sharpen(&self, image: &ImageData, amount: f32, radius: f32) -> Result<ImageData> {
452 let blurred = self.gaussian_blur(image, radius)?;
454
455 let (height, width, channels) = image.data.dim();
456 let mut sharpened_data = Array3::zeros((height, width, channels));
457
458 for y in 0..height {
460 for x in 0..width {
461 for c in 0..channels {
462 let original = image.data[[y, x, c]] as f32;
463 let blur = blurred.data[[y, x, c]] as f32;
464 let difference = original - blur;
465 let sharpened = original + amount * difference;
466 sharpened_data[[y, x, c]] = sharpened.clamp(0.0, 255.0) as u8;
467 }
468 }
469 }
470
471 Ok(ImageData {
472 data: sharpened_data,
473 metadata: image.metadata.clone(),
474 })
475 }
476
477 pub fn clear_cache(&mut self) {
479 self.cache.clear();
480 }
481
482 pub fn cache_stats(&self) -> (usize, usize) {
484 (self.cache.len(), self.max_cache_size)
485 }
486}
487
488impl From<ImageFormat> for image::ImageFormat {
490 fn from(format: ImageFormat) -> Self {
491 match format {
492 ImageFormat::PNG => image::ImageFormat::Png,
493 ImageFormat::JPEG => image::ImageFormat::Jpeg,
494 ImageFormat::BMP => image::ImageFormat::Bmp,
495 ImageFormat::TIFF => image::ImageFormat::Tiff,
496 ImageFormat::GIF => image::ImageFormat::Gif,
497 ImageFormat::WEBP => image::ImageFormat::WebP,
498 ImageFormat::Other => image::ImageFormat::Png, }
500 }
501}
502
503impl ImagePyramid {
504 pub fn get_level(&self, level: usize) -> Option<&ImageData> {
506 if level == 0 {
507 Some(&self.original)
508 } else {
509 self.levels.get(level - 1)
510 }
511 }
512
513 pub fn num_levels(&self) -> usize {
515 self.levels.len() + 1
516 }
517
518 pub fn find_best_level(&self, target_width: u32, target_height: u32) -> usize {
520 let mut best_level = 0;
521 let mut best_diff = u32::MAX;
522
523 for level in 0..self.num_levels() {
524 if let Some(level_image) = self.get_level(level) {
525 let width_diff = level_image.metadata.width.abs_diff(target_width);
526 let height_diff = level_image.metadata.height.abs_diff(target_height);
527 let total_diff = width_diff + height_diff;
528
529 if total_diff < best_diff {
530 best_diff = total_diff;
531 best_level = level;
532 }
533 }
534 }
535
536 best_level
537 }
538
539 pub fn get_level_for_size(&self, target_width: u32, target_height: u32) -> Option<&ImageData> {
541 let level = self.find_best_level(target_width, target_height);
542 self.get_level(level)
543 }
544}
545
546#[allow(dead_code)]
549pub fn create_image_pyramid(image: &ImageData) -> Result<ImagePyramid> {
550 let processor = EnhancedImageProcessor::new();
551 processor.create_pyramid(image, PyramidConfig::default())
552}
553
554#[allow(dead_code)]
556pub fn save_lossless<P: AsRef<Path>>(
557 image: &ImageData,
558 path: P,
559 format: ImageFormat,
560) -> Result<()> {
561 let processor = EnhancedImageProcessor::new();
562 let compression = CompressionOptions {
563 quality: CompressionQuality::Lossless,
564 progressive: false,
565 optimize: true,
566 compression_level: None,
567 };
568 processor.save_with_compression(image, path, format, Some(compression))
569}
570
571#[allow(dead_code)]
573pub fn save_high_quality<P: AsRef<Path>>(
574 image: &ImageData,
575 path: P,
576 format: ImageFormat,
577) -> Result<()> {
578 let processor = EnhancedImageProcessor::new();
579 let compression = CompressionOptions {
580 quality: CompressionQuality::High,
581 progressive: true,
582 optimize: true,
583 compression_level: Some(9),
584 };
585 processor.save_with_compression(image, path, format, Some(compression))
586}
587
588#[allow(dead_code)]
590pub fn batch_convert_with_compression<P1: AsRef<Path>, P2: AsRef<Path>>(
591 input_dir: P1,
592 output_dir: P2,
593 target_format: ImageFormat,
594 compression: CompressionOptions,
595) -> Result<()> {
596 use crate::image::{find_images, load_image};
597 use std::fs;
598
599 let input_dir = input_dir.as_ref();
600 let output_dir = output_dir.as_ref();
601
602 fs::create_dir_all(output_dir).map_err(|e| IoError::FileError(e.to_string()))?;
604
605 let processor = EnhancedImageProcessor::new().with_compression(compression);
606 let image_files = find_images(input_dir, "*", false)?;
607
608 for input_path in image_files {
609 let file_stem = input_path
610 .file_stem()
611 .ok_or_else(|| IoError::FileError("Invalid file name".to_string()))?;
612 let output_filename = format!(
613 "{}.{}",
614 file_stem.to_string_lossy(),
615 target_format.extension()
616 );
617 let output_path = output_dir.join(output_filename);
618
619 let image_data = load_image(&input_path)?;
620 processor.save_with_compression(&image_data, &output_path, target_format, None)?;
621
622 println!(
623 "Converted: {} -> {} ({})",
624 input_path.display(),
625 output_path.display(),
626 target_format.extension().to_uppercase()
627 );
628 }
629
630 Ok(())
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::image::ImageMetadata;
637 use scirs2_core::ndarray::Array3;
638
639 fn create_test_image() -> ImageData {
640 let data = Array3::zeros((100, 100, 3));
641 let metadata = ImageMetadata {
642 width: 100,
643 height: 100,
644 color_mode: ColorMode::RGB,
645 format: ImageFormat::PNG,
646 file_size: 0,
647 exif: None,
648 };
649 ImageData { data, metadata }
650 }
651
652 #[test]
653 fn test_compression_quality_values() {
654 assert_eq!(CompressionQuality::Lossless.value(), 100);
655 assert_eq!(CompressionQuality::High.value(), 95);
656 assert_eq!(CompressionQuality::Medium.value(), 80);
657 assert_eq!(CompressionQuality::Low.value(), 60);
658 assert_eq!(CompressionQuality::Custom(75).value(), 75);
659 assert_eq!(CompressionQuality::Custom(150).value(), 100); }
661
662 #[test]
663 fn test_pyramid_config_default() {
664 let config = PyramidConfig::default();
665 assert_eq!(config.levels, 4);
666 assert_eq!(config.scale_factor, 0.5);
667 assert_eq!(config.min_size, 32);
668 assert_eq!(config.interpolation, InterpolationMethod::Lanczos);
669 }
670
671 #[test]
672 fn test_enhanced_processor_creation() {
673 let processor = EnhancedImageProcessor::new();
674 assert_eq!(processor.compression.quality.value(), 95); assert!(processor.compression.optimize);
676 }
677
678 #[test]
679 fn test_processor_with_compression() {
680 let compression = CompressionOptions {
681 quality: CompressionQuality::Lossless,
682 progressive: true,
683 optimize: false,
684 compression_level: Some(5),
685 };
686
687 let processor = EnhancedImageProcessor::new().with_compression(compression.clone());
688 assert_eq!(processor.compression.quality.value(), 100);
689 assert!(processor.compression.progressive);
690 assert!(!processor.compression.optimize);
691 assert_eq!(processor.compression.compression_level, Some(5));
692 }
693
694 #[test]
695 fn test_interpolation_methods() {
696 assert_eq!(InterpolationMethod::Nearest, InterpolationMethod::Nearest);
697 assert_ne!(InterpolationMethod::Nearest, InterpolationMethod::Linear);
698 }
699
700 #[test]
701 fn test_image_pyramid_creation() {
702 let image = create_test_image();
703 let config = PyramidConfig {
704 levels: 2,
705 scale_factor: 0.5,
706 min_size: 10,
707 interpolation: InterpolationMethod::Linear,
708 };
709
710 let processor = EnhancedImageProcessor::new();
711 let pyramid = processor
712 .create_pyramid(&image, config)
713 .expect("Operation failed");
714
715 assert_eq!(pyramid.original.metadata.width, 100);
716 assert_eq!(pyramid.original.metadata.height, 100);
717 assert!(pyramid.levels.len() <= 2);
718 }
719
720 #[test]
721 fn test_pyramid_level_access() {
722 let image = create_test_image();
723 let processor = EnhancedImageProcessor::new();
724 let pyramid = processor
725 .create_pyramid(&image, PyramidConfig::default())
726 .expect("Operation failed");
727
728 assert!(pyramid.get_level(0).is_some());
730 assert_eq!(
731 pyramid
732 .get_level(0)
733 .expect("Operation failed")
734 .metadata
735 .width,
736 100
737 );
738
739 assert!(pyramid.num_levels() >= 1);
741 }
742
743 #[test]
744 fn test_find_best_pyramid_level() {
745 let image = create_test_image();
746 let processor = EnhancedImageProcessor::new();
747 let pyramid = processor
748 .create_pyramid(&image, PyramidConfig::default())
749 .expect("Operation failed");
750
751 let best_level = pyramid.find_best_level(100, 100);
753 assert_eq!(best_level, 0);
754
755 let best_level = pyramid.find_best_level(10, 10);
757 assert!(best_level > 0 || pyramid.num_levels() == 1);
758 }
759
760 #[test]
761 fn test_cache_operations() {
762 let mut processor = EnhancedImageProcessor::new().with_cache_size(128);
763 let (count, max_size) = processor.cache_stats();
764 assert_eq!(count, 0);
765 assert_eq!(max_size, 128);
766
767 processor.clear_cache();
768 let count_ = processor.cache_stats();
769 assert_eq!(count, 0);
770 }
771}