Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions src/cropping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,47 @@ pub fn crop_image_array(
width: usize,
height: usize,
) -> Result<Array3<u8>> {
let (img_height, img_width, _channels) = image.dim();
let (img_height, img_width, channels) = image.dim();

if width == 0 || height == 0 {
return Err(anyhow::anyhow!("Crop dimensions must be greater than zero"));
}

if x + width > img_width || y + height > img_height {
let end_x = x
.checked_add(width)
.ok_or_else(|| anyhow::anyhow!("Crop bounds exceed image dimensions"))?;
let end_y = y
.checked_add(height)
.ok_or_else(|| anyhow::anyhow!("Crop bounds exceed image dimensions"))?;

if end_x > img_width || end_y > img_height {
return Err(anyhow::anyhow!("Crop bounds exceed image dimensions"));
}

// Use assign to avoid allocation where possible, but still return owned array
let slice = image.slice(s![y..y + height, x..x + width, ..]);
// Only allocate when we need to return an owned array
if let Some(src) = image.as_slice() {
let output_len = height
.checked_mul(width)
.and_then(|pixels| pixels.checked_mul(channels))
.ok_or_else(|| anyhow::anyhow!("Crop dimensions are too large"))?;
let mut output = Vec::with_capacity(output_len);

// Every byte in the reserved output is initialized by crop_raw_buffer
// before the Vec length is exposed to safe Rust.
unsafe {
crop_raw_buffer(
src.as_ptr(),
(img_height, img_width, channels),
output.as_mut_ptr(),
(y, x, height, width),
);
output.set_len(output_len);
}

return Array3::from_shape_vec((height, width, channels), output).map_err(Into::into);
}

// Preserve support for non-contiguous ndarray views with the generic path.
let slice = image.slice(s![y..end_y, x..end_x, ..]);
Ok(slice.to_owned())
}

Expand Down Expand Up @@ -111,16 +139,7 @@ pub fn batch_center_crop_arrays(
let actual_width = target_width.min(img_width);
let actual_height = target_height.min(img_height);

// Validate bounds
if x + actual_width > img_width || y + actual_height > img_height {
return Err(anyhow::anyhow!("Crop bounds exceed image dimensions"));
}

// Direct slice and copy - optimized for speed
let cropped = image
.slice(s![y..y + actual_height, x..x + actual_width, ..])
.to_owned();
results.push(cropped);
results.push(crop_image_array(image, x, y, actual_width, actual_height)?);
}

Ok(results)
Expand Down
11 changes: 11 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ mod cropping_tests {
assert!(result.is_err());
}

#[test]
fn test_crop_non_contiguous_view() {
let img = create_test_image();
let strided = img.slice(ndarray::s![.., ..;2, ..]);
let result = crop_image_array(&strided, 5, 10, 20, 30).unwrap();

assert_eq!(result.dim(), (30, 20, 3));
assert_eq!(result[[0, 0, 0]], img[[10, 10, 0]]);
assert_eq!(result[[29, 19, 2]], img[[39, 48, 2]]);
}

#[test]
fn test_center_crop_image_array() {
let img = create_test_image();
Expand Down
66 changes: 40 additions & 26 deletions tests/comprehensive_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,45 +598,59 @@ mod performance_validation_tests {

#[test]
fn test_batch_operations_scale_appropriately() {
use std::hint::black_box;
use std::time::Instant;

let base_img = create_gradient_image();
let crop = (10, 10, 30, 20);
let batch_size = 50;
let batch_imgs = vec![base_img.clone(); batch_size];
let batch_crops = vec![crop; batch_size];

// Warm up - run a few operations to ensure consistent timing
// Warm both Rayon paths before measuring.
for _ in 0..3 {
let _ = batch_crop_image_arrays(std::slice::from_ref(&base_img), &[(10, 10, 30, 20)]);
black_box(batch_crop_image_arrays(
std::slice::from_ref(&base_img),
&[crop],
));
black_box(batch_crop_image_arrays(&batch_imgs, &batch_crops));
}

// Single operation - run multiple times and take average
let mut single_total = std::time::Duration::new(0, 0);
for _ in 0..5 {
// Each sample performs enough work to keep microsecond-scale timer
// noise from dominating the comparison.
let mut single_samples = Vec::with_capacity(11);
let mut batch_samples = Vec::with_capacity(11);
for _ in 0..11 {
let start = Instant::now();
let _single =
batch_crop_image_arrays(std::slice::from_ref(&base_img), &[(10, 10, 30, 20)]);
single_total += start.elapsed();
}
let single_duration = single_total / 5;

// Batch of 50 (larger batch for better parallelism benefits)
let batch_imgs = vec![base_img.clone(); 50];
let batch_crops = vec![(10, 10, 30, 20); 50];
for _ in 0..100 {
black_box(batch_crop_image_arrays(
std::slice::from_ref(&base_img),
&[crop],
));
}
single_samples.push(start.elapsed().as_nanos() / 100);

// Run batch operation multiple times and take average
let mut batch_total = std::time::Duration::new(0, 0);
for _ in 0..3 {
let start = Instant::now();
let _batch = batch_crop_image_arrays(&batch_imgs, &batch_crops);
batch_total += start.elapsed();
for _ in 0..10 {
black_box(batch_crop_image_arrays(&batch_imgs, &batch_crops));
}
batch_samples.push(start.elapsed().as_nanos() / (10 * batch_size as u128));
}
let batch_duration = batch_total / 3;

// Batch should not be more than 75x slower (more reasonable for 50x operations)
// Allow for system variance and parallelism overhead
let slowdown_ratio = batch_duration.as_nanos() as f64 / single_duration.as_nanos() as f64;
single_samples.sort_unstable();
batch_samples.sort_unstable();
let single_per_image = single_samples[single_samples.len() / 2];
let batch_per_image = batch_samples[batch_samples.len() / 2];
let per_image_ratio = batch_per_image as f64 / single_per_image as f64;

// Small crops may not amortize Rayon scheduling, so this is a broad
// regression guard rather than an assertion that batching must win.
assert!(
slowdown_ratio < 75.0,
"Batch processing should benefit from parallelism: ratio {:.2}, single: {:?}, batch: {:?}",
slowdown_ratio, single_duration, batch_duration
per_image_ratio < 20.0,
"Batch crop per-image cost regressed: ratio {:.2}, single: {}ns, batch: {}ns",
per_image_ratio,
single_per_image,
batch_per_image
);
}
}
Loading