1use crate::transforms::Transform;
17use torsh_core::dtype::{FloatElement, TensorElement};
18use torsh_core::error::Result;
19use torsh_tensor::Tensor;
20
21#[cfg(not(feature = "std"))]
22use alloc::{boxed::Box, vec::Vec};
23
24#[cfg(feature = "std")]
25use scirs2_core::random::thread_rng;
26
27#[cfg(not(feature = "std"))]
28use scirs2_core::random::thread_rng;
29
30pub struct AugmentationPipeline<T> {
32 transforms: Vec<Box<dyn Transform<T, Output = T> + Send + Sync>>,
33 probability: f32,
34}
35
36impl<T: 'static + Send + Sync> AugmentationPipeline<T> {
37 pub fn new() -> Self {
39 Self {
40 transforms: Vec::new(),
41 probability: 1.0,
42 }
43 }
44
45 pub fn with_probability(mut self, prob: f32) -> Self {
47 assert!(
48 (0.0..=1.0).contains(&prob),
49 "Probability must be between 0 and 1"
50 );
51 self.probability = prob;
52 self
53 }
54
55 pub fn add_transform<F>(mut self, transform: F) -> Self
57 where
58 F: Transform<T, Output = T> + 'static,
59 {
60 self.transforms.push(Box::new(transform));
61 self
62 }
63
64 pub fn add_conditional<F>(self, transform: F, prob: f32) -> Self
66 where
67 F: Transform<T, Output = T> + 'static,
68 {
69 self.add_transform(ConditionalTransform::new(transform, prob))
70 }
71}
72
73impl<T: 'static + Send + Sync> Default for AugmentationPipeline<T> {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl<T> Transform<T> for AugmentationPipeline<T> {
80 type Output = T;
81
82 fn transform(&self, mut input: T) -> Result<Self::Output> {
83 let mut rng = thread_rng();
84
85 if rng.random::<f32>() > self.probability {
87 return Ok(input);
88 }
89
90 for transform in &self.transforms {
92 input = transform.transform(input)?;
93 }
94
95 Ok(input)
96 }
97}
98
99pub struct ConditionalTransform<T, F> {
101 transform: F,
102 probability: f32,
103 _phantom: core::marker::PhantomData<T>,
104}
105
106impl<T, F> ConditionalTransform<T, F> {
107 pub fn new(transform: F, probability: f32) -> Self {
108 assert!(
109 (0.0..=1.0).contains(&probability),
110 "Probability must be between 0 and 1"
111 );
112 Self {
113 transform,
114 probability,
115 _phantom: core::marker::PhantomData,
116 }
117 }
118}
119
120impl<T, F> Transform<T> for ConditionalTransform<T, F>
121where
122 F: Transform<T, Output = T>,
123 T: Send + Sync,
124{
125 type Output = T;
126
127 fn transform(&self, input: T) -> Result<Self::Output> {
128 let mut rng = thread_rng();
129
130 if rng.random::<f32>() < self.probability {
131 self.transform.transform(input)
132 } else {
133 Ok(input)
134 }
135 }
136}
137
138pub struct RandomBrightness {
140 factor_range: (f32, f32),
141}
142
143impl RandomBrightness {
144 pub fn new(factor_range: (f32, f32)) -> Self {
145 assert!(factor_range.0 <= factor_range.1, "Invalid factor range");
146 Self { factor_range }
147 }
148
149 pub fn symmetric(factor: f32) -> Self {
151 Self::new((1.0 - factor, 1.0 + factor))
152 }
153}
154
155impl<T: FloatElement> Transform<Tensor<T>> for RandomBrightness {
156 type Output = Tensor<T>;
157
158 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
159 let mut rng = thread_rng();
160 let (lo, hi) = self.factor_range;
161 let factor = lo + rng.random::<f32>() * (hi - lo);
162 let shape = input.shape().dims().to_vec();
163 let device = input.device();
164 let data = input
165 .to_vec()
166 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
167 let out: Vec<T> = data
168 .iter()
169 .map(|&x| {
170 let v = <T as TensorElement>::to_f64(&x).unwrap_or(0.0) * factor as f64;
171 let clamped = v.max(0.0).min(1.0);
172 <T as TensorElement>::from_f64(clamped).unwrap_or(x)
173 })
174 .collect();
175 Tensor::from_data(out, shape, device).map_err(|e| e.into())
176 }
177}
178
179pub struct RandomContrast {
181 factor_range: (f32, f32),
182}
183
184impl RandomContrast {
185 pub fn new(factor_range: (f32, f32)) -> Self {
186 assert!(factor_range.0 <= factor_range.1, "Invalid factor range");
187 Self { factor_range }
188 }
189
190 pub fn symmetric(factor: f32) -> Self {
192 Self::new((1.0 - factor, 1.0 + factor))
193 }
194}
195
196impl<T: FloatElement> Transform<Tensor<T>> for RandomContrast {
197 type Output = Tensor<T>;
198
199 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
200 let mut rng = thread_rng();
201 let (lo, hi) = self.factor_range;
202 let factor = lo + rng.random::<f32>() * (hi - lo);
203 let shape = input.shape().dims().to_vec();
204 let device = input.device();
205 let data = input
206 .to_vec()
207 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
208 let n = data.len();
209 let mean: f64 = data
210 .iter()
211 .map(|x| <T as TensorElement>::to_f64(x).unwrap_or(0.0))
212 .sum::<f64>()
213 / n.max(1) as f64;
214 let out: Vec<T> = data
215 .iter()
216 .map(|&x| {
217 let v =
218 mean + (<T as TensorElement>::to_f64(&x).unwrap_or(0.0) - mean) * factor as f64;
219 let clamped = v.max(0.0).min(1.0);
220 <T as TensorElement>::from_f64(clamped).unwrap_or(x)
221 })
222 .collect();
223 Tensor::from_data(out, shape, device).map_err(|e| e.into())
224 }
225}
226
227pub struct RandomSaturation {
229 factor_range: (f32, f32),
230}
231
232impl RandomSaturation {
233 pub fn new(factor_range: (f32, f32)) -> Self {
234 assert!(factor_range.0 <= factor_range.1, "Invalid factor range");
235 Self { factor_range }
236 }
237
238 pub fn symmetric(factor: f32) -> Self {
240 Self::new((1.0 - factor, 1.0 + factor))
241 }
242}
243
244impl<T: FloatElement> Transform<Tensor<T>> for RandomSaturation {
245 type Output = Tensor<T>;
246
247 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
248 let mut rng = thread_rng();
249 let (lo, hi) = self.factor_range;
250 let factor = lo + rng.random::<f32>() * (hi - lo);
251 let binding = input.shape();
252 let dims = binding.dims();
253 if dims.len() != 3 || dims[0] != 3 {
255 return Ok(input);
256 }
257 let (_, height, width) = (dims[0], dims[1], dims[2]);
258 let hw = height * width;
259 let device = input.device();
260 let data = input
261 .to_vec()
262 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
263 const LUM_R: f64 = 0.299;
265 const LUM_G: f64 = 0.587;
266 const LUM_B: f64 = 0.114;
267 let mut out = data.clone();
268 for px in 0..hw {
269 let r = <T as TensorElement>::to_f64(&data[px]).unwrap_or(0.0);
270 let g = <T as TensorElement>::to_f64(&data[hw + px]).unwrap_or(0.0);
271 let b = <T as TensorElement>::to_f64(&data[2 * hw + px]).unwrap_or(0.0);
272 let lum = LUM_R * r + LUM_G * g + LUM_B * b;
273 let sat = factor as f64;
274 let new_r = (lum + sat * (r - lum)).max(0.0).min(1.0);
275 let new_g = (lum + sat * (g - lum)).max(0.0).min(1.0);
276 let new_b = (lum + sat * (b - lum)).max(0.0).min(1.0);
277 out[px] = <T as TensorElement>::from_f64(new_r).unwrap_or(data[px]);
278 out[hw + px] = <T as TensorElement>::from_f64(new_g).unwrap_or(data[hw + px]);
279 out[2 * hw + px] = <T as TensorElement>::from_f64(new_b).unwrap_or(data[2 * hw + px]);
280 }
281 Tensor::from_data(out, dims.to_vec(), device).map_err(|e| e.into())
282 }
283}
284
285pub struct RandomHue {
287 delta_range: (f32, f32),
288}
289
290impl RandomHue {
291 pub fn new(delta_range: (f32, f32)) -> Self {
292 assert!(delta_range.0 <= delta_range.1, "Invalid delta range");
293 assert!(
294 delta_range.0 >= -1.0 && delta_range.1 <= 1.0,
295 "Hue delta must be in [-1, 1]"
296 );
297 Self { delta_range }
298 }
299
300 pub fn symmetric(delta: f32) -> Self {
302 Self::new((-delta, delta))
303 }
304}
305
306impl<T: FloatElement> Transform<Tensor<T>> for RandomHue {
307 type Output = Tensor<T>;
308
309 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
310 let mut rng = thread_rng();
315 let (lo, hi) = self.delta_range;
316 let delta = lo + rng.random::<f32>() * (hi - lo);
317 let binding = input.shape();
318 let dims = binding.dims();
319 if dims.len() != 3 || dims[0] != 3 {
321 return Ok(input);
322 }
323 let (_, height, width) = (dims[0], dims[1], dims[2]);
324 let hw = height * width;
325 let device = input.device();
326 let data = input
327 .to_vec()
328 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
329 let angle = delta as f64 * std::f64::consts::PI;
332 let r_scale = 1.0 + angle.sin() * 0.5;
333 let g_scale = 1.0 - angle.abs().sin() * 0.1;
334 let b_scale = 1.0 - angle.sin() * 0.5;
335 let mut out = data.clone();
336 for px in 0..hw {
337 let r = <T as TensorElement>::to_f64(&data[px]).unwrap_or(0.0);
338 let g = <T as TensorElement>::to_f64(&data[hw + px]).unwrap_or(0.0);
339 let b = <T as TensorElement>::to_f64(&data[2 * hw + px]).unwrap_or(0.0);
340 out[px] =
341 <T as TensorElement>::from_f64((r * r_scale).max(0.0).min(1.0)).unwrap_or(data[px]);
342 out[hw + px] = <T as TensorElement>::from_f64((g * g_scale).max(0.0).min(1.0))
343 .unwrap_or(data[hw + px]);
344 out[2 * hw + px] = <T as TensorElement>::from_f64((b * b_scale).max(0.0).min(1.0))
345 .unwrap_or(data[2 * hw + px]);
346 }
347 Tensor::from_data(out, dims.to_vec(), device).map_err(|e| e.into())
348 }
349}
350
351pub struct RandomVerticalFlip {
353 prob: f32,
354}
355
356impl RandomVerticalFlip {
357 pub fn new(prob: f32) -> Self {
358 assert!(
359 (0.0..=1.0).contains(&prob),
360 "Probability must be between 0 and 1"
361 );
362 Self { prob }
363 }
364}
365
366impl<T: FloatElement> Transform<Tensor<T>> for RandomVerticalFlip {
367 type Output = Tensor<T>;
368
369 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
370 let mut rng = thread_rng();
371 if rng.random::<f32>() >= self.prob {
372 return Ok(input);
373 }
374 let binding = input.shape();
375 let dims = binding.dims();
376 if dims.len() < 2 {
377 return Err(torsh_core::error::TorshError::InvalidArgument(
378 "Input tensor must have at least 2 dimensions for vertical flip".to_string(),
379 ));
380 }
381 let device = input.device();
382 let data = input
383 .to_vec()
384 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
385 let (height, width, channels) = if dims.len() == 2 {
387 (dims[0], dims[1], 1usize)
388 } else {
389 (
390 dims[dims.len() - 2],
391 dims[dims.len() - 1],
392 dims[..dims.len() - 2].iter().product(),
393 )
394 };
395 let mut out = data.clone();
396 for c in 0..channels {
397 for row in 0..height / 2 {
398 let mirror_row = height - 1 - row;
399 for col in 0..width {
400 let idx1 = c * height * width + row * width + col;
401 let idx2 = c * height * width + mirror_row * width + col;
402 out.swap(idx1, idx2);
403 }
404 }
405 }
406 Tensor::from_data(out, dims.to_vec(), device).map_err(|e| e.into())
407 }
408}
409
410pub struct GaussianNoise {
412 mean: f32,
413 std: f32,
414}
415
416impl GaussianNoise {
417 pub fn new(mean: f32, std: f32) -> Self {
418 assert!(std >= 0.0, "Standard deviation must be non-negative");
419 Self { mean, std }
420 }
421
422 pub fn with_std(std: f32) -> Self {
424 Self::new(0.0, std)
425 }
426}
427
428impl<T: FloatElement> Transform<Tensor<T>> for GaussianNoise {
429 type Output = Tensor<T>;
430
431 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
432 if self.std <= 0.0 {
433 return Ok(input);
434 }
435 let mut rng = thread_rng();
436 let shape = input.shape().dims().to_vec();
437 let device = input.device();
438 let data = input
439 .to_vec()
440 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
441 let mean_f64 = self.mean as f64;
442 let std_f64 = self.std as f64;
443 let out: Vec<T> = data
444 .iter()
445 .map(|&x| {
446 let u1: f32 = rng.random::<f32>().max(f32::EPSILON);
448 let u2: f32 = rng.random::<f32>();
449 let noise = ((-2.0 * u1.ln()) as f64).sqrt()
450 * (2.0 * std::f64::consts::PI * u2 as f64).cos();
451 let noisy =
452 <T as TensorElement>::to_f64(&x).unwrap_or(0.0) + mean_f64 + std_f64 * noise;
453 <T as TensorElement>::from_f64(noisy).unwrap_or(x)
454 })
455 .collect();
456 Tensor::from_data(out, shape, device).map_err(|e| e.into())
457 }
458}
459
460pub struct RandomErasing {
462 prob: f32,
463 scale_range: (f32, f32),
464 ratio_range: (f32, f32),
465 fill_value: f32,
466}
467
468impl RandomErasing {
469 pub fn new(prob: f32, scale_range: (f32, f32), ratio_range: (f32, f32)) -> Self {
470 assert!(
471 (0.0..=1.0).contains(&prob),
472 "Probability must be between 0 and 1"
473 );
474 assert!(scale_range.0 <= scale_range.1, "Invalid scale range");
475 assert!(ratio_range.0 <= ratio_range.1, "Invalid ratio range");
476
477 Self {
478 prob,
479 scale_range,
480 ratio_range,
481 fill_value: 0.0,
482 }
483 }
484
485 pub fn with_fill_value(mut self, fill_value: f32) -> Self {
486 self.fill_value = fill_value;
487 self
488 }
489}
490
491impl<T: FloatElement> Transform<Tensor<T>> for RandomErasing {
492 type Output = Tensor<T>;
493
494 fn transform(&self, input: Tensor<T>) -> Result<Self::Output> {
495 let mut rng = thread_rng();
496 if rng.random::<f32>() >= self.prob {
497 return Ok(input);
498 }
499 let binding = input.shape();
500 let dims = binding.dims();
501 if dims.len() < 2 {
502 return Err(torsh_core::error::TorshError::InvalidArgument(
503 "Input tensor must have at least 2 dimensions for random erasing".to_string(),
504 ));
505 }
506 let device = input.device();
507 let (height, width, channels) = if dims.len() == 2 {
508 (dims[0], dims[1], 1usize)
509 } else {
510 (
511 dims[dims.len() - 2],
512 dims[dims.len() - 1],
513 dims[..dims.len() - 2].iter().product(),
514 )
515 };
516 let total_area = (height * width) as f32;
517 let (scale_lo, scale_hi) = self.scale_range;
519 let area_frac = scale_lo + rng.random::<f32>() * (scale_hi - scale_lo);
520 let erase_area = (total_area * area_frac) as usize;
521 let (ratio_lo, ratio_hi) = self.ratio_range;
523 let ratio = ratio_lo + rng.random::<f32>() * (ratio_hi - ratio_lo);
524 let erase_h = ((erase_area as f32 / ratio).sqrt() as usize).clamp(1, height);
525 let erase_w = ((erase_area as f32 * ratio).sqrt() as usize).clamp(1, width);
526 if erase_h >= height || erase_w >= width {
527 return Ok(input);
528 }
529 let top = rng.gen_range(0..=(height - erase_h));
530 let left = rng.gen_range(0..=(width - erase_w));
531 let fill = <T as TensorElement>::from_f64(self.fill_value as f64)
532 .unwrap_or_else(<T as TensorElement>::zero);
533 let mut data = input
534 .to_vec()
535 .map_err(|e| torsh_core::error::TorshError::Other(format!("to_vec failed: {}", e)))?;
536 for c in 0..channels {
537 for row in top..(top + erase_h) {
538 for col in left..(left + erase_w) {
539 let idx = c * height * width + row * width + col;
540 data[idx] = fill;
541 }
542 }
543 }
544 Tensor::from_data(data, dims.to_vec(), device).map_err(|e| e.into())
545 }
546}
547
548impl AugmentationPipeline<Tensor<f32>> {
550 pub fn light_augmentation() -> Self {
552 Self::new()
553 .add_conditional(
554 crate::tensor_transforms::RandomHorizontalFlip::new(0.5),
555 1.0,
556 )
557 .add_conditional(RandomBrightness::symmetric(0.1), 0.3)
558 .add_conditional(RandomContrast::symmetric(0.1), 0.3)
559 }
560
561 pub fn medium_augmentation() -> Self {
563 Self::new()
564 .add_conditional(
565 crate::tensor_transforms::RandomHorizontalFlip::new(0.5),
566 1.0,
567 )
568 .add_conditional(RandomVerticalFlip::new(0.1), 1.0)
569 .add_conditional(RandomBrightness::symmetric(0.2), 0.5)
570 .add_conditional(RandomContrast::symmetric(0.2), 0.5)
571 .add_conditional(RandomSaturation::symmetric(0.2), 0.3)
572 .add_conditional(GaussianNoise::with_std(0.01), 0.2)
573 }
574
575 pub fn heavy_augmentation() -> Self {
577 Self::new()
578 .add_conditional(
579 crate::tensor_transforms::RandomHorizontalFlip::new(0.5),
580 1.0,
581 )
582 .add_conditional(RandomVerticalFlip::new(0.2), 1.0)
583 .add_conditional(RandomBrightness::symmetric(0.3), 0.7)
584 .add_conditional(RandomContrast::symmetric(0.3), 0.7)
585 .add_conditional(RandomSaturation::symmetric(0.3), 0.5)
586 .add_conditional(RandomHue::symmetric(0.1), 0.3)
587 .add_conditional(GaussianNoise::with_std(0.02), 0.3)
588 .add_conditional(RandomErasing::new(0.5, (0.02, 0.33), (0.3, 3.3)), 1.0)
589 }
590
591 pub fn imagenet_augmentation() -> Self {
593 Self::new()
594 .add_conditional(
595 crate::tensor_transforms::RandomHorizontalFlip::new(0.5),
596 1.0,
597 )
598 .add_conditional(RandomBrightness::symmetric(0.2), 0.4)
599 .add_conditional(RandomContrast::symmetric(0.2), 0.4)
600 .add_conditional(RandomSaturation::symmetric(0.2), 0.4)
601 .add_conditional(RandomHue::symmetric(0.1), 0.1)
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use torsh_core::device::DeviceType;
609 use torsh_tensor::Tensor;
610
611 fn mock_tensor() -> Tensor<f32> {
613 Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu).unwrap()
614 }
615
616 #[test]
617 fn test_augmentation_pipeline_creation() {
618 let pipeline = AugmentationPipeline::<i32>::new();
619 assert_eq!(pipeline.probability, 1.0);
620 assert_eq!(pipeline.transforms.len(), 0);
621 }
622
623 #[test]
624 fn test_augmentation_pipeline_with_probability() {
625 let pipeline = AugmentationPipeline::<i32>::new().with_probability(0.5);
626 assert_eq!(pipeline.probability, 0.5);
627 }
628
629 #[test]
630 #[should_panic(expected = "Probability must be between 0 and 1")]
631 fn test_invalid_probability() {
632 AugmentationPipeline::<i32>::new().with_probability(1.5);
633 }
634
635 #[test]
636 fn test_conditional_transform_creation() {
637 let transform: ConditionalTransform<i32, _> =
638 ConditionalTransform::new(crate::transforms::lambda(|x: i32| Ok(x * 2)), 0.5);
639 assert_eq!(transform.probability, 0.5);
640 }
641
642 #[test]
643 fn test_random_brightness_creation() {
644 let brightness = RandomBrightness::new((0.8, 1.2));
645 assert_eq!(brightness.factor_range, (0.8, 1.2));
646 }
647
648 #[test]
649 fn test_random_brightness_symmetric() {
650 let brightness = RandomBrightness::symmetric(0.2);
651 assert_eq!(brightness.factor_range, (0.8, 1.2));
652 }
653
654 #[test]
655 fn test_gaussian_noise_creation() {
656 let noise = GaussianNoise::new(0.0, 0.1);
657 assert_eq!(noise.mean, 0.0);
658 assert_eq!(noise.std, 0.1);
659 }
660
661 #[test]
662 fn test_gaussian_noise_with_std() {
663 let noise = GaussianNoise::with_std(0.05);
664 assert_eq!(noise.mean, 0.0);
665 assert_eq!(noise.std, 0.05);
666 }
667
668 #[test]
669 fn test_random_erasing_creation() {
670 let erasing = RandomErasing::new(0.5, (0.02, 0.33), (0.3, 3.3));
671 assert_eq!(erasing.prob, 0.5);
672 assert_eq!(erasing.scale_range, (0.02, 0.33));
673 assert_eq!(erasing.ratio_range, (0.3, 3.3));
674 assert_eq!(erasing.fill_value, 0.0);
675 }
676
677 #[test]
678 fn test_light_augmentation_preset() {
679 let pipeline = AugmentationPipeline::light_augmentation();
680 assert_eq!(pipeline.transforms.len(), 3);
681 }
682
683 #[test]
684 fn test_medium_augmentation_preset() {
685 let pipeline = AugmentationPipeline::medium_augmentation();
686 assert_eq!(pipeline.transforms.len(), 6);
687 }
688
689 #[test]
690 fn test_heavy_augmentation_preset() {
691 let pipeline = AugmentationPipeline::heavy_augmentation();
692 assert_eq!(pipeline.transforms.len(), 8);
693 }
694
695 #[test]
696 fn test_augmentation_transform_shape_preserved() {
697 let tensor = mock_tensor();
698 let brightness = RandomBrightness::symmetric(0.1);
699 let result = brightness.transform(tensor.clone()).unwrap();
700
701 assert_eq!(result.shape(), tensor.shape());
703 }
704
705 #[test]
706 fn test_random_brightness_changes_tensor() {
707 let brightness = RandomBrightness::new((0.5, 0.7));
709 let tensor = Tensor::from_data(vec![1.0f32; 4], vec![2, 2], DeviceType::Cpu).unwrap();
710 let result = brightness.transform(tensor).unwrap();
711 let result_data = result.to_vec().unwrap();
712 assert!(
714 result_data.iter().all(|&x| x < 1.0),
715 "Brightness transform must darken the tensor (factor in [0.5, 0.7])"
716 );
717 }
718
719 #[test]
720 fn test_gaussian_noise_changes_tensor() {
721 let noise = GaussianNoise::with_std(0.5);
722 let mut changed = false;
724 for _ in 0..10 {
725 let tensor = Tensor::from_data(vec![0.5f32; 16], vec![4, 4], DeviceType::Cpu).unwrap();
726 let result = noise.transform(tensor).unwrap();
727 let data = result.to_vec().unwrap();
728 if data.iter().any(|&x| (x - 0.5f32).abs() > 1e-6) {
729 changed = true;
730 break;
731 }
732 }
733 assert!(changed, "GaussianNoise must change tensor values");
734 }
735
736 #[test]
737 fn test_random_vertical_flip_changes_tensor() {
738 let flip = RandomVerticalFlip::new(1.0);
740 let tensor =
742 Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu).unwrap();
743 let result = flip.transform(tensor).unwrap();
744 let data = result.to_vec().unwrap();
745 assert!(
747 (data[0] - 3.0).abs() < 1e-6 && (data[1] - 4.0).abs() < 1e-6,
748 "Vertical flip must reverse rows: got {:?}",
749 data
750 );
751 }
752
753 #[test]
754 fn test_random_erasing_changes_tensor() {
755 let erasing = RandomErasing::new(1.0, (0.5, 0.9), (1.0, 1.0));
757 let tensor = Tensor::from_data(vec![1.0f32; 100], vec![10, 10], DeviceType::Cpu).unwrap();
759 let result = erasing.transform(tensor).unwrap();
760 let data = result.to_vec().unwrap();
761 assert!(
762 data.iter().any(|&x| x == 0.0f32),
763 "RandomErasing must fill some values with fill_value (0.0)"
764 );
765 }
766}