imageproc/
seam_carving.rs

1//! An implementation of [seam carving]. Currently in a pretty rough state.
2//! See examples/seam_carving.rs for an example.
3//!
4//! [seam carving]: https://en.wikipedia.org/wiki/Seam_carving
5
6use crate::definitions::{HasBlack, Image};
7use crate::gradients::sobel_gradient_map;
8use crate::map::{map_colors, WithChannel};
9use image::{GrayImage, Luma, Pixel, Rgb};
10use std::cmp::min;
11
12/// An image seam connecting the bottom of an image to its top (in that order).
13pub struct VerticalSeam(Vec<u32>);
14
15/// Reduces the width of an image using seam carving.
16///
17/// Warning: this is very slow! It implements the algorithm from
18/// https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf, with some
19/// extra unnecessary allocations thrown in. Rather than attempting to optimise the implementation
20/// of this inherently slow algorithm, the planned next step is to switch to the algorithm from
21/// https://users.cs.cf.ac.uk/Paul.Rosin/resources/papers/seam-carving-ChinaF.pdf.
22pub fn shrink_width<P>(image: &Image<P>, target_width: u32) -> Image<P>
23// TODO: this is pretty silly! We should just be able to express that we want a pixel which is a slice of integral values
24where
25    P: Pixel<Subpixel = u8> + WithChannel<u16> + WithChannel<i16>,
26    <P as WithChannel<u16>>::Pixel: HasBlack,
27{
28    assert!(
29        target_width <= image.width(),
30        "target_width must be <= input image width"
31    );
32
33    let iterations = image.width() - target_width;
34    let mut result = image.clone();
35
36    for _ in 0..iterations {
37        let seam = find_vertical_seam(&result);
38        result = remove_vertical_seam(&result, &seam);
39    }
40
41    result
42}
43
44/// Computes an 8-connected path from the bottom of the image to the top whose sum of
45/// gradient magnitudes is minimal.
46pub fn find_vertical_seam<P>(image: &Image<P>) -> VerticalSeam
47where
48    P: Pixel<Subpixel = u8> + WithChannel<u16> + WithChannel<i16>,
49    <P as WithChannel<u16>>::Pixel: HasBlack,
50{
51    let (width, height) = image.dimensions();
52    assert!(
53        image.width() >= 2,
54        "Cannot find seams if image width is < 2"
55    );
56
57    let mut gradients = sobel_gradient_map(image, |p| {
58        let gradient_sum: u16 = p.channels().iter().sum();
59        let gradient_mean: u16 = gradient_sum / P::CHANNEL_COUNT as u16;
60        Luma([gradient_mean as u32])
61    });
62
63    // Find the least energy path through the gradient image.
64    for y in 1..height {
65        for x in 0..width {
66            set_path_energy(&mut gradients, x, y);
67        }
68    }
69
70    // Retrace our steps to find the vertical seam.
71    let mut min_x = 0;
72    let mut min_energy = gradients.get_pixel(0, height - 1)[0];
73
74    for x in 1..width {
75        let c = gradients.get_pixel(x, height - 1)[0];
76        if c < min_energy {
77            min_x = x;
78            min_energy = c;
79        }
80    }
81
82    let mut seam = Vec::with_capacity(height as usize);
83
84    seam.push(min_x);
85
86    let mut last_x = min_x;
87
88    for y in (1..height).rev() {
89        let above = gradients.get_pixel(last_x, y - 1)[0];
90        if last_x > 0 {
91            let left = gradients.get_pixel(last_x - 1, y - 1)[0];
92            if left < above {
93                min_x = last_x - 1;
94                min_energy = left;
95            }
96        }
97        if last_x < width - 1 {
98            let right = gradients.get_pixel(last_x + 1, y - 1)[0];
99            if right < min_energy {
100                min_x = last_x + 1;
101                min_energy = right;
102            }
103        }
104
105        last_x = min_x;
106        seam.push(min_x);
107    }
108
109    VerticalSeam(seam)
110}
111
112/// Assumes that the previous rows have all been processed.
113fn set_path_energy(path_energies: &mut Image<Luma<u32>>, x: u32, y: u32) {
114    let above = path_energies.get_pixel(x, y - 1)[0];
115    let mut min_energy = above;
116
117    if x > 0 {
118        let above_left = path_energies.get_pixel(x - 1, y - 1)[0];
119        min_energy = min(above, above_left);
120    }
121    if x < path_energies.width() - 1 {
122        let above_right = path_energies.get_pixel(x + 1, y - 1)[0];
123        min_energy = min(min_energy, above_right);
124    }
125
126    let current = path_energies.get_pixel(x, y)[0];
127    path_energies.put_pixel(x, y, Luma([min_energy + current]));
128}
129
130/// Returns the result of removing `seam` from `image`.
131// This should just mutate an image in place. The problem is that we don't have a
132// way of talking about views of ImageBuffer without devolving into supporting
133// arbitrary GenericImages. And a lot of other functions don't support those because
134// it would make them a lot slower.
135pub fn remove_vertical_seam<P>(image: &Image<P>, seam: &VerticalSeam) -> Image<P>
136where
137    P: Pixel,
138{
139    assert!(
140        seam.0.len() as u32 == image.height(),
141        "seam length does not match image height"
142    );
143
144    let (width, height) = image.dimensions();
145    let mut out = Image::new(width - 1, height);
146
147    for y in 0..height {
148        let x_seam = seam.0[(height - y - 1) as usize];
149        for x in 0..x_seam {
150            out.put_pixel(x, y, *image.get_pixel(x, y));
151        }
152        for x in (x_seam + 1)..width {
153            out.put_pixel(x - 1, y, *image.get_pixel(x, y));
154        }
155    }
156
157    out
158}
159
160/// Draws a series of `seams` on `image` in red. Assumes that the provided seams were
161/// removed in the given order from the input image.
162pub fn draw_vertical_seams(image: &GrayImage, seams: &[VerticalSeam]) -> Image<Rgb<u8>> {
163    let height = image.height();
164
165    let mut offsets = vec![vec![]; height as usize];
166    let mut out = map_colors(image, |p| p.to_rgb());
167
168    for seam in seams {
169        for (y, x) in (0..height).rev().zip(&seam.0) {
170            let mut x_original = *x;
171            for o in &offsets[y as usize] {
172                if *o < *x {
173                    x_original += 1;
174                }
175            }
176            out.put_pixel(x_original, y, Rgb([255, 0, 0]));
177            offsets[y as usize].push(x_original);
178        }
179    }
180
181    out
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::utils::gray_bench_image;
188    use test::{black_box, Bencher};
189
190    macro_rules! bench_shrink_width {
191        ($name:ident, side: $s:expr, shrink_by: $m:expr) => {
192            #[bench]
193            fn $name(b: &mut Bencher) {
194                let image = gray_bench_image($s, $s);
195                b.iter(|| {
196                    let filtered = shrink_width(&image, $s - $m);
197                    black_box(filtered);
198                })
199            }
200        };
201    }
202
203    bench_shrink_width!(bench_shrink_width_s100_r1, side: 100, shrink_by: 1);
204    bench_shrink_width!(bench_shrink_width_s100_r4, side: 100, shrink_by: 4);
205    bench_shrink_width!(bench_shrink_width_s100_r8, side: 100, shrink_by: 8);
206}