/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 | | } |