picture/processing/
mod.rs

1use crate::prelude::*;
2use crate::util::{dimension_to_usize, index_point};
3use crate::Dimension;
4
5/// Common sampling filters.
6pub mod filters;
7
8// TODO: maybe think of a better name?
9/// Trait for channel types that can be processed.
10pub trait Processable: Copy {
11    /// Converts this value to a [`f32`].
12    fn to_f32(self) -> f32;
13
14    /// Converts a [`f32`] into [`Self`].
15    ///
16    /// For numeric types, note that the value _will not_ necessarily be in the
17    /// valid range (e.g. it might be 258.2 for a [`u8`]). You should clamp the
18    /// value in these cases.
19    fn from_f32(value: f32) -> Self;
20}
21
22macro_rules! impl_processable {
23    ($($type:ty),*) => {
24        $(
25            impl Processable for $type {
26                #[inline(always)]
27                fn to_f32(self) -> f32 {
28                    self as f32
29                }
30
31                #[inline(always)]
32                fn from_f32(value: f32) -> Self {
33                    value.clamp(Self::MIN as f32, Self::MAX as f32) as Self
34                }
35            }
36        )*
37    };
38}
39
40impl_processable!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, usize, isize);
41
42// useful resources:
43// - https://entropymine.com/imageworsener
44// - https://cs1230.graphics/lectures - specifically image processing I, II and III
45
46/// Resamples a view horizontally to the given width using the given filter.
47/// Height is kept the same.
48///
49/// `window` is the maximum distance a pixel can be to the one being currently
50/// processed before being cut out of the filter.
51#[must_use = "the resampled buffer is returned and the original view is left unmodified"]
52pub fn resample_horizontal<I, P, C, F, const N: usize>(
53    view: &I,
54    width: Dimension,
55    filter: F,
56    window: f32,
57) -> ImgBuf<P, Vec<P>>
58where
59    I: ImgView<Pixel = P>,
60    P: Pixel<Channels = [C; N]>,
61    C: Processable,
62    F: Fn(f32) -> f32,
63{
64    if width == 0 {
65        return ImgBuf::from_container(Vec::new(), width, view.height());
66    }
67
68    // create container for result
69    let mut container =
70        Vec::with_capacity(dimension_to_usize(width) * dimension_to_usize(view.height()));
71    let container_pixels = container.spare_capacity_mut();
72
73    // find the ratio between the source width and the target width
74    let ratio = view.width() as f32 / width as f32;
75    let sampling_ratio = ratio.max(1.0);
76    let inverse_sampling_ratio = 1.0 / sampling_ratio;
77
78    // if we're upsampling (ratio < 1), there's no need to scale things.
79    // however, if we're downsampling (ratio > 1), we need to scale stuff
80    // by the ratio so that we can preserve information.
81    // sampling_ratio's purpose is exactly that: it's value is 1.0 if
82    // upsampling, ratio if downsampling.
83
84    // scale the window accordingly
85    let window = window * sampling_ratio;
86
87    // precalculate weights
88    let max_src_x_f32 = (view.width() - 1) as f32;
89    let mut weights = Vec::with_capacity((2 * (window as usize) + 1) * dimension_to_usize(width));
90    let mut weights_start_index = Vec::with_capacity(width as usize);
91    for target_x in 0..width {
92        let equivalent_src_x = target_x as f32 * ratio + 0.5 * (ratio - 1.0);
93
94        let min_src_pixel_x = (equivalent_src_x - window).clamp(0.0, max_src_x_f32) as Dimension;
95        let max_src_pixel_x = (equivalent_src_x + window).clamp(0.0, max_src_x_f32) as Dimension;
96
97        weights_start_index.push(weights.len());
98        for src_pixel_x in min_src_pixel_x..=max_src_pixel_x {
99            weights.push(filter(
100                (src_pixel_x as f32 - equivalent_src_x) * inverse_sampling_ratio,
101            ));
102        }
103    }
104
105    // now actually resample
106    for target_x in 0..width {
107        // these could be cached as well, but it makes no performance difference (and increases
108        // memory usage), so we just calculate them again
109        let equivalent_src_x = target_x as f32 * ratio + (1.0 - 1.0 / ratio) / (2.0 / ratio);
110
111        let min_src_pixel_x = (equivalent_src_x - window).clamp(0.0, max_src_x_f32) as Dimension;
112        let max_src_pixel_x = (equivalent_src_x + window).clamp(0.0, max_src_x_f32) as Dimension;
113
114        let weights_start = weights_start_index[target_x as usize];
115        for target_y in 0..view.height() {
116            let mut weight_sum = 0f32;
117            let mut channel_value_sum = [0f32; N];
118            for (index, src_pixel_x) in (min_src_pixel_x..=max_src_pixel_x).enumerate() {
119                // SAFETY: target_y is in the 0..img.height() range and src_pixel_x is clamped
120                // between 0 and img.width() - 1. therefore, this coordinate is always in bounds.
121                let src_pixel = unsafe { view.pixel_unchecked((src_pixel_x, target_y)) };
122                let channels = src_pixel.channels();
123                let weight = weights[weights_start + index];
124                weight_sum += weight;
125
126                for channel_index in 0..N {
127                    let value = weight * channels[channel_index].to_f32();
128                    channel_value_sum[channel_index] += value;
129                }
130            }
131
132            let result: arrayvec::ArrayVec<_, N> = channel_value_sum
133                .into_iter()
134                .map(|v| C::from_f32(v / weight_sum))
135                .collect();
136
137            // SAFETY: this index will always be valid since target_x and target_y are always in
138            // the correct range.
139            unsafe {
140                container_pixels
141                    .get_unchecked_mut(index_point((target_x, target_y), width))
142                    .write(P::new(result.into_inner_unchecked()));
143            }
144        }
145    }
146
147    // SAFETY: all pixels have already been initialized in the previous loop.
148    unsafe {
149        let size = dimension_to_usize(width) * dimension_to_usize(view.height());
150        container.set_len(size);
151    }
152
153    ImgBuf::from_container(container, width, view.height())
154}
155
156/// Resamples a view vertically to the given height using the given filter.
157/// Width is kept the same.
158///
159/// `window` is the maximum distance a pixel can be to the one being currently
160/// processed before being cut out of the filter.
161#[must_use = "the resampled buffer is returned and the original view is left unmodified"]
162pub fn resample_vertical<I, P, C, F, const N: usize>(
163    view: &I,
164    height: Dimension,
165    filter: F,
166    window: f32,
167) -> ImgBuf<P, Vec<P>>
168where
169    I: ImgView<Pixel = P>,
170    P: Pixel<Channels = [C; N]>,
171    C: Processable,
172    F: Fn(f32) -> f32,
173{
174    if height == 0 {
175        return ImgBuf::from_container(Vec::new(), view.width(), height);
176    }
177
178    // create container for result
179    let mut container =
180        Vec::with_capacity(dimension_to_usize(height) * dimension_to_usize(view.width()));
181    let container_pixels = container.spare_capacity_mut();
182
183    // find the ratio between the source height and the target height
184    let ratio = view.height() as f32 / height as f32;
185    let sampling_ratio = ratio.max(1.0);
186    let inverse_sampling_ratio = 1.0 / sampling_ratio;
187
188    // if we're upsampling (ratio < 1), there's no need to scale things.
189    // however, if we're downsampling (ratio > 1), we need to scale stuff
190    // by the ratio so that we can preserve information.
191    // sampling_ratio's purpose is exactly that: it's value is 1.0 if
192    // upsampling, ratio if downsampling.
193
194    // scale the window accordingly
195    let window = window * sampling_ratio;
196
197    // precalculate weights
198    let max_src_y_f32 = (view.height() - 1) as f32;
199    let mut weights = Vec::with_capacity((2 * (window as usize) + 1) * dimension_to_usize(height));
200    let mut weights_start_index = Vec::with_capacity(height as usize);
201    for target_y in 0..height {
202        let equivalent_src_y = target_y as f32 * ratio + 0.5 * (ratio - 1.0);
203
204        let min_src_pixel_y = (equivalent_src_y - window).clamp(0.0, max_src_y_f32) as Dimension;
205        let max_src_pixel_y = (equivalent_src_y + window).clamp(0.0, max_src_y_f32) as Dimension;
206
207        weights_start_index.push(weights.len());
208        for src_pixel_y in min_src_pixel_y..=max_src_pixel_y {
209            weights.push(filter(
210                (src_pixel_y as f32 - equivalent_src_y) * inverse_sampling_ratio,
211            ));
212        }
213    }
214
215    // now actually resample
216    for target_y in 0..height {
217        // these could be cached as well, but it makes no performance difference (and increases
218        // memory usage), so we just calculate them again
219        let equivalent_src_y = target_y as f32 * ratio + 0.5 * (ratio - 1.0);
220
221        let min_src_pixel_y = (equivalent_src_y - window).clamp(0.0, max_src_y_f32) as Dimension;
222        let max_src_pixel_y = (equivalent_src_y + window).clamp(0.0, max_src_y_f32) as Dimension;
223
224        let weights_start = weights_start_index[target_y as usize];
225        for target_x in 0..view.width() {
226            let mut weight_sum = 0f32;
227            let mut channel_value_sum = [0f32; N];
228            for (index, src_pixel_y) in (min_src_pixel_y..=max_src_pixel_y).enumerate() {
229                // SAFETY: target_x is in the 0..img.width() range and src_pixel_y is clamped
230                // between 0 and img.height() - 1. therefore, this coordinate is always in bounds.
231                let src_pixel = unsafe { view.pixel_unchecked((target_x, src_pixel_y)) };
232                let channels = src_pixel.channels();
233                let weight = weights[weights_start + index];
234                weight_sum += weight;
235
236                for channel_index in 0..N {
237                    let value = weight * channels[channel_index].to_f32();
238                    channel_value_sum[channel_index] += value;
239                }
240            }
241
242            let result: arrayvec::ArrayVec<_, N> = channel_value_sum
243                .into_iter()
244                .map(|v| C::from_f32(v / weight_sum))
245                .collect();
246
247            // SAFETY: this index will always be valid since target_x and target_y are always in
248            // the correct range.
249            unsafe {
250                container_pixels
251                    .get_unchecked_mut(index_point((target_x, target_y), view.width()))
252                    .write(P::new(result.into_inner_unchecked()));
253            }
254        }
255    }
256
257    // SAFETY: all pixels have already been initialized in the previous loop.
258    unsafe {
259        let size = dimension_to_usize(height) * dimension_to_usize(view.width());
260        container.set_len(size);
261    }
262
263    ImgBuf::from_container(container, view.width(), height)
264}
265
266/// Resamples a view to the given dimensions using the given filter. This is
267/// equivalent to doing a horizontal resample followed by a vertical one.
268///
269/// `window` is the maximum distance a pixel can be to the one being currently
270/// processed before being cut out of the filter.
271#[must_use = "the resampled buffer is returned and the original view is left unmodified"]
272pub fn resample<I, P, C, F, const N: usize>(
273    view: &I,
274    (width, height): (Dimension, Dimension),
275    filter: F,
276    window: f32,
277) -> ImgBuf<P, Vec<P>>
278where
279    I: ImgView<Pixel = P>,
280    P: Pixel<Channels = [C; N]>,
281    C: Processable,
282    F: Fn(f32) -> f32,
283{
284    let horizontal = resample_horizontal(view, width, &filter, window);
285    resample_vertical(&horizontal, height, filter, window)
286}
287
288/// Performs a box blur in a view and returns the result.
289#[must_use = "the blurred buffer is returned and the original view is left unmodified"]
290pub fn box_blur<I, P, C, const N: usize>(view: &I, strength: f32) -> ImgBuf<P, Vec<P>>
291where
292    I: ImgView<Pixel = P>,
293    P: Pixel<Channels = [C; N]>,
294    C: Processable,
295{
296    assert!(strength > 0.0);
297    resample(view, view.dimensions(), filters::box_filter, strength)
298}
299
300/// Performs a gaussian blur in a view and returns the result.
301#[must_use = "the blurred buffer is returned and the original view is left unmodified"]
302pub fn gaussian_blur<I, P, C, const N: usize>(view: &I, strength: f32) -> ImgBuf<P, Vec<P>>
303where
304    I: ImgView<Pixel = P>,
305    P: Pixel<Channels = [C; N]>,
306    C: Processable,
307{
308    assert!(strength > 0.0);
309    resample(
310        view,
311        view.dimensions(),
312        |x| filters::gaussian(x, strength),
313        2.0 * strength,
314    )
315}
316
317/// Filter type to use when resizing a view using the [`resize`] function.
318#[derive(Debug, Clone, Copy, PartialEq)]
319pub enum ResizeFilter {
320    Box,
321    Triangle,
322    BSpline,
323    Mitchell,
324    CatmullRom,
325    Lanczos2,
326    Lanczos3,
327}
328
329/// Resizes a view to the given dimensions using the given resizing filter.
330#[must_use = "the resized buffer is returned and the original view is left unmodified"]
331pub fn resize<I, P, C, const N: usize>(
332    view: &I,
333    dimensions: (Dimension, Dimension),
334    filter: ResizeFilter,
335) -> ImgBuf<P, Vec<P>>
336where
337    I: ImgView<Pixel = P>,
338    P: Pixel<Channels = [C; N]>,
339    C: Processable,
340{
341    match filter {
342        ResizeFilter::Box => resample(view, dimensions, filters::box_filter, 0.0),
343        ResizeFilter::Triangle => resample(view, dimensions, filters::triangle, 1.0),
344        ResizeFilter::BSpline => resample(view, dimensions, filters::b_spline, 2.0),
345        ResizeFilter::Mitchell => resample(view, dimensions, filters::mitchell, 2.0),
346        ResizeFilter::CatmullRom => resample(view, dimensions, filters::catmull_rom, 2.0),
347        ResizeFilter::Lanczos2 => resample(view, dimensions, filters::lanczos2, 2.0),
348        ResizeFilter::Lanczos3 => resample(view, dimensions, filters::lanczos3, 3.0),
349    }
350}
351
352/// Flips the given view horizontally.
353pub fn flip_horizontal<I>(view: &mut I)
354where
355    I: ImgViewMut,
356{
357    for y in 0..view.height() {
358        for x in 0..(view.width() / 2) {
359            let left_pixel_bounds = Rect::new((x, y), (1, 1));
360            let right_pixel_bounds = Rect::new((view.width() - 1 - x, y), (1, 1));
361
362            let [mut left, mut right] = view
363                .view_mut_multiple([left_pixel_bounds, right_pixel_bounds])
364                .unwrap();
365
366            std::mem::swap(
367                left.pixel_mut((0, 0)).unwrap(),
368                right.pixel_mut((0, 0)).unwrap(),
369            );
370        }
371    }
372}
373
374/// Flips the given view vertically.
375pub fn flip_vertical<I>(view: &mut I)
376where
377    I: ImgViewMut,
378{
379    for x in 0..view.width() {
380        for y in 0..(view.height() / 2) {
381            let top_pixel_bounds = Rect::new((x, y), (1, 1));
382            let bottom_pixel_bounds = Rect::new((x, view.height() - 1 - y), (1, 1));
383
384            let [mut top, mut bottom] = view
385                .view_mut_multiple([top_pixel_bounds, bottom_pixel_bounds])
386                .unwrap();
387
388            std::mem::swap(
389                top.pixel_mut((0, 0)).unwrap(),
390                bottom.pixel_mut((0, 0)).unwrap(),
391            );
392        }
393    }
394}