Skip to content

Commit c4c8525

Browse files
authored
Merge pull request #2766 from image-rs/efficient-copy
Speed up copy for ImageBuffer
2 parents 8b28f75 + 41046d3 commit c4c8525

File tree

6 files changed

+388
-26
lines changed

6 files changed

+388
-26
lines changed

benches/copy_from.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,44 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
22
use image::{GenericImage, ImageBuffer, Rgba};
33

44
pub fn bench_copy_from(c: &mut Criterion) {
5+
let at = image::math::Rect::from_xy_ranges(256..1280, 256..1280);
6+
57
let src = ImageBuffer::from_pixel(2048, 2048, Rgba([255u8, 0, 0, 255]));
68
let mut dst = ImageBuffer::from_pixel(2048, 2048, Rgba([0u8, 0, 0, 255]));
9+
let part = ImageBuffer::from_pixel(at.width, at.height, Rgba([255u8, 0, 0, 255]));
10+
11+
let view = image::GenericImageView::view(&src, at);
12+
13+
const BG: Rgba<u8> = Rgba([0u8, 0, 0, 255]);
14+
let samples = image::flat::FlatSamples::with_monocolor(&BG, at.width, at.height);
15+
let singular = samples.as_view().unwrap();
16+
17+
let mut samples = src.as_flat_samples();
18+
samples.layout.width = 1024;
19+
samples.layout.width_stride *= 2;
20+
samples.layout.height = 1024;
21+
samples.layout.height_stride *= 2;
22+
let skip = samples.as_view().unwrap();
723

824
c.bench_function("copy_from", |b| {
925
b.iter(|| dst.copy_from(black_box(&src), 0, 0));
1026
});
27+
28+
c.bench_function("copy_at", |b| {
29+
b.iter(|| dst.copy_from(black_box(&part), at.x, at.y));
30+
});
31+
32+
c.bench_function("copy_view", |b| {
33+
b.iter(|| dst.copy_from(black_box(&*view), at.x, at.y));
34+
});
35+
36+
c.bench_function("copy_fill", |b| {
37+
b.iter(|| dst.copy_from(black_box(&singular), at.x, at.y));
38+
});
39+
40+
c.bench_function("copy_strides", |b| {
41+
b.iter(|| dst.copy_from(black_box(&skip), at.x, at.y));
42+
});
1143
}
1244

1345
criterion_group!(benches, bench_copy_from);

src/images/buffer.rs

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::color::{FromColor, FromPrimitive, Luma, LumaA, Rgb, Rgba};
1010
use crate::error::{
1111
ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind,
1212
};
13-
use crate::flat::{FlatSamples, SampleLayout};
13+
use crate::flat::{FlatSamples, SampleLayout, ViewOfPixel};
1414
use crate::math::Rect;
1515
use crate::metadata::cicp::{CicpApplicable, CicpPixelCast, CicpRgb, ColorComponentForCicp};
1616
use crate::traits::{EncodableLayout, Pixel, PixelWithColorType};
@@ -1322,6 +1322,16 @@ where
13221322
*self.get_pixel(x, y)
13231323
}
13241324

1325+
fn to_pixel_view(&self) -> Option<ViewOfPixel<'_, Self::Pixel>> {
1326+
let samples = FlatSamples {
1327+
samples: &*self.data,
1328+
layout: self.sample_layout(),
1329+
color_hint: None,
1330+
};
1331+
1332+
samples.into_view().ok()
1333+
}
1334+
13251335
/// Returns the pixel located at (x, y), ignoring bounds checking.
13261336
#[inline(always)]
13271337
unsafe fn unsafe_get_pixel(&self, x: u32, y: u32) -> P {
@@ -1364,6 +1374,61 @@ where
13641374
self.get_pixel_mut(x, y).blend(&p);
13651375
}
13661376

