Coverage Report

Created: 2026-10-17 08:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/imageops/fast_blur.rs
Line
Count
Source
1
use std::ops::{Add, AddAssign, Sub, SubAssign};
2
3
use num_traits::Bounded;
4
5
use crate::imageops::filter_1d::{SafeAdd, SafeMul};
6
use crate::primitive_sealed::WithBlurAcc;
7
use crate::{ImageBuffer, Pixel, Primitive};
8
9
/// Accumulator type for box blur of subpixel type `T`.
10
pub(crate) trait BlurAccumulator<T>:
11
    Copy + Sized + Add<Output = Self> + AddAssign + Sub<Output = Self> + SubAssign
12
{
13
    type Weight: Copy;
14
15
    const ZERO: Self;
16
17
    /// Largest kernel size for which this accumulator is exact.
18
    const MAX_KERNEL_SIZE: usize;
19
20
    /// Lift a single source sample into the accumulator type.
21
    fn from_primitive(value: T) -> Self;
22
    /// Multiply an accumulator by an integer count (used for edge replication).
23
    fn scale(self, count: usize) -> Self;
24
    /// Precompute the per-kernel-size weight applied at every store.
25
    fn create_weight(kernel_size: usize) -> Self::Weight;
26
    /// Apply the weight and convert back to the source primitive.
27
    fn to_store(self, weight: Self::Weight) -> T;
28
}
29
30
/// Precomputed reciprocal for fast integer division of u32 accumulators.
31
///
32
/// Replaces `(acc + ks/2) / ks` with a multiply-shift using the identity:
33
///   `floor(n / d) == floor(n * ceil(2^32 / d) / 2^32)`
34
/// which is exact for all `n` where `(d-1)*n < d * 2^32` (Granlund-Montgomery '94).
35
/// For blur accumulators (`n <= kernel_size * 255`), this holds for all practical sizes.
36
#[derive(Clone, Copy)]
37
pub(crate) struct U8Weight {
38
    reciprocal: u32,    // ceil(2^32 / kernel_size)
39
    rounding_bias: u32, // kernel_size / 2
40
}
41
42
impl BlurAccumulator<u8> for u32 {
43
    type Weight = U8Weight;
44
    const ZERO: u32 = 0;
45
    // u32 stays exact while sum_max = kernel_size * u8::MAX fits in u32.
46
    // 2^24 * 255 < 2^32 - 2^24 (also leaves headroom for the rounding bias add).
47
    const MAX_KERNEL_SIZE: usize = 1 << 24;
48
    #[inline(always)]
49
0
    fn from_primitive(value: u8) -> u32 {
50
0
        value as u32
51
0
    }
52
    #[inline(always)]
53
0
    fn scale(self, count: usize) -> u32 {
54
0
        self * count as u32
55
0
    }
56
    #[inline(always)]
57
0
    fn create_weight(kernel_size: usize) -> U8Weight {
58
0
        let ks = kernel_size as u32;
59
0
        debug_assert!(ks >= 2);
60
0
        U8Weight {
61
0
            // ceil(2^32 / ks) = floor((2^32 - 1) / ks) + 1 = (u32::MAX / ks) + 1
62
0
            reciprocal: (u32::MAX / ks) + 1,
63
0
            rounding_bias: ks / 2,
64
0
        }
65
0
    }
66
    #[inline(always)]
67
0
    fn to_store(self, weight: U8Weight) -> u8 {
68
0
        let n = self + weight.rounding_bias;
69
0
        ((n as u64 * weight.reciprocal as u64) >> 32) as u8
70
0
    }
71
}
72
73
impl<T: Primitive> BlurAccumulator<T> for f32 {
74
    type Weight = f32;
75
    const ZERO: f32 = 0.0;
76
    // No hard overflow
77
    const MAX_KERNEL_SIZE: usize = usize::MAX;
78
    #[inline(always)]
79
0
    fn from_primitive(value: T) -> f32 {
80
0
        value.to_f32().unwrap()
81
0
    }
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<f32>>::from_primitive
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u8>>::from_primitive
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u16>>::from_primitive
82
    #[inline(always)]
83
0
    fn scale(self, count: usize) -> f32 {
84
0
        self * count as f32
85
0
    }
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<f32>>::scale
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u8>>::scale
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u16>>::scale
86
    #[inline(always)]
87
0
    fn create_weight(kernel_size: usize) -> f32 {
88
0
        1.0 / kernel_size as f32
89
0
    }
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<f32>>::create_weight
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u8>>::create_weight
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u16>>::create_weight
90
    #[inline(always)]
91
0
    fn to_store(self, weight: f32) -> T {
92
0
        T::clamp_nearest_from(self * weight)
93
0
    }
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<f32>>::to_store
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u8>>::to_store
Unexecuted instantiation: <f32 as image::imageops::fast_blur::BlurAccumulator<u16>>::to_store
94
}
95
96
/// Approximation of Gaussian blur.
97
///
98
/// # Arguments
99
///
100
/// * `image_buffer` - source image.
101
/// * `sigma` - value controls image flattening level.
102
///
103
/// This method assumes alpha pre-multiplication for images that contain non-constant alpha.
104
///
105
/// This method typically assumes that the input is scene-linear light.
106
/// If it is not, color distortion may occur.
107
///
108
/// Source: Kovesi, P.:  Fast Almost-Gaussian Filtering The Australian Pattern
109
/// Recognition Society Conference: DICTA 2010. December 2010. Sydney.
110
#[must_use]
111
0
pub fn fast_blur<P: Pixel>(
112
0
    input_buffer: &ImageBuffer<P, Vec<P::Subpixel>>,
113
0
    sigma: f32,
114
0
) -> ImageBuffer<P, Vec<P::Subpixel>> {
115
0
    let (width, height) = input_buffer.dimensions();
116
117
0
    if width == 0 || height == 0 {
118
0
        return input_buffer.clone();
119
0
    }
120
121
0
    let num_passes = 3;
122
123
0
    let boxes = boxes_for_gauss(sigma, num_passes);
124
0
    if boxes.is_empty() {
125
0
        return input_buffer.clone();
126
0
    }
127
128
0
    let samples = input_buffer.subpixels();
129
130
0
    let destination_size = match (width as usize)
131
0
        .safe_mul(height as usize)
132
0
        .and_then(|x| x.safe_mul(P::CHANNEL_COUNT as usize))
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<f32>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<u16>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<f32>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<u16>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<f32>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<u16>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<f32>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<u16>>::{closure#0}
133
    {
134
0
        Ok(s) => s,
135
0
        Err(_) => panic!("Width and height and channels count exceeded pointer size"),
136
    };
137
138
0
    let first_box = boxes[0];
139
140
0
    let mut transient = vec![P::Subpixel::min_value(); destination_size];
141
0
    let mut dst = vec![P::Subpixel::min_value(); destination_size];
142
143
    // If destination_size isn't failed this one must not fail either
144
0
    let stride = width as usize * P::CHANNEL_COUNT as usize;
145
146
    // bound + radius + 1 must fit in a pointer size
147
0
    test_radius_size(width as usize, first_box);
148
0
    test_radius_size(height as usize, first_box);
149
150
0
    box_blur_horizontal_pass_strategy::<P>(
151
0
        samples,
152
0
        stride,
153
0
        &mut transient,
154
0
        stride,
155
0
        width,
156
0
        first_box,
157
    );
158
159
0
    box_blur_vertical_pass_strategy::<P>(
160
0
        &transient, stride, &mut dst, stride, width, height, first_box,
161
    );
162
163
0
    for &box_container in boxes.iter().skip(1) {
164
0
        // bound + radius + 1 must fit in a pointer size
165
0
        test_radius_size(width as usize, box_container);
166
0
        test_radius_size(height as usize, box_container);
167
0
168
0
        box_blur_horizontal_pass_strategy::<P>(
169
0
            &dst,
170
0
            stride,
171
0
            &mut transient,
172
0
            stride,
173
0
            width,
174
0
            box_container,
175
0
        );
176
0
177
0
        box_blur_vertical_pass_strategy::<P>(
178
0
            &transient,
179
0
            stride,
180
0
            &mut dst,
181
0
            stride,
182
0
            width,
183
0
            height,
184
0
            box_container,
185
0
        );
186
0
    }
187
188
0
    let mut buffer = ImageBuffer::from_raw(width, height, dst).unwrap();
189
0
    buffer.copy_color_space_from(input_buffer);
190
0
    buffer
191
0
}
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<f32>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<u8>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgb<u16>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<f32>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<u8>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Luma<u16>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<f32>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<u8>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::Rgba<u16>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<f32>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<u8>>
Unexecuted instantiation: image::imageops::fast_blur::fast_blur::<image::color::LumaA<u16>>
192
193
#[inline]
194
0
fn test_radius_size(bound: usize, radius: usize) {
195
0
    match bound.safe_add(radius) {
196
0
        Ok(_) => {}
197
0
        Err(_) => panic!("Radius overflowed maximum possible size"),
198
    }
199
0
}
200
201
0
fn boxes_for_gauss(sigma: f32, n: usize) -> Vec<usize> {
202
0
    let w_ideal = f32::sqrt((12.0 * sigma.powi(2) / (n as f32)) + 1.0);
203
0
    let mut w_l = w_ideal.floor();
204
0
    if w_l % 2.0 == 0.0 {
205
0
        w_l -= 1.0;
206
0
    }
207
0
    let w_u = w_l + 2.0;
208
209
0
    let m_ideal = 0.25 * (n as f32) * (w_l + 3.0) - 3.0 * sigma.powi(2) * (w_l + 1.0).recip();
210
211
0
    let m = f32::round(m_ideal) as usize;
212
213
0
    (0..n)
214
0
        .map(|i| if i < m { w_l as usize } else { w_u as usize })
215
0
        .map(|i| ceil_to_odd(i.saturating_sub(1) / 2))
216
0
        .collect::<Vec<_>>()
217
0
}
218
219
#[inline]
220
0
fn ceil_to_odd(x: usize) -> usize {
221
0
    if x.is_multiple_of(2) {
222
0
        x + 1
223
    } else {
224
0
        x
225
    }
226
0
}
227
228
0
fn box_blur_horizontal_pass_strategy<T: Pixel>(
229
0
    src: &[T::Subpixel],
230
0
    src_stride: usize,
231
0
    dst: &mut [T::Subpixel],
232
0
    dst_stride: usize,
233
0
    width: u32,
234
0
    radius: usize,
235
0
) {
236
    type Acc<P> = <P as WithBlurAcc>::BlurAcc;
237
0
    let kernel_size = radius * 2 + 1;
238
0
    if kernel_size <= Acc::<T::Subpixel>::MAX_KERNEL_SIZE {
239
0
        box_blur_horizontal_pass_dispatch_cn::<T, Acc<T::Subpixel>>(
240
0
            src, src_stride, dst, dst_stride, width, radius,
241
0
        );
242
0
    } else {
243
0
        // fall back to f32, which has no overflow ceiling
244
0
        box_blur_horizontal_pass_dispatch_cn::<T, f32>(
245
0
            src, src_stride, dst, dst_stride, width, radius,
246
0
        );
247
0
    }
248
0
}
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgb<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgb<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgb<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Luma<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Luma<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Luma<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgba<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgba<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::Rgba<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::LumaA<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::LumaA<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_strategy::<image::color::LumaA<u16>>
249
250
0
fn box_blur_horizontal_pass_dispatch_cn<T: Pixel, A: BlurAccumulator<T::Subpixel>>(
251
0
    src: &[T::Subpixel],
252
0
    src_stride: usize,
253
0
    dst: &mut [T::Subpixel],
254
0
    dst_stride: usize,
255
0
    width: u32,
256
0
    radius: usize,
257
0
) {
258
0
    if T::CHANNEL_COUNT == 1 {
259
0
        box_blur_horizontal_pass::<T::Subpixel, A, 1>(
260
0
            src, src_stride, dst, dst_stride, width, radius,
261
0
        );
262
0
    } else if T::CHANNEL_COUNT == 2 {
263
0
        box_blur_horizontal_pass::<T::Subpixel, A, 2>(
264
0
            src, src_stride, dst, dst_stride, width, radius,
265
0
        );
266
0
    } else if T::CHANNEL_COUNT == 3 {
267
0
        box_blur_horizontal_pass::<T::Subpixel, A, 3>(
268
0
            src, src_stride, dst, dst_stride, width, radius,
269
0
        );
270
0
    } else if T::CHANNEL_COUNT == 4 {
271
0
        box_blur_horizontal_pass::<T::Subpixel, A, 4>(
272
0
            src, src_stride, dst, dst_stride, width, radius,
273
0
        );
274
0
    } else {
275
0
        unimplemented!("More than 4 channels is not yet implemented");
276
    }
277
0
}
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgb<f32>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgb<u8>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgb<u8>, u32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgb<u16>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Luma<f32>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Luma<u8>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Luma<u8>, u32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Luma<u16>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgba<f32>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgba<u8>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgba<u8>, u32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::Rgba<u16>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::LumaA<f32>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::LumaA<u8>, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::LumaA<u8>, u32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass_dispatch_cn::<image::color::LumaA<u16>, f32>
278
279
0
fn box_blur_vertical_pass_strategy<T: Pixel>(
280
0
    src: &[T::Subpixel],
281
0
    src_stride: usize,
282
0
    dst: &mut [T::Subpixel],
283
0
    dst_stride: usize,
284
0
    width: u32,
285
0
    height: u32,
286
0
    radius: usize,
287
0
) {
288
    type Acc<P> = <P as WithBlurAcc>::BlurAcc;
289
0
    let kernel_size = radius * 2 + 1;
290
0
    let n = T::CHANNEL_COUNT as usize;
291
0
    if kernel_size <= Acc::<T::Subpixel>::MAX_KERNEL_SIZE {
292
0
        box_blur_vertical_pass::<T::Subpixel, Acc<T::Subpixel>>(
293
0
            src, src_stride, dst, dst_stride, width, height, radius, n,
294
0
        );
295
0
    } else {
296
0
        // fall back to f32, which has no overflow ceiling
297
0
        box_blur_vertical_pass::<T::Subpixel, f32>(
298
0
            src, src_stride, dst, dst_stride, width, height, radius, n,
299
0
        );
300
0
    }
301
0
}
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgb<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgb<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgb<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Luma<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Luma<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Luma<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgba<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgba<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::Rgba<u16>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::LumaA<f32>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::LumaA<u8>>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass_strategy::<image::color::LumaA<u16>>
302
303
0
fn box_blur_horizontal_pass<P: Primitive, A: BlurAccumulator<P>, const CN: usize>(
304
0
    src: &[P],
305
0
    src_stride: usize,
306
0
    dst: &mut [P],
307
0
    dst_stride: usize,
308
0
    width: u32,
309
0
    radius: usize,
310
0
) {
311
0
    assert!(width > 0, "Width must be sanitized before this method");
312
0
    test_radius_size(width as usize, radius);
313
314
0
    let kernel_size = radius * 2 + 1;
315
0
    let edge_count = (kernel_size / 2) + 1;
316
0
    let half_kernel = kernel_size / 2;
317
0
    let width_bound = width as usize - 1;
318
319
0
    let weight = A::create_weight(kernel_size);
320
321
    // Horizontal blurring consists from 4 phases
322
    // 1 - Fill initial sliding window
323
    // 2 - Blur dangerous leading zone where clamping is required
324
    // 3 - Blur *normal* zone where clamping is not required
325
    // 4 - Blur dangerous trailing zone where clamping is required
326
327
0
    for (dst, src) in dst
328
0
        .chunks_exact_mut(dst_stride)
329
0
        .zip(src.chunks_exact(src_stride))
330
    {
331
0
        let mut sums = [A::ZERO; CN];
332
333
0
        let chunk0 = &src[..CN];
334
335
        // replicate edge
336
0
        for c in 0..CN {
337
0
            sums[c] = A::from_primitive(chunk0[c]).scale(edge_count);
338
0
        }
339
340
0
        for x in 1..=half_kernel {
341
0
            let px = x.min(width_bound) * CN;
342
0
            let chunk = &src[px..px + CN];
343
0
            for c in 0..CN {
344
0
                sums[c] += A::from_primitive(chunk[c]);
345
0
            }
346
        }
347
348
0
        for x in 0..half_kernel.min(width as usize) {
349
0
            let next = (x + half_kernel + 1).min(width_bound) * CN;
350
0
            let previous = (x as i64 - half_kernel as i64).max(0) as usize * CN;
351
352
0
            let dst_chunk = &mut dst[x * CN..x * CN + CN];
353
0
            for c in 0..CN {
354
0
                dst_chunk[c] = sums[c].to_store(weight);
355
0
            }
356
357
0
            let next_chunk = &src[next..next + CN];
358
0
            let previous_chunk = &src[previous..previous + CN];
359
0
            for c in 0..CN {
360
0
                sums[c] += A::from_primitive(next_chunk[c]);
361
0
                sums[c] -= A::from_primitive(previous_chunk[c]);
362
0
            }
363
        }
364
365
0
        let max_x_before_clamping = width_bound.saturating_sub(half_kernel + 1);
366
0
        let row_length = src.len();
367
368
0
        let mut last_processed_item = half_kernel;
369
370
0
        if ((half_kernel * 2 + 1) * CN < row_length) && ((max_x_before_clamping * CN) < row_length)
371
        {
372
0
            let data_section = src;
373
0
            let advanced_kernel_part = &data_section[(half_kernel * 2 + 1) * CN..];
374
0
            let section_length = max_x_before_clamping - half_kernel;
375
0
            let dst = &mut dst[half_kernel * CN..(half_kernel * CN + section_length * CN)];
376
377
0
            let dst_chunks = dst.as_chunks_mut::<CN>().0.iter_mut();
378
0
            let data_section_chunks = data_section.as_chunks::<CN>().0.iter();
379
0
            let advanced_kernel_part_chunks = advanced_kernel_part.as_chunks::<CN>().0.iter();
380
0
            for ((dst_chunk, src_previous), src_next) in dst_chunks
381
0
                .zip(data_section_chunks)
382
0
                .zip(advanced_kernel_part_chunks)
383
            {
384
0
                for c in 0..CN {
385
0
                    dst_chunk[c] = sums[c].to_store(weight);
386
0
                }
387
0
                for c in 0..CN {
388
0
                    sums[c] += A::from_primitive(src_next[c]);
389
0
                    sums[c] -= A::from_primitive(src_previous[c]);
390
0
                }
391
            }
392
393
0
            last_processed_item = max_x_before_clamping;
394
0
        }
395
396
0
        for x in last_processed_item..width as usize {
397
0
            let next = (x + half_kernel + 1).min(width_bound) * CN;
398
0
            let previous = (x as i64 - half_kernel as i64).max(0) as usize * CN;
399
400
0
            let dst_chunk = &mut dst[x * CN..x * CN + CN];
401
0
            for c in 0..CN {
402
0
                dst_chunk[c] = sums[c].to_store(weight);
403
0
            }
404
405
0
            let next_chunk = &src[next..next + CN];
406
0
            let previous_chunk = &src[previous..previous + CN];
407
0
            for c in 0..CN {
408
0
                sums[c] += A::from_primitive(next_chunk[c]);
409
0
                sums[c] -= A::from_primitive(previous_chunk[c]);
410
0
            }
411
        }
412
    }
413
0
}
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<f32, f32, 1>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<f32, f32, 2>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<f32, f32, 3>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<f32, f32, 4>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, f32, 1>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, f32, 2>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, f32, 3>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, f32, 4>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, u32, 1>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, u32, 2>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, u32, 3>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u8, u32, 4>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u16, f32, 1>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u16, f32, 2>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u16, f32, 3>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_horizontal_pass::<u16, f32, 4>
414
415
#[allow(clippy::too_many_arguments)]
416
0
fn box_blur_vertical_pass<P: Primitive, A: BlurAccumulator<P>>(
417
0
    src: &[P],
418
0
    src_stride: usize,
419
0
    dst: &mut [P],
420
0
    dst_stride: usize,
421
0
    width: u32,
422
0
    height: u32,
423
0
    radius: usize,
424
0
    n: usize,
425
0
) {
426
0
    assert!(width > 0, "Width must be sanitized before this method");
427
0
    assert!(height > 0, "Height must be sanitized before this method");
428
0
    test_radius_size(width as usize, radius);
429
430
0
    let kernel_size = radius * 2 + 1;
431
0
    let edge_count = (kernel_size / 2) + 1;
432
0
    let half_kernel = kernel_size / 2;
433
0
    let height_bound = height as usize - 1;
434
435
0
    let weight = A::create_weight(kernel_size);
436
0
    let buf_size = width as usize * n;
437
438
    // Instead of summing each column separately we use here transient buffer that
439
    // averages columns in row manner.
440
    // So, we make the initial buffer at the top edge
441
    // and then doing blur by averaging the whole row ( which is in buffer )
442
    // and subtracting and adding next and previous rows in horizontal manner.
443
444
0
    let mut buffer = vec![A::ZERO; buf_size];
445
446
0
    for (x, bf) in buffer.iter_mut().enumerate() {
447
0
        let mut w = A::from_primitive(src[x]).scale(edge_count);
448
0
        for y in 1..=half_kernel {
449
0
            let y_src_shift = y.min(height_bound) * src_stride;
450
0
            w += A::from_primitive(src[y_src_shift + x]);
451
0
        }
452
0
        *bf = w;
453
    }
454
455
0
    for (dst_row, y) in dst.chunks_exact_mut(dst_stride).zip(0..height as usize) {
456
0
        let next = (y + half_kernel + 1).min(height_bound) * src_stride;
457
0
        let previous = (y as i64 - half_kernel as i64).max(0) as usize * src_stride;
458
459
0
        let next_row = &src[next..next + buf_size];
460
0
        let previous_row = &src[previous..previous + buf_size];
461
462
0
        for (((src_next, src_previous), buf), dst) in next_row
463
0
            .iter()
464
0
            .zip(previous_row.iter())
465
0
            .zip(buffer.iter_mut())
466
0
            .zip(dst_row.iter_mut())
467
0
        {
468
0
            let acc = *buf;
469
0
            *dst = acc.to_store(weight);
470
0
            *buf = acc + A::from_primitive(*src_next) - A::from_primitive(*src_previous);
471
0
        }
472
    }
473
0
}
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass::<f32, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass::<u8, f32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass::<u8, u32>
Unexecuted instantiation: image::imageops::fast_blur::box_blur_vertical_pass::<u16, f32>
474
475
#[cfg(test)]
476
mod tests {
477
    use super::BlurAccumulator;
478
    use crate::{DynamicImage, GrayAlphaImage, GrayImage, RgbImage, RgbaImage};
479
480
    #[test]
481
    fn u8_weight_exhaustive_small() {
482
        for ks in (3u32..=51).step_by(2) {
483
            let w = <u32 as BlurAccumulator<u8>>::create_weight(ks as usize);
484
            let max_acc = ks * 255;
485
            for acc in 0..=max_acc {
486
                let expected = ((acc + ks / 2) / ks) as u8;
487
                let got = acc.to_store(w);
488
                assert_eq!(got, expected, "ks={ks}, acc={acc}");
489
            }
490
        }
491
    }
492
493
    #[test]
494
    fn u8_weight_sampled_large() {
495
        for ks in (53u32..=1025).step_by(2) {
496
            let w = <u32 as BlurAccumulator<u8>>::create_weight(ks as usize);
497
            let max_acc = ks * 255;
498
            let step = (max_acc / 10_000).max(1) as usize;
499
            for acc in (0..=max_acc).step_by(step) {
500
                let expected = ((acc + ks / 2) / ks) as u8;
501
                let got = acc.to_store(w);
502
                assert_eq!(got, expected, "ks={ks}, acc={acc}");
503
            }
504
            assert_eq!(0u32.to_store(w), ((ks / 2) / ks) as u8);
505
            assert_eq!(max_acc.to_store(w), ((max_acc + ks / 2) / ks) as u8);
506
        }
507
    }
508
509
    use std::time::{SystemTime, UNIX_EPOCH};
510
511
    struct Rng {
512
        state: u64,
513
    }
514
515
    impl Rng {
516
        fn new(seed: u64) -> Self {
517
            Self { state: seed }
518
        }
519
        fn next_u32(&mut self) -> u32 {
520
            self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
521
            (self.state >> 32) as u32
522
        }
523
524
        fn next_u8(&mut self) -> u8 {
525
            (self.next_u32() % 256) as u8
526
        }
527
528
        fn next_f32_in_range(&mut self, a: f32, b: f32) -> f32 {
529
            let u = self.next_u32();
530
            let unit = (u as f32) / (u32::MAX as f32 + 1.0);
531
            a + (b - a) * unit
532
        }
533
    }
534
535
    #[test]
536
    fn test_box_blur() {
537
        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
538
        let mut rng = Rng::new((now.as_millis() & 0xffff_ffff_ffff_ffff) as u64);
539
        for _ in 0..35 {
540
            let width = rng.next_u8();
541
            let height = rng.next_u8();
542
            let sigma = rng.next_f32_in_range(0., 100.);
543
            let px = rng.next_u8();
544
            let cn = rng.next_u8();
545
            if width == 0 || height == 0 || sigma <= 0. {
546
                continue;
547
            }
548
            match cn % 4 {
549
                0 => {
550
                    let vc = vec![px; width as usize * height as usize];
551
                    let image = DynamicImage::from(
552
                        GrayImage::from_vec(u32::from(width), u32::from(height), vc).unwrap(),
553
                    );
554
                    let res = image.fast_blur(sigma);
555
                    for clr in res.as_bytes() {
556
                        assert_eq!(*clr, px);
557
                    }
558
                }
559
                1 => {
560
                    let vc = vec![px; width as usize * height as usize * 2];
561
                    let image = DynamicImage::from(
562
                        GrayAlphaImage::from_vec(u32::from(width), u32::from(height), vc).unwrap(),
563
                    );
564
                    let res = image.fast_blur(sigma);
565
                    for clr in res.as_bytes() {
566
                        assert_eq!(*clr, px);
567
                    }
568
                }
569
                2 => {
570
                    let vc = vec![px; width as usize * height as usize * 3];
571
                    let image = DynamicImage::from(
572
                        RgbImage::from_vec(u32::from(width), u32::from(height), vc).unwrap(),
573
                    );
574
                    let res = image.fast_blur(sigma);
575
                    for clr in res.as_bytes() {
576
                        assert_eq!(*clr, px);
577
                    }
578
                }
579
                3 => {
580
                    let vc = vec![px; width as usize * height as usize * 4];
581
                    let image = DynamicImage::from(
582
                        RgbaImage::from_vec(u32::from(width), u32::from(height), vc).unwrap(),
583
                    );
584
                    let res = image.fast_blur(sigma);
585
                    for clr in res.as_bytes() {
586
                        assert_eq!(*clr, px);
587
                    }
588
                }
589
                _ => {}
590
            }
591
        }
592
    }
593
}