Skip to content

Commit d9ab111

Browse files
authored
Merge pull request #2769 from image-rs/backport-efficient-copy
Backport efficient copy
2 parents c1a08e1 + c9cbbb4 commit d9ab111

File tree

6 files changed

+468
-28
lines changed

6 files changed

+468
-28
lines changed

benches/copy_from.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,98 @@ 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 = rect_from_xy_ranges(256..1280, 256..1280);
6+
7+
let mut target = ImageBuffer::from_pixel(2048, 2048, Rgba([0u8, 0, 0, 255]));
58
let src = ImageBuffer::from_pixel(2048, 2048, Rgba([255u8, 0, 0, 255]));
6-
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.x, at.y, at.width, at.height);
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| {
9-
b.iter(|| dst.copy_from(black_box(&src), 0, 0));
25+
b.iter(|| target.copy_from(black_box(&src), 0, 0));
26+
});
27+
28+
c.bench_function("copy_at", |b| {
29+
b.iter(|| target.copy_from(black_box(&part), at.x, at.y));
30+
});
31+
32+
c.bench_function("copy_view", |b| {
33+
b.iter(|| target.copy_from(black_box(&*view), at.x, at.y));
34+
});
35+
36+
c.bench_function("copy_fill", |b| {
37+
b.iter(|| target.copy_from(black_box(&singular), at.x, at.y));
38+
});
39+
40+
c.bench_function("copy_strides", |b| {
41+
b.iter(|| target.copy_from(black_box(&skip), at.x, at.y));
42+
});
43+
}
44+
45+
pub fn bench_copy_subimage_from(c: &mut Criterion) {
46+
let vp = rect_from_xy_ranges(256..1280, 256..1280);
47+
let at = rect_from_xy_ranges(128..512, 128..512);
48+
49+
let mut target = ImageBuffer::from_pixel(2048, 2048, Rgba([0u8, 0, 0, 255]));
50+
let mut target = target.sub_image(vp.x, vp.y, vp.width, vp.height);
51+
52+
let src = ImageBuffer::from_pixel(vp.width, vp.height, Rgba([255u8, 0, 0, 255]));
53+
let part = ImageBuffer::from_pixel(at.width, at.height, Rgba([255u8, 0, 0, 255]));
54+
let view = image::GenericImageView::view(&src, at.x, at.y, at.width, at.height);
55+
56+
const BG: Rgba<u8> = Rgba([0u8, 0, 0, 255]);
57+
let samples = image::flat::FlatSamples::with_monocolor(&BG, at.width, at.height);
58+
let singular = samples.as_view().unwrap();
59+
60+
let mut samples = src.as_flat_samples();
61+
samples.layout.width = at.width / 2;
62+
samples.layout.width_stride *= 2;
63+
samples.layout.height = at.height / 2;
64+
samples.layout.height_stride *= 2;
65+
let skip = samples.as_view().unwrap();
66+
67+
c.bench_function("copy_subimage_from", |b| {
68+
b.iter(|| target.copy_from(black_box(&src), 0, 0));
69+
});
70+
71+
c.bench_function("copy_subimage_at", |b| {
72+
b.iter(|| target.copy_from(black_box(&part), at.x, at.y));
73+
});
74+
75+
c.bench_function("copy_subimage_view", |b| {
76+
b.iter(|| target.copy_from(black_box(&*view), at.x, at.y));
77+
});
78+
79+
c.bench_function("copy_subimage_fill", |b| {
80+
b.iter(|| target.copy_from(black_box(&singular), at.x, at.y));
81+
});
82+
83+
c.bench_function("copy_subimage_strides", |b| {
84+
b.iter(|| target.copy_from(black_box(&skip), at.x, at.y));
1085
});
1186
}
1287

13-
criterion_group!(benches, bench_copy_from);
88+
criterion_group!(benches, bench_copy_from, bench_copy_subimage_from);
1489
criterion_main!(benches);
90+
91+
// Backport of the constructor.
92+
fn rect_from_xy_ranges(x: std::ops::Range<u32>, y: std::ops::Range<u32>) -> image::math::Rect {
93+
image::math::Rect {
94+
x: x.start,
95+
y: y.start,
96+
width: x.end - x.start,
97+
height: y.end - y.start,
98+
}
99+
}

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};
@@ -1238,6 +1238,16 @@ where
12381238
*self.get_pixel(x, y)
12391239
}
12401240

1241+
fn to_pixel_view(&self) -> Option<ViewOfPixel<'_, Self::Pixel>> {
1242+
let samples = FlatSamples {
1243+
samples: &*self.data,
1244+
layout: self.sample_layout(),
1245+
color_hint: None,
1246+
};
1247+
1248+
samples.into_view().ok()
1249+
}
1250+
12411251
/// Returns the pixel located at (x, y), ignoring bounds checking.
12421252
#[inline(always)]
12431253
unsafe fn unsafe_get_pixel(&self, x: u32, y: u32) -> P {
@@ -1280,6 +1290,61 @@ where
12801290
self.get_pixel_mut(x, y).blend(&p);
12811291
}
12821292

