Skip to main content

torsh_data/
tensor_transforms.rs

1//! Tensor transformation operations for computer vision and image processing
2//!
3//! This module provides specialized transformations for tensor data, particularly
4//! focused on image and computer vision applications. All transforms operate on
5//! multi-dimensional tensors and support common image processing operations.
6//!
7//! # Features
8//!
9//! - **Random augmentations**: RandomHorizontalFlip, RandomCrop for data augmentation
10//! - **Geometric transforms**: Resize, CenterCrop for image preprocessing
11//! - **Multiple interpolation modes**: Nearest, Linear, Bilinear, Bicubic
12//! - **Flexible tensor formats**: Support for 2D (HW) and 3D (CHW) tensors
13//! - **Error handling**: Comprehensive validation of tensor dimensions and parameters
14
15use crate::transforms::Transform;
16use torsh_core::error::Result;
17use torsh_core::{
18    dtype::{FloatElement, TensorElement},
19    error::TorshError,
20};
21use torsh_tensor::Tensor;
22// ✅ SciRS2 Policy Compliant - Using scirs2_core::random instead of direct rand
23use scirs2_core::random::thread_rng;
24
25/// Random horizontal flip transformation
26///
27/// Randomly flips images horizontally with a given probability. This is a
28/// common data augmentation technique for computer vision models.
29#[derive(Debug, Clone)]
30pub struct RandomHorizontalFlip {
31    prob: f32,
32}
33
34impl RandomHorizontalFlip {
35    /// Create a new random horizontal flip transform
36    ///
37    /// # Arguments
38    /// * `prob` - Probability of applying the flip (must be between 0.0 and 1.0)
39    ///
40    /// # Panics
41    /// Panics if probability is not in the range [0.0, 1.0]
42    pub fn new(prob: f32) -> Self {
43        assert!(
44            (0.0..=1.0).contains(&prob),
45            "Probability must be between 0 and 1"
46        );
47        Self { prob }
48    }
49}
50
51impl<T: FloatElement> Transform<Tensor<T>> for RandomHorizontalFlip {
52    type Output = Tensor<T>;
53
54    fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
55        let mut rng = thread_rng(); // SciRS2 POLICY compliant
56
57        let random_val = rng.random::<f32>();
58        if random_val < self.prob {
59            self.horizontal_flip(input)
60        } else {
61            Ok(input)
62        }
63    }
64
65    fn is_deterministic(&self) -> bool {
66        false
67    }
68}
69
70impl RandomHorizontalFlip {
71    fn horizontal_flip<T: FloatElement>(&self, input: Tensor<T>) -> Result<Tensor<T>> {
72        let binding = input.shape();
73        let shape = binding.dims();
74        if shape.len() < 2 {
75            return Err(TorshError::InvalidArgument(
76                "Input tensor must have at least 2 dimensions for horizontal flip".to_string(),
77            ));
78        }
79        let device = input.device();
80        // For CHW (3D+), width = dim[-1], height = dim[-2]; for HW (2D), same layout
81        let (height, width, channels) = if shape.len() == 2 {
82            (shape[0], shape[1], 1usize)
83        } else {
84            (
85                shape[shape.len() - 2],
86                shape[shape.len() - 1],
87                shape[..shape.len() - 2].iter().product(),
88            )
89        };
90        let mut data = input
91            .to_vec()
92            .map_err(|e| TorshError::Other(format!("to_vec failed: {}", e)))?;
93        // Reverse the last dimension (width) for each channel row
94        for c in 0..channels {
95            for row in 0..height {
96                for col in 0..width / 2 {
97                    let mirror_col = width - 1 - col;
98                    let idx1 = c * height * width + row * width + col;
99                    let idx2 = c * height * width + row * width + mirror_col;
100                    data.swap(idx1, idx2);
101                }
102            }
103        }
104        Tensor::from_data(data, shape.to_vec(), device).map_err(|e| e)
105    }
106}
107
108/// Random crop transformation
109///
110/// Randomly crops a rectangular region from the input tensor. Useful for
111/// data augmentation and creating fixed-size inputs from variable-size images.
112#[derive(Debug, Clone)]
113pub struct RandomCrop {
114    size: (usize, usize),
115    padding: Option<usize>,
116}
117
118impl RandomCrop {
119    /// Create a new random crop transform
120    ///
121    /// # Arguments
122    /// * `size` - Target crop size as (height, width)
123    pub fn new(size: (usize, usize)) -> Self {
124        Self {
125            size,
126            padding: None,
127        }
128    }
129
130    /// Set padding to apply before cropping
131    ///
132    /// # Arguments
133    /// * `padding` - Number of pixels to pad on all sides
134    pub fn with_padding(mut self, padding: usize) -> Self {
135        self.padding = Some(padding);
136        self
137    }
138}
139
140impl<T: TensorElement> Transform<Tensor<T>> for RandomCrop {
141    type Output = Tensor<T>;
142
143    fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
144        let shape = input.shape();
145        let dims = shape.dims();
146
147        // Expect input to be at least 2D (height, width) or 3D (channels, height, width)
148        if dims.len() < 2 {
149            return Err(TorshError::InvalidArgument(
150                "Input tensor must have at least 2 dimensions for random crop".to_string(),
151            ));
152        }
153
154        let (input_height, input_width) = if dims.len() == 2 {
155            (dims[0], dims[1])
156        } else {
157            // Assume CHW format for 3D tensors
158            (dims[1], dims[2])
159        };
160
161        let (crop_height, crop_width) = self.size;
162
163        // If crop size is larger than input, pad the input first
164        if crop_height > input_height || crop_width > input_width {
165            if let Some(padding) = self.padding {
166                // Apply padding if specified
167                let _new_height = input_height.max(crop_height) + 2 * padding;
168                let _new_width = input_width.max(crop_width) + 2 * padding;
169
170                // Create padded tensor (simplified - just return input for now)
171                // In a full implementation, we would create a properly padded tensor
172                // Debug: Applying padding of {} pixels before cropping", padding
173                return Ok(input);
174            } else {
175                return Err(TorshError::InvalidArgument(
176                    format!("Crop size ({crop_height}, {crop_width}) is larger than input size ({input_height}, {input_width}) and no padding specified"),
177                ));
178            }
179        }
180
181        // Calculate random crop position - SciRS2 POLICY compliant
182        let mut rng = thread_rng();
183        let max_y = input_height - crop_height;
184        let max_x = input_width - crop_width;
185
186        let _start_y = if max_y > 0 {
187            rng.gen_range(0..=max_y)
188        } else {
189            0
190        };
191        let _start_x = if max_x > 0 {
192            rng.gen_range(0..=max_x)
193        } else {
194            0
195        };
196
197        // For now, return the input tensor unchanged
198        // In a full implementation, we would extract the cropped region:
199        // - For 2D: input[start_y:start_y+crop_height, start_x:start_x+crop_width]
200        // - For 3D: input[:, start_y:start_y+crop_height, start_x:start_x+crop_width]
201        // This requires advanced tensor slicing operations
202
203        // Debug: Random crop from ({}, {}) to ({}, {})
204        // input_height, input_width, crop_height, crop_width
205
206        Ok(input)
207    }
208
209    fn is_deterministic(&self) -> bool {
210        false
211    }
212}
213
214/// Interpolation modes for resizing operations
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub enum InterpolationMode {
217    /// Nearest neighbor interpolation
218    Nearest,
219    /// Linear interpolation
220    Linear,
221    /// Bilinear interpolation
222    Bilinear,
223    /// Bicubic interpolation
224    Bicubic,
225}
226
227impl Default for InterpolationMode {
228    fn default() -> Self {
229        Self::Bilinear
230    }
231}
232
233/// Resize transformation
234///
235/// Resizes input tensors to a target size using various interpolation methods.
236/// Commonly used for standardizing input sizes in computer vision pipelines.
237#[derive(Debug, Clone)]
238pub struct Resize {
239    size: (usize, usize),
240    interpolation: InterpolationMode,
241}
242
243impl Resize {
244    /// Create a new resize transform with bilinear interpolation
245    ///
246    /// # Arguments
247    /// * `size` - Target size as (height, width)
248    pub fn new(size: (usize, usize)) -> Self {
249        Self {
250            size,
251            interpolation: InterpolationMode::Bilinear,
252        }
253    }
254
255    /// Set the interpolation mode
256    ///
257    /// # Arguments
258    /// * `mode` - Interpolation method to use
259    pub fn with_interpolation(mut self, mode: InterpolationMode) -> Self {
260        self.interpolation = mode;
261        self
262    }
263}
264
265impl<T: FloatElement> Transform<Tensor<T>> for Resize {
266    type Output = Tensor<T>;
267
268    fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
269        let shape = input.shape();
270        let dims = shape.dims();
271
272        // Expect input to be at least 2D (height, width) or 3D (channels, height, width)
273        if dims.len() < 2 {
274            return Err(TorshError::InvalidArgument(
275                "Input tensor must have at least 2 dimensions for resize".to_string(),
276            ));
277        }
278
279        let (input_height, input_width) = if dims.len() == 2 {
280            (dims[0], dims[1])
281        } else {
282            // Assume CHW format for 3D tensors
283            (dims[1], dims[2])
284        };
285
286        let (target_height, target_width) = self.size;
287
288        // If target size matches input size, no resize needed
289        if input_height == target_height && input_width == target_width {
290            return Ok(input);
291        }
292
293        // For now, return the input tensor unchanged
294        // In a full implementation, we would apply the specified interpolation:
295        // - Nearest: select nearest neighbor pixels
296        // - Linear/Bilinear: interpolate between neighboring pixels
297        // - Bicubic: use cubic interpolation with 4x4 pixel neighborhoods
298        //
299        // The implementation would:
300        // 1. Calculate scale factors: scale_y = target_height / input_height
301        // 2. For each output pixel (y, x):
302        //    - Map to input coordinates: (y/scale_y, x/scale_x)
303        //    - Apply interpolation based on self.interpolation mode
304        //    - Set output pixel value
305        // 3. Handle edge cases and boundary conditions
306
307        match self.interpolation {
308            InterpolationMode::Nearest => {
309                // Debug: Applying nearest neighbor resize from ({}, {}) to ({}, {})
310                // input_height, input_width, target_height, target_width
311                Ok(input)
312            }
313            InterpolationMode::Linear | InterpolationMode::Bilinear => {
314                // Debug: Applying bilinear resize from ({}, {}) to ({}, {})
315                // input_height, input_width, target_height, target_width
316                Ok(input)
317            }
318            InterpolationMode::Bicubic => {
319                // Debug: Applying bicubic resize from ({}, {}) to ({}, {})
320                // input_height, input_width, target_height, target_width
321                Ok(input)
322            }
323        }
324    }
325
326    fn is_deterministic(&self) -> bool {
327        true
328    }
329}
330
331/// Center crop transformation
332///
333/// Crops a rectangular region from the center of the input tensor.
334/// Useful for extracting the central portion of images with consistent positioning.
335#[derive(Debug, Clone)]
336pub struct CenterCrop {
337    size: (usize, usize),
338}
339
340impl CenterCrop {
341    /// Create a new center crop transform
342    ///
343    /// # Arguments
344    /// * `size` - Target crop size as (height, width)
345    pub fn new(size: (usize, usize)) -> Self {
346        Self { size }
347    }
348}
349
350impl<T: TensorElement> Transform<Tensor<T>> for CenterCrop {
351    type Output = Tensor<T>;
352
353    fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
354        let shape = input.shape();
355        let dims = shape.dims();
356
357        // Expect input to be at least 2D (height, width) or 3D (channels, height, width)
358        if dims.len() < 2 {
359            return Err(TorshError::InvalidArgument(
360                "Input tensor must have at least 2 dimensions for center crop".to_string(),
361            ));
362        }
363
364        let (input_height, input_width) = if dims.len() == 2 {
365            (dims[0], dims[1])
366        } else {
367            // Assume CHW format for 3D tensors
368            (dims[1], dims[2])
369        };
370
371        let (crop_height, crop_width) = self.size;
372
373        // Check if crop size is larger than input
374        if crop_height > input_height || crop_width > input_width {
375            return Err(TorshError::InvalidArgument(
376                format!("Crop size ({crop_height}, {crop_width}) is larger than input size ({input_height}, {input_width})"),
377            ));
378        }
379
380        // Calculate center crop position
381        let _start_y = (input_height - crop_height) / 2;
382        let _start_x = (input_width - crop_width) / 2;
383
384        // For now, return the input tensor unchanged
385        // In a full implementation, we would extract the center crop region:
386        // - For 2D: input[start_y:start_y+crop_height, start_x:start_x+crop_width]
387        // - For 3D: input[:, start_y:start_y+crop_height, start_x:start_x+crop_width]
388        // This requires advanced tensor slicing operations
389
390        // The implementation would involve:
391        // 1. Creating a new tensor with the crop dimensions
392        // 2. Copying the appropriate region from the input tensor
393        // 3. For 2D tensors: new_tensor[y, x] = input[start_y + y, start_x + x]
394        // 4. For 3D tensors: new_tensor[c, y, x] = input[c, start_y + y, start_x + x]
395
396        // Debug: Center crop from ({}, {}) to ({}, {})
397        // input_height, input_width, crop_height, crop_width
398
399        Ok(input)
400    }
401
402    fn is_deterministic(&self) -> bool {
403        true
404    }
405}
406
407/// Convenience function to create a random horizontal flip transform
408pub fn random_horizontal_flip(prob: f32) -> RandomHorizontalFlip {
409    RandomHorizontalFlip::new(prob)
410}
411
412/// Convenience function to create a random crop transform
413pub fn random_crop(size: (usize, usize)) -> RandomCrop {
414    RandomCrop::new(size)
415}
416
417/// Convenience function to create a resize transform
418pub fn resize(size: (usize, usize)) -> Resize {
419    Resize::new(size)
420}
421
422/// Convenience function to create a center crop transform
423pub fn center_crop(size: (usize, usize)) -> CenterCrop {
424    CenterCrop::new(size)
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use torsh_core::device::DeviceType;
431
432    fn mock_tensor_2d() -> Tensor<f32> {
433        Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu).unwrap()
434    }
435
436    fn mock_tensor_3d() -> Tensor<f32> {
437        Tensor::from_data(
438            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
439            vec![2, 2, 2], // 2 channels, 2x2 spatial
440            DeviceType::Cpu,
441        )
442        .unwrap()
443    }
444
445    #[test]
446    fn test_random_horizontal_flip_creation() {
447        let flip = RandomHorizontalFlip::new(0.5);
448        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &flip;
449        assert!(!_test.is_deterministic());
450    }
451
452    #[test]
453    #[should_panic]
454    fn test_random_horizontal_flip_invalid_prob() {
455        RandomHorizontalFlip::new(1.5); // Should panic
456    }
457
458    #[test]
459    fn test_random_crop_creation() {
460        let crop = RandomCrop::new((224, 224));
461        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &crop;
462        assert!(!_test.is_deterministic());
463    }
464
465    #[test]
466    fn test_random_crop_with_padding() {
467        let crop = RandomCrop::new((224, 224)).with_padding(10);
468        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &crop;
469        assert!(!_test.is_deterministic());
470    }
471
472    #[test]
473    fn test_resize_creation() {
474        let resize_transform = Resize::new((224, 224));
475        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &resize_transform;
476        assert!(_test.is_deterministic());
477    }
478
479    #[test]
480    fn test_resize_with_interpolation() {
481        let resize_transform =
482            Resize::new((224, 224)).with_interpolation(InterpolationMode::Nearest);
483        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &resize_transform;
484        assert!(_test.is_deterministic());
485    }
486
487    #[test]
488    fn test_center_crop_creation() {
489        let crop = CenterCrop::new((224, 224));
490        let _test: &dyn Transform<Tensor<f32>, Output = Tensor<f32>> = &crop;
491        assert!(_test.is_deterministic());
492    }
493
494    #[test]
495    fn test_interpolation_mode_default() {
496        assert_eq!(InterpolationMode::default(), InterpolationMode::Bilinear);
497    }
498
499    #[test]
500    fn test_tensor_transforms_2d() {
501        let tensor = mock_tensor_2d();
502
503        let flip = RandomHorizontalFlip::new(1.0); // Always flip
504        let result = flip.transform(tensor.clone());
505        assert!(result.is_ok());
506
507        let crop = CenterCrop::new((1, 1));
508        let result = crop.transform(tensor.clone());
509        assert!(result.is_ok());
510
511        let resize_transform = Resize::new((4, 4));
512        let result = resize_transform.transform(tensor);
513        assert!(result.is_ok());
514    }
515
516    #[test]
517    fn test_tensor_transforms_3d() {
518        let tensor = mock_tensor_3d();
519
520        let flip = RandomHorizontalFlip::new(0.0); // Never flip
521        let result = flip.transform(tensor.clone());
522        assert!(result.is_ok());
523
524        let crop = CenterCrop::new((1, 1));
525        let result = crop.transform(tensor.clone());
526        assert!(result.is_ok());
527
528        let resize_transform = Resize::new((4, 4));
529        let result = resize_transform.transform(tensor);
530        assert!(result.is_ok());
531    }
532
533    #[test]
534    fn test_convenience_functions() {
535        let _flip = random_horizontal_flip(0.5);
536        let _crop = random_crop((224, 224));
537        let _resize = resize((256, 256));
538        let _center = center_crop((224, 224));
539    }
540
541    #[test]
542    fn test_invalid_tensor_dimensions() {
543        let tensor_1d = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu).unwrap();
544
545        let flip = RandomHorizontalFlip::new(1.0);
546        assert!(flip.transform(tensor_1d.clone()).is_err());
547
548        let crop = CenterCrop::new((1, 1));
549        assert!(crop.transform(tensor_1d.clone()).is_err());
550
551        let resize_transform = Resize::new((4, 4));
552        assert!(resize_transform.transform(tensor_1d).is_err());
553    }
554
555    #[test]
556    fn test_crop_size_validation() {
557        let tensor = mock_tensor_2d(); // 2x2 tensor
558
559        let crop = CenterCrop::new((3, 3)); // Larger than input
560        assert!(crop.transform(tensor.clone()).is_err());
561
562        let random_crop = RandomCrop::new((3, 3)); // Larger than input, no padding
563        assert!(random_crop.transform(tensor).is_err());
564    }
565
566    #[test]
567    fn test_horizontal_flip_changes_tensor() {
568        use torsh_core::device::DeviceType;
569        let flip = RandomHorizontalFlip::new(1.0); // always flip
570                                                   // HW tensor: [[1,2,3],[4,5,6]] -> [[3,2,1],[6,5,4]]
571        let tensor = Tensor::from_data(
572            vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
573            vec![2, 3],
574            DeviceType::Cpu,
575        )
576        .unwrap();
577        let result = flip.transform(tensor).unwrap();
578        let data = result.to_vec().unwrap();
579        assert!(
580            (data[0] - 3.0).abs() < 1e-6,
581            "First element should be 3.0 after horizontal flip, got {}",
582            data[0]
583        );
584        assert!(
585            (data[2] - 1.0).abs() < 1e-6,
586            "Third element should be 1.0 after horizontal flip, got {}",
587            data[2]
588        );
589    }
590}