1377+
fn copy_from_samples(
1378+
&mut self,
1379+
view: ViewOfPixel<'_, Self::Pixel>,
1380+
x: u32,
1381+
y: u32,
1382+
) -> ImageResult<()> {
1383+
let (width, height) = view.dimensions();
1384+
let pix_stride = usize::from(<Self::Pixel as Pixel>::CHANNEL_COUNT);
1385+
Rect::from_image_at(&view, x, y).test_in_bounds(self)?;
1386+
1387+
if width == 0 || height == 0 || pix_stride == 0 {
1388+
return Ok(());
1389+
}
1390+
1391+
// Since this image is not empty, all its indices fit into `usize` as they address the
1392+
// memory resident buffer of `self`.
1393+
let row_len = width as usize * pix_stride;
1394+
let img_sh = self.width as usize;
1395+
1396+
let (sw, sh) = view.strides_wh();
1397+
let view_samples: &[_] = view.samples();
1398+
let inner = self.inner_pixels_mut();
1399+
1400+
let img_pixel_indices_unchecked =
1401+
|x: u32, y: u32| (y as usize * img_sh + x as usize) * pix_stride;
1402+
1403+
// Can we use row-by-row byte copy?
1404+
if sw == pix_stride {
1405+
for j in 0..height {
1406+
let start = img_pixel_indices_unchecked(x, j + y);
1407+
let img_row = &mut inner[start..][..row_len];
1408+
let view_row = &view_samples[j as usize * sh..][..row_len];
1409+
img_row.copy_from_slice(view_row);
1410+
}
1411+
1412+
return Ok(());
1413+
}
1414+
1415+
// Fallback behavior.
1416+
for j in 0..height {
1417+
let img_start = img_pixel_indices_unchecked(x, j + y);
1418+
let img_row = &mut inner[img_start..][..row_len];
1419+
let pixels = img_row.chunks_exact_mut(pix_stride);
1420+
1421+
let view_start = j as usize * sh;
1422+
1423+
for (i, sp) in pixels.enumerate() {
1424+
let view_pixel = &view_samples[i * sw + view_start..][..pix_stride];
1425+
sp.copy_from_slice(view_pixel);
1426+
}
1427+
}
1428+
1429+
Ok(())
1430+
}
1431+
13671432
fn copy_within(&mut self, source: Rect, x: u32, y: u32) -> bool {
13681433
let Rect {
13691434
x: sx,
@@ -1928,8 +1993,8 @@ mod test {
19281993
use crate::metadata::CicpMatrixCoefficients;
19291994
use crate::metadata::CicpTransform;
19301995
use crate::metadata::CicpVideoFullRangeFlag;
1931-
use crate::GenericImage as _;
19321996
use crate::ImageFormat;
1997+
use crate::{GenericImage as _, GenericImageView as _};
19331998
use crate::{Luma, LumaA, Pixel, Rgb, Rgba};
19341999
use num_traits::Zero;
19352000

@@ -2367,6 +2432,160 @@ mod test {
23672432
"ImageBuffer::<Rgba<f32>, _> { width: 16, height: 16, color: CicpRgb { primaries: Rgb240m, transfer: LogSqrt, luminance: NonConstant } }"
23682433
);
23692434
}
2435+
2436+
/// We specialize copy_from on types that provide `as_samples` so test that.
2437+
#[test]
2438+
fn copy_from_subimage_to_middle() {
2439+
let mut source = RgbImage::new(16, 16);
2440+
let mut target = RgbImage::new(16, 16);
2441+
2442+
source.put_pixel(8, 8, Rgb([255, 8, 8]));
2443+
source.put_pixel(9, 8, Rgb([255, 9, 8]));
2444+
source.put_pixel(9, 9, Rgb([255, 9, 9]));
2445+
2446+
let view = source.view(Rect::from_xy_ranges(8..10, 8..10));
2447+
assert!(target.copy_from(&*view, 4, 4).is_ok());
2448+
2449+
// Check the pixel was copied.
2450+
assert_eq!(*target.get_pixel(4, 4), Rgb([255, 8, 8]));
2451+
assert_eq!(*target.get_pixel(5, 4), Rgb([255, 9, 8]));
2452+
assert_eq!(*target.get_pixel(5, 5), Rgb([255, 9, 9]));
2453+
2454+
// Check that were the only copied pixel.
2455+
assert_eq!(
2456+
target.iter().copied().map(usize::from).sum::<usize>(),
2457+
3 * (255 + 8 + 9)
2458+
);
2459+
}
2460+
2461+
#[test]
2462+
fn copy_from_band() {
2463+
let source = RgbImage::from_fn(16, 8, |x, y| Rgb([x as u8, y as u8, 0]));
2464+
let mut target = RgbImage::new(16, 16);
2465+
2466+
assert!(target.copy_from(&source, 0, 4).is_ok());
2467+
2468+
let lhs = source.chunks_exact(48);
2469+
let rhs = target.chunks_exact(48).skip(4).take(8);
2470+
2471+
assert!(lhs.eq(rhs));
2472+
}
2473+
2474+
#[test]
2475+
fn copy_from_pixel() {
2476+
let bg = Rgb([255, 0, 128]);
2477+
let samples = crate::flat::FlatSamples::with_monocolor(&bg, 4, 4);
2478+
let source = samples.as_view().unwrap();
2479+
2480+
let mut target = RgbImage::new(16, 16);
2481+
assert!(target.copy_from(&source, 4, 4).is_ok());
2482+
2483+
for i in 4..8 {
2484+
for j in 4..8 {
2485+
assert_eq!(*target.get_pixel(i, j), bg);
2486+
}
2487+
}
2488+
2489+
assert_eq!(
2490+
target.iter().copied().map(usize::from).sum::<usize>(),
2491+
16 * (255 + 128)
2492+
);
2493+
}
2494+
2495+
#[test]
2496+
fn copy_from_strided() {
2497+
#[rustfmt::skip]
2498+
let sample_data = [
2499+
1, 0xff, 0, 0, 2, 0xff,
2500+
3, 0xff, 0, 0, 4, 0xff
2501+
];
2502+
2503+
let samples = crate::flat::FlatSamples {
2504+
samples: &sample_data,
2505+
layout: crate::flat::SampleLayout {
2506+
channels: 2,
2507+
channel_stride: 1,
2508+
width: 2,
2509+
width_stride: 4,
2510+
height: 2,
2511+
height_stride: 6,
2512+
},
2513+
color_hint: None,
2514+
};
2515+
2516+
let source = samples.as_view::<LumaA<u8>>().unwrap();
2517+
let mut target = crate::GrayAlphaImage::new(16, 16);
2518+
assert!(target.copy_from(&source, 4, 4).is_ok());
2519+
2520+
assert_eq!(*target.get_pixel(4, 4), LumaA([1, 0xff]));
2521+
assert_eq!(*target.get_pixel(5, 4), LumaA([2, 0xff]));
2522+
assert_eq!(*target.get_pixel(4, 5), LumaA([3, 0xff]));
2523+
assert_eq!(*target.get_pixel(5, 5), LumaA([4, 0xff]));
2524+
2525+
assert_eq!(
2526+
target.iter().copied().map(usize::from).sum::<usize>(),
2527+
sample_data.iter().copied().map(usize::from).sum::<usize>(),
2528+
);
2529+
}
2530+
2531+
#[test]
2532+
fn copy_from_strided_subimage() {
2533+
#[rustfmt::skip]
2534+
let sample_data = [
2535+
1, 0xff, 0, 0, 2, 0xff,
2536+
3, 0xff, 0, 0, 4, 0xff
2537+
];
2538+
2539+
let samples = crate::flat::FlatSamples {
2540+
samples: &sample_data,
2541+
layout: crate::flat::SampleLayout {
2542+
channels: 2,
2543+
channel_stride: 1,
2544+
width: 2,
2545+
width_stride: 4,
2546+
height: 2,
2547+
height_stride: 6,
2548+
},
2549+
color_hint: None,
2550+
};
2551+
2552+
let view = samples.as_view::<LumaA<u8>>().unwrap();
2553+
let source = view.view(Rect::from_xy_ranges(1..2, 0..2));
2554+
2555+
let mut target = crate::GrayAlphaImage::new(16, 16);
2556+
assert!(target.copy_from(&*source, 4, 4).is_ok());
2557+
2558+
assert_eq!(*target.get_pixel(4, 4), LumaA([2, 0xff]));
2559+
assert_eq!(*target.get_pixel(4, 5), LumaA([4, 0xff]));
2560+
2561+
assert_eq!(
2562+
target.iter().copied().map(usize::from).sum::<usize>(),
2563+
2usize + 0xff + 4 + 0xff
2564+
);
2565+
}
2566+
2567+
#[test]
2568+
fn copy_from_subimage_subimage() {
2569+
let mut source = RgbImage::new(16, 16);
2570+
let mut target = RgbImage::new(16, 16);
2571+
2572+
source.put_pixel(8, 8, Rgb([255, 8, 8]));
2573+
source.put_pixel(9, 8, Rgb([255, 9, 8]));
2574+
source.put_pixel(9, 9, Rgb([255, 9, 9]));
2575+
2576+
let view = source.view(Rect::from_xy_ranges(8..10, 8..10));
2577+
let view = view.view(Rect::from_xy_ranges(1..2, 0..1));
2578+
assert!(target.copy_from(&*view, 4, 4).is_ok());
2579+
2580+
// Check the pixel was copied.
2581+
assert_eq!(*target.get_pixel(4, 4), Rgb([255, 9, 8]));
2582+
2583+
// Check that was the only copied pixel.
2584+
assert_eq!(
2585+
target.iter().copied().map(usize::from).sum::<usize>(),
2586+
255 + 9 + 8
2587+
);
2588+
}
23702589
}
23712590

23722591
#[cfg(test)]

0 commit comments

Comments
 (0)