Skip to main content

kornia_imgproc/
padding.rs

1use kornia_image::{allocator::ImageAllocator, Image, ImageError, ImageSize};
2use rayon::prelude::*;
3
4/// A border type for the spatial padding.
5#[derive(Debug, Clone, Copy)]
6pub enum PaddingMode {
7    /// This border type fills the border with a single, constant color value.
8    ///
9    /// Example: ...d c b a | 0 0 0 0...
10    Constant,
11
12    /// This border type takes the outermost row or column of pixels and repeats it into the padded region.
13    ///
14    /// Example: ...d c b a | a a a a...
15    Replicate,
16
17    /// This border type reflects the pixel values at the boundary, starting with the pixel 'next' to the edge.
18    ///
19    /// Example: ...d c b a | b c d e...
20    Reflect101,
21
22    /// This border type reflects the pixel values at the boundary, starting with the edge pixel itself.
23    ///
24    /// Example: ...d c b a | a b c d...
25    Reflect,
26
27    /// This border type wraps the content from the opposite side to fill the border.
28    ///
29    /// Example: ...d c b a | w x y z...
30    Wrap,
31}
32impl PaddingMode {
33    #[inline]
34    fn reflect(i: isize, len: usize) -> usize {
35        if len == 1 {
36            return 0;
37        }
38        let len = len as isize;
39        let mut i = i;
40        while i < 0 || i >= len {
41            if i < 0 {
42                i = -i - 1;
43            } else if i >= len {
44                i = 2 * len - i - 1;
45            }
46        }
47        i as usize
48    }
49
50    #[inline]
51    fn reflect101(i: isize, len: usize) -> usize {
52        if len == 1 {
53            return 0;
54        }
55        let len = len as isize;
56        let mut i = i;
57        while i < 0 || i >= len {
58            if i < 0 {
59                i = -i;
60            } else if i >= len {
61                i = 2 * len - i - 2;
62            }
63        }
64        i as usize
65    }
66
67    #[inline]
68    fn wrap(i: isize, len: usize) -> usize {
69        ((i % len as isize + len as isize) % len as isize) as usize
70    }
71
72    /// Maps index `i` to a valid index i.e. within `[0, len)` according to the padding mode.
73    ///
74    /// - `Replicate`: clamp to edge
75    /// - `Reflect`: mirror including edge
76    /// - `Reflect101`: mirror excluding edge
77    /// - `Wrap`: circular wrap
78    /// - `Constant`: returns 0 (not used directly)
79    ///
80    /// # Arguments
81    /// - `i`: The (possibly out-of-range) coordinate index.
82    /// - `len`: The valid length of the dimension.
83    ///
84    /// # Returns
85    /// A valid mapped index within `[0, len)`.
86    #[inline]
87    pub fn map_index(&self, i: isize, len: usize) -> usize {
88        match self {
89            PaddingMode::Replicate => i.clamp(0, len as isize - 1) as usize,
90            PaddingMode::Reflect => Self::reflect(i, len),
91            PaddingMode::Reflect101 => Self::reflect101(i, len),
92            PaddingMode::Wrap => Self::wrap(i, len),
93            PaddingMode::Constant => 0,
94        }
95    }
96
97    /// Applies the selected padding mode to fill image borders in `new_data`.
98    ///
99    /// # Arguments
100    /// - `new_data`: Target image buffer (already containing the original image in the center).
101    /// - `old_width`, `old_height`: Dimensions of the original image.
102    /// - `new_width`, `new_height`: Dimensions of the padded image.
103    /// - `padding`: `left`, `right`, `top` and `bottom` padding extents in pixels.
104    ///
105    /// # Notes
106    /// - [`PaddingMode::Constant`] is assumed to be already applied when initializing `new_data`.
107    /// - Other modes (`Replicate`, `Reflect`, `Reflect101`, `Wrap`) will fill the outer border areas.
108    pub fn apply_padding<T: Copy + Send + Sync, const C: usize>(
109        &self,
110        new_data: &mut [T],
111        old_width: usize,
112        old_height: usize,
113        new_width: usize,
114        new_height: usize,
115        padding: &Padding2D,
116    ) {
117        if let PaddingMode::Constant = self {
118            return; // already filled
119        }
120
121        let top = padding.top;
122        let bottom = padding.bottom;
123        let left = padding.left;
124        let right = padding.right;
125        let row_stride = new_width * C;
126
127        const ROWS_PER_TASK: usize = 16;
128        let chunk_elems = ROWS_PER_TASK * row_stride;
129
130        // top
131        {
132            let (top_section, rest) = new_data.split_at_mut(top * row_stride);
133
134            top_section
135                .par_chunks_mut(chunk_elems)
136                .enumerate()
137                .for_each(|(chunk_idx, dst_chunk)| {
138                    let row_base = chunk_idx * ROWS_PER_TASK;
139                    dst_chunk
140                        .chunks_exact_mut(row_stride)
141                        .enumerate()
142                        .for_each(|(dr, dst_row)| {
143                            let y = row_base + dr;
144                            let src_y = self.map_index(y as isize - top as isize, old_height);
145                            let src_row = &rest[src_y * row_stride..(src_y + 1) * row_stride];
146                            dst_row.copy_from_slice(src_row);
147                        });
148                });
149        }
150
151        // bottom
152        {
153            let split_point = (new_height - bottom) * row_stride;
154            let (rest, bottom_section) = new_data.split_at_mut(split_point);
155
156            bottom_section
157                .par_chunks_mut(chunk_elems)
158                .enumerate()
159                .for_each(|(chunk_idx, dst_chunk)| {
160                    let row_base = chunk_idx * ROWS_PER_TASK;
161                    dst_chunk
162                        .chunks_exact_mut(row_stride)
163                        .enumerate()
164                        .for_each(|(dr, dst_row)| {
165                            let idx = row_base + dr;
166                            let y = new_height - bottom + idx;
167                            let src_y = self.map_index(y as isize - top as isize, old_height);
168                            let src_start = (src_y + top) * row_stride;
169                            let src_row = &rest[src_start..src_start + row_stride];
170                            dst_row.copy_from_slice(src_row);
171                        });
172                });
173        }
174
175        new_data.par_chunks_mut(chunk_elems).for_each(|dst_chunk| {
176            dst_chunk.chunks_exact_mut(row_stride).for_each(|row| {
177                // left
178                for x in 0..left {
179                    let src_x = self.map_index(x as isize - left as isize, old_width);
180                    let src_idx = (left + src_x) * C;
181                    let dst_idx = x * C;
182                    row.copy_within(src_idx..src_idx + C, dst_idx);
183                }
184
185                // right
186                for x in (new_width - right)..new_width {
187                    let src_x = self.map_index(x as isize - left as isize, old_width);
188                    let src_idx = (left + src_x) * C;
189                    let dst_idx = x * C;
190                    row.copy_within(src_idx..src_idx + C, dst_idx);
191                }
192            });
193        });
194    }
195}
196
197/// Represents 2D padding with top, bottom, left, and right values (in pixels).
198pub struct Padding2D {
199    /// Amount of padding to add on the top side.
200    pub top: usize,
201    /// Amount of padding to add on the bottom side.
202    pub bottom: usize,
203    /// Amount of padding to add on the left side.
204    pub left: usize,
205    /// Amount of padding to add on the right side.
206    pub right: usize,
207}
208impl Padding2D {
209    /// Validates that a new image size correctly matches the expected dimensions
210    /// after applying this padding to an existing image.
211    ///
212    /// # Arguments
213    /// - `old_size`: The original image size before padding.
214    /// - `new_size`: The resulting image size after padding.
215    ///
216    /// # Returns
217    /// - `true` if the `new_size` width and height are equal to
218    ///   `old_size.width + left + right` and `old_size.height + top + bottom`, respectively.
219    /// - `false` otherwise.
220    ///
221    /// # Example
222    /// ```rust
223    /// use kornia_image::ImageSize;
224    /// use kornia_imgproc::padding::Padding2D;
225    /// let padding = Padding2D { top: 1, bottom: 1, left: 2, right: 2 };
226    /// let old_size = ImageSize { width: 4, height: 4 };
227    /// let new_size = ImageSize { width: 8, height: 6 };
228    ///
229    /// assert!(padding.validate_size(old_size, new_size));
230    /// ```
231    pub fn validate_size(&self, old_size: ImageSize, new_size: ImageSize) -> bool {
232        new_size.width == old_size.width + self.left + self.right
233            && new_size.height == old_size.height + self.top + self.bottom
234    }
235}
236
237/// Creates a new image with spatial padding applied to reach target size,
238/// centering the original image and using the specified fill value and type.
239///
240/// # Arguments
241///
242/// * `src` - The source image to pad.
243/// * `dst` - The destination image where the padded output will be stored.
244/// * `padding` - The amount of padding (in pixels) for all four sides defined in [`Padding2D`] (top, bottom, left, right).
245/// * `padding_mode` - The type of border handling to use defined in [`PaddingMode`] (e.g., Constant, Replicate, Reflect, Reflect101, Wrap).
246/// * `constant_value` - The pixel value used for constant padding, specified as an array of length `C` (one value per channel).
247///
248/// # Errors
249///
250/// Returns an error if the size of `dst` does not match with the expected size
251/// i.e. after applying padding specified in argument `padding` on `src`.
252///
253/// # Example
254///
255/// ```rust
256/// use kornia_image::{allocator::CpuAllocator, ImageSize, Image};
257/// use kornia_imgproc::padding::{PaddingMode, Padding2D, spatial_padding};
258///
259/// // Create a 2x2 RGB image filled with 1s
260/// let src = Image::<u8, 3, _>::new(
261///     ImageSize { width: 2, height: 2 },
262///     vec![1u8; 2 * 2 * 3],
263///     CpuAllocator,
264/// ).unwrap();
265///
266/// // Create destination image
267/// let mut dst = Image::<u8, 3, _>::new(
268///     ImageSize { width: 4, height: 4 },
269///     vec![0u8; 4 * 4 * 3],
270///     CpuAllocator,
271/// ).unwrap();
272///
273/// // Apply 1-pixel constant padding with black (0) border
274/// spatial_padding(
275///     &src,
276///     &mut dst,
277///     Padding2D { top: 1, bottom: 1, left: 1, right: 1 },
278///     PaddingMode::Constant,
279///     [0u8; 3],
280/// ).unwrap();
281///
282/// // The resulting image should now be 4x4 in size
283/// assert_eq!(dst.size().width, 4);
284/// assert_eq!(dst.size().height, 4);
285/// ```
286pub fn spatial_padding<T, const C: usize, A1: ImageAllocator, A2: ImageAllocator>(
287    src: &Image<T, C, A1>,
288    dst: &mut Image<T, C, A2>,
289    padding: Padding2D,
290    padding_mode: PaddingMode,
291    constant_value: [T; C],
292) -> Result<(), ImageError>
293where
294    T: Copy + Default + Send + Sync,
295{
296    if !padding.validate_size(src.size(), dst.size()) {
297        return Err(ImageError::InvalidImageSize(
298            dst.width(),
299            dst.height(),
300            src.width() + padding.left + padding.right,
301            src.height() + padding.top + padding.bottom,
302        ));
303    }
304
305    let old_width = src.width();
306    let old_height = src.height();
307    let new_width = dst.width();
308    let new_height = dst.height();
309
310    let old_data = src.as_slice();
311    let new_data = dst.as_slice_mut();
312
313    match padding_mode {
314        // if constant padding, fill with constant value
315        PaddingMode::Constant => {
316            new_data
317                .chunks_exact_mut(C)
318                .for_each(|chunk| chunk.copy_from_slice(&constant_value));
319        }
320        _ => {
321            new_data.fill(T::default());
322        }
323    }
324
325    // copy old image data as center of new image data
326    let new_stride = new_width * C;
327    let old_stride = old_width * C;
328
329    let row_offset = padding.top * new_stride + padding.left * C;
330
331    for (src_row, dst_row) in old_data
332        .chunks_exact(old_stride)
333        .zip(new_data[row_offset..].chunks_exact_mut(new_stride))
334    {
335        dst_row[..old_stride].copy_from_slice(src_row);
336    }
337
338    padding_mode.apply_padding::<T, C>(
339        new_data, old_width, old_height, new_width, new_height, &padding,
340    );
341
342    Ok(())
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use kornia_image::{allocator::CpuAllocator, Image, ImageError, ImageSize};
349
350    // helper functions
351    fn make_src_2x2_rgb() -> Result<Image<u8, 3, CpuAllocator>, ImageError> {
352        Image::new(
353            ImageSize {
354                width: 2,
355                height: 2,
356            },
357            vec![1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
358            CpuAllocator,
359        )
360    }
361
362    fn make_dst_4x4_rgb() -> Result<Image<u8, 3, CpuAllocator>, ImageError> {
363        Image::new(
364            ImageSize {
365                width: 4,
366                height: 4,
367            },
368            vec![0u8; 48],
369            CpuAllocator,
370        )
371    }
372
373    const PAD_1: Padding2D = Padding2D {
374        top: 1,
375        bottom: 1,
376        left: 1,
377        right: 1,
378    };
379
380    #[test]
381    fn test_spatial_padding_constant() -> Result<(), ImageError> {
382        let src = make_src_2x2_rgb()?;
383        let mut dst = make_dst_4x4_rgb()?;
384
385        spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Constant, [9, 9, 9])?;
386
387        let d = dst.as_slice();
388
389        // corners
390        assert_eq!(&d[0..3], &[9, 9, 9]);
391        assert_eq!(&d[45..48], &[9, 9, 9]);
392
393        // top edge
394        assert_eq!(&d[3..6], &[9, 9, 9]);
395
396        // actual image
397        assert_eq!(&d[15..18], &[1, 1, 1]);
398        assert_eq!(&d[30..33], &[4, 4, 4]);
399
400        Ok(())
401    }
402
403    #[test]
404    fn test_spatial_padding_replicate() -> Result<(), ImageError> {
405        let src = make_src_2x2_rgb()?;
406        let mut dst = make_dst_4x4_rgb()?;
407
408        spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Replicate, [0, 0, 0])?;
409
410        let d = dst.as_slice();
411
412        // corners
413        assert_eq!(&d[0..3], &[1, 1, 1]);
414        assert_eq!(&d[45..48], &[4, 4, 4]);
415
416        // edges
417        assert_eq!(&d[3..6], &[1, 1, 1]);
418        assert_eq!(&d[21..24], &[2, 2, 2]);
419
420        Ok(())
421    }
422
423    #[test]
424    fn test_spatial_padding_reflect101() -> Result<(), ImageError> {
425        let src = make_src_2x2_rgb()?;
426        let mut dst = make_dst_4x4_rgb()?;
427
428        spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Reflect101, [0, 0, 0])?;
429
430        let d = dst.as_slice();
431
432        // corners
433        assert_eq!(&d[0..3], &[4, 4, 4]);
434        assert_eq!(&d[9..12], &[3, 3, 3]);
435
436        // top edge
437        assert_eq!(&d[3..6], &[3, 3, 3]);
438
439        // actual image
440        assert_eq!(&d[15..18], &[1, 1, 1]);
441
442        Ok(())
443    }
444
445    #[test]
446    fn test_spatial_padding_reflect() -> Result<(), ImageError> {
447        let src = make_src_2x2_rgb()?;
448        let mut dst = make_dst_4x4_rgb()?;
449
450        spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Reflect, [0, 0, 0])?;
451
452        let d = dst.as_slice();
453
454        // corners
455        assert_eq!(&d[0..3], &[1, 1, 1]);
456        assert_eq!(&d[9..12], &[2, 2, 2]);
457
458        // edges
459        assert_eq!(&d[6..9], &[2, 2, 2]);
460        assert_eq!(&d[39..42], &[3, 3, 3]);
461
462        Ok(())
463    }
464
465    #[test]
466    fn test_spatial_padding_wrap() -> Result<(), ImageError> {
467        let src = make_src_2x2_rgb()?;
468        let mut dst = make_dst_4x4_rgb()?;
469
470        spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Wrap, [0, 0, 0])?;
471
472        let d = dst.as_slice();
473
474        // corners
475        assert_eq!(&d[0..3], &[4, 4, 4]);
476        assert_eq!(&d[9..12], &[3, 3, 3]);
477        assert_eq!(&d[36..39], &[2, 2, 2]);
478        assert_eq!(&d[45..48], &[1, 1, 1]);
479
480        // edges
481        assert_eq!(&d[12..15], &[2, 2, 2]);
482
483        Ok(())
484    }
485
486    #[test]
487    fn test_spatial_padding_dst_size_mismatch() -> Result<(), ImageError> {
488        let src = make_src_2x2_rgb()?;
489        let mut dst = Image::<u8, 3, _>::new(
490            ImageSize {
491                width: 3,
492                height: 4,
493            },
494            vec![0u8; 36],
495            CpuAllocator,
496        )?;
497
498        let res = spatial_padding(&src, &mut dst, PAD_1, PaddingMode::Replicate, [0, 0, 0]);
499        assert!(res.is_err());
500
501        Ok(())
502    }
503
504    #[test]
505    fn test_spatial_padding_larger_than_image_replicate() -> Result<(), ImageError> {
506        let src = Image::<u8, 3, _>::new(
507            ImageSize {
508                width: 1,
509                height: 1,
510            },
511            vec![7, 7, 7],
512            CpuAllocator,
513        )?;
514
515        let padding = Padding2D {
516            top: 3,
517            bottom: 3,
518            left: 4,
519            right: 4,
520        };
521
522        let mut dst = Image::<u8, 3, _>::new(
523            ImageSize {
524                width: 9,
525                height: 7,
526            },
527            vec![0u8; 189],
528            CpuAllocator,
529        )?;
530
531        spatial_padding(&src, &mut dst, padding, PaddingMode::Replicate, [0, 0, 0])?;
532
533        for px in dst.as_slice().chunks_exact(3) {
534            assert_eq!(px, &[7, 7, 7]);
535        }
536
537        Ok(())
538    }
539
540    #[test]
541    fn test_spatial_padding_larger_than_image_wrap() -> Result<(), ImageError> {
542        let src = Image::<u8, 3, _>::new(
543            ImageSize {
544                width: 1,
545                height: 1,
546            },
547            vec![5, 5, 5],
548            CpuAllocator,
549        )?;
550
551        let padding = Padding2D {
552            top: 2,
553            bottom: 2,
554            left: 2,
555            right: 2,
556        };
557
558        let mut dst = Image::<u8, 3, _>::new(
559            ImageSize {
560                width: 5,
561                height: 5,
562            },
563            vec![0u8; 75],
564            CpuAllocator,
565        )?;
566
567        spatial_padding(&src, &mut dst, padding, PaddingMode::Wrap, [0, 0, 0])?;
568
569        for px in dst.as_slice().chunks_exact(3) {
570            assert_eq!(px, &[5, 5, 5]);
571        }
572
573        Ok(())
574    }
575}