bokeh_creator/
renderer.rs

1#[cfg(feature = "noise")]
2use crate::settings::Noise;
3use crate::settings::{FilterType, Settings};
4use glam::{USizeVec2, Vec2};
5#[cfg(feature = "image")]
6use image::{ImageBuffer, Pixel};
7#[cfg(feature = "noise")]
8use libnoise::prelude::*;
9use ndarray::ArrayViewMut3;
10
11use image_ndarray::prelude::*;
12use num_traits::AsPrimitive;
13
14/// Value of RAD, as we don't have a const for that in f32::consts unfortunately
15const RAD: f32 = 57.2957;
16/// Make the ring size less extreme as its only meant to be for small values
17const RING_NORMALIZER: f32 = 0.1;
18/// Value to reduce the size of the outer blur
19const OUTER_BLUR_LIMIT: f32 = 0.4;
20/// Simple value that reduces the overall size of the kernel
21const RADIUS_NORMALIZER: f32 = 0.3;
22
23/// Object to render bokeh kernels
24///
25/// This is the disc that being perceived as the 'Bokeh'
26/// This image will be used to convolute the entire image with.
27///
28///
29/// ## Usage
30///
31/// The object has three entry points (depending on the features enabled).
32///
33/// Make sure to create the settings first, then pass them to this
34/// object when calling `Renderer::new(settings);`
35///
36/// ### Rendering
37/// When not using any features, call the `render_pixel()` method for all coordinates
38/// in your image to fetch the result for each pixel.
39///
40/// Or provide your own ndarray where each value will be computed automatically with
41/// `Renderer::render_to_array`
42///
43/// ### Example
44/// ```rust
45/// use bokeh_creator::{Renderer, Settings};
46/// use glam::USizeVec2;
47///
48/// let resolution = 64;
49/// let settings = Settings::default();
50/// let renderer = Renderer::new(settings, [resolution, resolution].into());
51/// let mut image = vec![vec![0.0; resolution]; resolution];
52///
53/// // this is not the most efficient way, its just to showcase basic image processing
54/// for (y, row) in image.iter_mut().enumerate() {
55///     for (x, pixel) in row.iter_mut().enumerate() {
56///         *pixel = renderer.render_pixel(USizeVec2::new(x, y), 0);
57///     }
58/// }
59/// ```
60pub struct Renderer {
61    /// Settings specified to use for rendering
62    settings: Settings,
63    /// Resolution of filter image
64    resolution: USizeVec2,
65    /// Center of the image in x and y
66    center: Vec2,
67    /// Degrees between each blade
68    blade_degree: f32,
69    /// Radius of kernel image in pixels
70    radius_px: f32,
71
72    #[cfg(feature = "noise")]
73    /// Generator from libnoise to create noise with
74    noise_generator: Fbm<2, Simplex<2>>,
75}
76
77impl Renderer {
78    /// Create a new instance of the renderer with the specified settings.
79    pub fn new(settings: Settings, resolution: USizeVec2) -> Self {
80        let center = resolution.as_vec2() * 0.5;
81        let blade_degree = Self::get_blade_degree(settings.blades);
82
83        let radius_px = center * settings.radius - 1.0;
84        let radius_px = radius_px - (radius_px * settings.outer_blur.abs() * RADIUS_NORMALIZER);
85        Self {
86            settings,
87            center,
88            resolution,
89            blade_degree,
90            radius_px: radius_px.max_element(),
91            #[cfg(feature = "noise")]
92            noise_generator: Self::get_noise_generator(settings.noise),
93        }
94    }
95
96    /// Get the degrees towards the center of the kernel
97    fn get_degrees(&self, position: Vec2) -> f32 {
98        let relative_position = position - self.center;
99        let radians = f32::atan2(relative_position.y, relative_position.x);
100        radians * RAD + self.settings.angle
101    }
102
103    #[cfg(feature = "noise")]
104    /// Configure the noise generator
105    ///
106    /// TODO: could be improved by specifying settings in the Settings struct to configure the type of noise.
107    fn get_noise_generator(settings: Noise) -> Fbm<2, Simplex<2>> {
108        Source::simplex(settings.seed.max(0) as u64).fbm(settings.octaves as u32, 0.013, 2.0, 0.5)
109    }
110
111    /// To get the blades added, we shift the radius a bit between the blades.
112    ///
113    /// The amount of curvature defines the amount of radius shift.
114    fn get_blade_radius_multiplier(&self, position: Vec2) -> f32 {
115        let degrees = self.get_degrees(position);
116        let mut blades_offset = ((degrees)
117            - (f32::floor(degrees / self.blade_degree) * self.blade_degree))
118            / self.blade_degree;
119        blades_offset -= 0.5;
120        blades_offset = blades_offset.abs();
121
122        let curvature = match self.settings.filter_type {
123            FilterType::DISC => 1.0,
124            _ => self.settings.curvature,
125        };
126
127        (blades_offset - (blades_offset * blades_offset)) * (1.0 - f32::min(curvature, 1.0)) * 2.0
128    }
129
130    /// Simple screen operation (add to image without brightening)
131    fn screen(a: f32, b: f32) -> f32 {
132        a + b - (a * b)
133    }
134
135    /// Calculate the value of the ring, by percentage of the radius.
136    fn get_ring_value(&self, pixel_percentage: f32) -> f32 {
137        let ring_range = RING_NORMALIZER * self.settings.ring_size;
138        let mut ring_multiplier = 1.0 - pixel_percentage;
139        ring_multiplier = if ring_multiplier < ring_range && ring_multiplier > 0.0 {
140            1.0
141        } else {
142            0.0
143        };
144        let mut inner_blur_multiplier = 0.0;
145        if self.settings.inner_blur != 0.0 && pixel_percentage < 1.0 {
146            inner_blur_multiplier = pixel_percentage / (1.0 - ring_range);
147            inner_blur_multiplier = inner_blur_multiplier.clamp(0.0, 1.0);
148            inner_blur_multiplier = (inner_blur_multiplier
149                - (1.0 - (self.settings.inner_blur * 2.0)))
150                / (1.0 - (1.0 - (self.settings.inner_blur * 2.0)));
151            inner_blur_multiplier = inner_blur_multiplier.clamp(0.0, 1.0);
152            inner_blur_multiplier = inner_blur_multiplier * inner_blur_multiplier;
153        }
154        let mut outer_blur_multiplier = 0.0;
155        if self.settings.outer_blur != 0.0 && pixel_percentage > 1.0 {
156            outer_blur_multiplier = (pixel_percentage
157                - (1.0 + (self.settings.outer_blur.abs() * OUTER_BLUR_LIMIT)))
158                / (1.0 - (1.0 + (self.settings.outer_blur.abs() * OUTER_BLUR_LIMIT)));
159            outer_blur_multiplier = outer_blur_multiplier.clamp(0.0, 1.0);
160            outer_blur_multiplier = outer_blur_multiplier * outer_blur_multiplier;
161        }
162        ring_multiplier = Self::screen(ring_multiplier, inner_blur_multiplier);
163        ring_multiplier = Self::screen(ring_multiplier, outer_blur_multiplier);
164        ring_multiplier
165    }
166
167    /// Returns 1 if its within the range of the kernel
168    fn get_inner_value(pixel_percentage: f32) -> f32 {
169        if pixel_percentage < 1.0 {
170            return 1.0;
171        }
172        0.0
173    }
174
175    /// Get degrees per blade
176    fn get_blade_degree(blades: i32) -> f32 {
177        if blades == 0 {
178            return 0.0;
179        }
180        360.0 / blades as f32
181    }
182
183    /// Calculate the value of both ring and inner color.
184    ///
185    /// TODO: implement channel support
186    fn get_bokeh_value(&self, position: Vec2, _channel: usize) -> f32 {
187        let radius_multiplier = self.get_blade_radius_multiplier(position);
188        let calculated_radius = f32::max(
189            self.radius_px - ((self.radius_px / (self.settings.blades as f32)) * radius_multiplier),
190            0.0,
191        );
192        let pixel_percentage = position.distance(self.center).abs() / calculated_radius;
193        let ring = self.get_ring_value(pixel_percentage);
194        let inner = f32::max(Self::get_inner_value(pixel_percentage) - ring, 0.0);
195        Self::screen(
196            ring * self.settings.ring_color,
197            inner * self.settings.inner_color,
198        )
199    }
200
201    /// Render a single pixel and include noise if the `noise` feature is enabled.
202    pub fn render_pixel(&self, position: USizeVec2, channel: usize) -> f32 {
203        let offset_multiplier = Vec2::new(
204            3.0 - f32::min(self.settings.aspect_ratio, 1.0) * 2.0,
205            f32::max(self.settings.aspect_ratio, 1.0) * 2.0 - 1.0,
206        );
207        let coordinate = position.as_vec2() * offset_multiplier;
208        let bokeh = self.get_bokeh_value(coordinate, channel);
209        if self.settings.noise.intensity == 0.0 || self.settings.noise.size == 0.0 {
210            return bokeh;
211        }
212
213        self.apply_noise(position, offset_multiplier, bokeh)
214    }
215
216    #[cfg(feature = "noise")]
217    /// Apply noise to the bokeh value
218    fn apply_noise(&self, position: USizeVec2, offset_multiplier: Vec2, bokeh: f32) -> f32 {
219        let frequency = 1.0 + (1.0 / (self.settings.noise.size * 0.01));
220        let noise_sample_position =
221            (position.as_vec2() - self.center) * offset_multiplier * frequency
222                / self.resolution.as_vec2();
223
224        let mut noise = self.noise_generator.sample([
225            noise_sample_position.x as f64,
226            noise_sample_position.y as f64,
227        ]) as f32;
228        noise = noise.clamp(-1.0, 1.0);
229        noise = (noise + 1.0) * 0.5;
230        noise = noise.powf(2.2);
231        noise * bokeh * self.settings.noise.intensity.clamp(0.0, 1.0)
232            + (bokeh * (1.0 - self.settings.noise.intensity.clamp(0.0, 1.0)))
233    }
234
235    #[cfg(not(feature = "noise"))]
236    /// Replacement for apply_noise if the feature is not enabled, doesn't do anything
237    fn apply_noise(&self, _: USizeVec2, _: Vec2, bokeh: f32) -> f32 {
238        bokeh
239    }
240
241    #[cfg(feature = "image")]
242    /// Render the bokeh for the provided image.
243    pub fn render_to_image<P, T>(image: &mut ImageBuffer<P, Vec<T>>, settings: Settings)
244    where
245        P: Pixel<Subpixel = T> + Sync,
246        T: Clone + Copy + NormalizedFloat<T> + AsPrimitive<f32> + AsPrimitive<f64> + Default,
247    {
248        Self::render_to_array(settings, &mut image.as_ndarray_mut().view_mut());
249    }
250
251    pub fn render_to_array<T>(settings: Settings, target: &mut ArrayViewMut3<T>)
252    where
253        T: NormalizedFloat<T> + AsPrimitive<f32> + AsPrimitive<f64> + Default,
254    {
255        let resolution = USizeVec2::new(target.dim().1, target.dim().0);
256        let renderer = Self::new(settings, resolution);
257        target
258            .indexed_iter_mut()
259            .for_each(|((y, x, channel), value)| {
260                *value =
261                    T::from_f32_normalized(renderer.render_pixel(USizeVec2::new(x, y), channel))
262                        .unwrap_or_default()
263            });
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::settings::Noise;
271    use image::{DynamicImage, Rgba32FImage};
272    use image_compare::Algorithm;
273    use rstest::rstest;
274    use std::path::PathBuf;
275
276    /// Just a utility to make it easier to save the test results
277    fn store_test_result(image: &Rgba32FImage, path: PathBuf) {
278        DynamicImage::from(image.clone())
279            .to_rgb8()
280            .save(path)
281            .unwrap();
282    }
283
284    fn get_comparison_score(a: Rgba32FImage, b: Rgba32FImage) -> f64 {
285        let a = DynamicImage::from(a.clone()).to_luma8();
286        let b = DynamicImage::from(b.clone()).to_luma8();
287
288        image_compare::gray_similarity_structure(&Algorithm::MSSIMSimple, &a, &b)
289            .unwrap()
290            .score
291    }
292
293    fn load_test_image(path: PathBuf) -> Rgba32FImage {
294        let image = image::open(path);
295        image.unwrap().to_rgba32f()
296    }
297
298    #[rstest]
299    #[case(Settings::default(), PathBuf::from("./test/images/1_expected.jpg"))]
300    #[case(
301        Settings {
302            filter_type: FilterType::BLADE,
303            angle: 195.3,
304            curvature: 0.1,
305            ..Default::default()
306        },
307        PathBuf::from("./test/images/2_expected.jpg")
308    )]
309    #[case(
310        Settings {
311            filter_type: FilterType::BLADE,
312            angle: 90.0,
313            blades: 15,
314            ..Default::default()
315        },
316        PathBuf::from("./test/images/3_expected.jpg")
317    )]
318    #[case(
319        Settings {
320            aspect_ratio: 0.5,
321            ..Default::default()
322        },
323        PathBuf::from("./test/images/4_expected.jpg")
324    )]
325    #[case(
326        Settings {
327            aspect_ratio: 2.0,
328            ..Default::default()
329        },
330        PathBuf::from("./test/images/5_expected.jpg")
331    )]
332    #[case(
333        Settings {
334            ring_color: 0.5,
335            inner_color: 0.9,
336            ring_size: 0.5,
337            ..Default::default()
338        },
339        PathBuf::from("./test/images/6_expected.jpg")
340    )]
341    #[case(
342        Settings {
343            noise: {
344                Noise {
345                    size: 0.3,
346                    intensity: 1.0,
347                    ..Default::default()
348                }
349            },
350            ..Default::default()
351        },
352        PathBuf::from("./test/images/7_expected.jpg")
353    )]
354    #[case(
355        Settings {
356            noise: {
357                Noise {
358                    intensity: 0.0,
359                    ..Default::default()
360                }
361            },
362            ..Default::default()
363        },
364        PathBuf::from("./test/images/8_expected.jpg")
365    )]
366    #[case(
367        Settings {
368            noise: {
369                Noise {
370                    seed: 30,
371                    ..Default::default()
372                }
373            },
374            ..Default::default()
375        },
376        PathBuf::from("./test/images/9_expected.jpg")
377    )]
378
379    /// Test result of kernel rendering
380    fn test_kernel(#[case] settings: Settings, #[case] expected: PathBuf) {
381        let expected_image = match expected.exists() {
382            true => load_test_image(expected.clone()),
383            false => Rgba32FImage::new(256, 256),
384        };
385
386        let mut result = Rgba32FImage::new(256, 256);
387        Renderer::render_to_image(&mut result, settings);
388
389        if !(expected.clone().exists()) {
390            store_test_result(&result, expected);
391        }
392
393        let score = get_comparison_score(expected_image, result);
394        println!("Test got score: {}", score);
395
396        assert!(score > 0.9); // Because of compression with jpegs :)
397    }
398}