1293+
fn copy_from_samples(
1294+
&mut self,
1295+
view: ViewOfPixel<'_, Self::Pixel>,
1296+
x: u32,
1297+
y: u32,
1298+
) -> ImageResult<()> {
1299+
let (width, height) = view.dimensions();
1300+
let pix_stride = usize::from(<Self::Pixel as Pixel>::CHANNEL_COUNT);
1301+
Rect::from_image_at(&view, x, y).test_in_bounds(self)?;
1302+
1303+
if width == 0 || height == 0 || pix_stride == 0 {
1304+
return Ok(());
1305+
}
1306+
1307+
// Since this image is not empty, all its indices fit into `usize` as they address the
1308+
// memory resident buffer of `self`.
1309+
let row_len = width as usize * pix_stride;
1310+
let img_sh = self.width as usize;
1311+
1312+
let (sw, sh) = view.strides_wh();
1313+
let view_samples: &[_] = view.samples();
1314+
let inner = self.inner_pixels_mut();
1315+
1316+
let img_pixel_indices_unchecked =
1317+
|x: u32, y: u32| (y as usize * img_sh + x as usize) * pix_stride;
1318+
1319+
// Can we use row-by-row byte copy?
1320+
if sw == pix_stride {
1321+
for j in 0..height {
1322+
let start = img_pixel_indices_unchecked(x, j + y);
1323+
let img_row = &mut inner[start..][..row_len];
1324+
let view_row = &view_samples[j as usize * sh..][..row_len];
1325+
img_row.copy_from_slice(view_row);
1326+
}
1327+
1328+
return Ok(());
1329+
}
1330+
1331+
// Fallback behavior.
1332+
for j in 0..height {
1333+
let img_start = img_pixel_indices_unchecked(x, j + y);
1334+
let img_row = &mut inner[img_start..][..row_len];
1335+
let pixels = img_row.chunks_exact_mut(pix_stride);
1336+
1337+
let view_start = j as usize * sh;
1338+
1339+
for (i, sp) in pixels.enumerate() {
1340+
let view_pixel = &view_samples[i * sw + view_start..][..pix_stride];
1341+
sp.copy_from_slice(view_pixel);
1342+
}
1343+
}
1344+
1345+
Ok(())
1346+
}
1347+
12831348
fn copy_within(&mut self, source: Rect, x: u32, y: u32) -> bool {
12841349
let Rect {
12851350
x: sx,
@@ -1769,8 +1834,8 @@ mod test {
17691834
use crate::math::Rect;
17701835
use crate::metadata::Cicp;
17711836
use crate::metadata::CicpTransform;
1772-
use crate::GenericImage as _;
17731837
use crate::ImageFormat;
1838+
use crate::{GenericImage as _, GenericImageView as _};
17741839
use crate::{Luma, LumaA, Pixel, Rgb, Rgba};
17751840
use num_traits::Zero;
17761841

@@ -2131,6 +2196,160 @@ mod test {
21312196
let result = target.copy_from_color_space(&source, options);
21322197
assert!(matches!(result, Err(crate::ImageError::Parameter(_))));
21332198
}
2199+
2200+
/// We specialize copy_from on types that provide `as_samples` so test that.
2201+
#[test]
2202+
fn copy_from_subimage_to_middle() {
2203+
let mut source = RgbImage::new(16, 16);
2204+
let mut target = RgbImage::new(16, 16);
2205+
2206+
source.put_pixel(8, 8, Rgb([255, 8, 8]));
2207+
source.put_pixel(9, 8, Rgb([255, 9, 8]));
2208+
source.put_pixel(9, 9, Rgb([255, 9, 9]));
2209+
2210+
let view = source.view(8, 8, 2, 2);
2211+
assert!(target.copy_from(&*view, 4, 4).is_ok());
2212+
2213+
// Check the pixel was copied.
2214+
assert_eq!(*target.get_pixel(4, 4), Rgb([255, 8, 8]));
2215+
assert_eq!(*target.get_pixel(5, 4), Rgb([255, 9, 8]));
2216+
assert_eq!(*target.get_pixel(5, 5), Rgb([255, 9, 9]));
2217+
2218+
// Check that were the only copied pixel.
2219+
assert_eq!(
2220+
target.iter().copied().map(usize::from).sum::<usize>(),
2221+
3 * (255 + 8 + 9)
2222+
);
2223+
}
2224+
2225+
#[test]
2226+
fn copy_from_band() {
2227+
let source = RgbImage::from_fn(16, 8, |x, y| Rgb([x as u8, y as u8, 0]));
2228+
let mut target = RgbImage::new(16, 16);
2229+
2230+
assert!(target.copy_from(&source, 0, 4).is_ok());
2231+
2232+
let lhs = source.chunks_exact(48);
2233+
let rhs = target.chunks_exact(48).skip(4).take(8);
2234+
2235+
assert!(lhs.eq(rhs));
2236+
}
2237+
2238+
#[test]
2239+
fn copy_from_pixel() {
2240+
let bg = Rgb([255, 0, 128]);
2241+
let samples = crate::flat::FlatSamples::with_monocolor(&bg, 4, 4);
2242+
let source = samples.as_view().unwrap();
2243+
2244+
let mut target = RgbImage::new(16, 16);
2245+
assert!(target.copy_from(&source, 4, 4).is_ok());
2246+
2247+
for i in 4..8 {
2248+
for j in 4..8 {
2249+
assert_eq!(*target.get_pixel(i, j), bg);
2250+
}
2251+
}
2252+
2253+
assert_eq!(
2254+
target.iter().copied().map(usize::from).sum::<usize>(),
2255+
16 * (255 + 128)
2256+
);
2257+
}
2258+
2259+
#[test]
2260+
fn copy_from_strided() {
2261+
#[rustfmt::skip]
2262+
let sample_data = [
2263+
1, 0xff, 0, 0, 2, 0xff,
2264+
3, 0xff, 0, 0, 4, 0xff
2265+
];
2266+
2267+
let samples = crate::flat::FlatSamples {
2268+
samples: &sample_data,
2269+
layout: crate::flat::SampleLayout {
2270+
channels: 2,
2271+
channel_stride: 1,
2272+
width: 2,
2273+
width_stride: 4,
2274+
height: 2,
2275+
height_stride: 6,
2276+
},
2277+
color_hint: None,
2278+
};
2279+
2280+
let source = samples.as_view::<LumaA<u8>>().unwrap();
2281+
let mut target = crate::GrayAlphaImage::new(16, 16);
2282+
assert!(target.copy_from(&source, 4, 4).is_ok());
2283+
2284+
assert_eq!(*target.get_pixel(4, 4), LumaA([1, 0xff]));
2285+
assert_eq!(*target.get_pixel(5, 4), LumaA([2, 0xff]));
2286+
assert_eq!(*target.get_pixel(4, 5), LumaA([3, 0xff]));
2287+
assert_eq!(*target.get_pixel(5, 5), LumaA([4, 0xff]));
2288+
2289+
assert_eq!(
2290+
target.iter().copied().map(usize::from).sum::<usize>(),
2291+
sample_data.iter().copied().map(usize::from).sum::<usize>(),
2292+
);
2293+
}
2294+
2295+
#[test]
2296+
fn copy_from_strided_subimage() {
2297+
#[rustfmt::skip]
2298+
let sample_data = [
2299+
1, 0xff, 0, 0, 2, 0xff,
2300+
3, 0xff, 0, 0, 4, 0xff
2301+
];
2302+
2303+
let samples = crate::flat::FlatSamples {
2304+
samples: &sample_data,
2305+
layout: crate::flat::SampleLayout {
2306+
channels: 2,
2307+
channel_stride: 1,
2308+
width: 2,
2309+
width_stride: 4,
2310+
height: 2,
2311+
height_stride: 6,
2312+
},
2313+
color_hint: None,
2314+
};
2315+
2316+
let view = samples.as_view::<LumaA<u8>>().unwrap();
2317+
let source = view.view(1, 0, 1, 2);
2318+
2319+
let mut target = crate::GrayAlphaImage::new(16, 16);
2320+
assert!(target.copy_from(&*source, 4, 4).is_ok());
2321+
2322+
assert_eq!(*target.get_pixel(4, 4), LumaA([2, 0xff]));
2323+
assert_eq!(*target.get_pixel(4, 5), LumaA([4, 0xff]));
2324+
2325+
assert_eq!(
2326+
target.iter().copied().map(usize::from).sum::<usize>(),
2327+
2usize + 0xff + 4 + 0xff
2328+
);
2329+
}
2330+
2331+
#[test]
2332+
fn copy_from_subimage_subimage() {
2333+
let mut source = RgbImage::new(16, 16);
2334+
let mut target = RgbImage::new(16, 16);
2335+
2336+
source.put_pixel(8, 8, Rgb([255, 8, 8]));
2337+
source.put_pixel(9, 8, Rgb([255, 9, 8]));
2338+
source.put_pixel(9, 9, Rgb([255, 9, 9]));
2339+
2340+
let view = source.view(8, 8, 2, 2);
2341+
let view = view.view(1, 0, 1, 1);
2342+
assert!(target.copy_from(&*view, 4, 4).is_ok());
2343+
2344+
// Check the pixel was copied.
2345+
assert_eq!(*target.get_pixel(4, 4), Rgb([255, 9, 8]));
2346+
2347+
// Check that was the only copied pixel.
2348+
assert_eq!(
2349+
target.iter().copied().map(usize::from).sum::<usize>(),
2350+
255 + 9 + 8
2351+
);
2352+
}
21342353
}
21352354

21362355
#[cfg(test)]

0 commit comments

Comments
 (0)