Convert blur, median_filter, color_overlay, and image_color_palette to record kernels

This commit is contained in:
Dennis Kobert
2026-08-23 17:10:00 +00:00
parent 511b4da9f5
commit 9ba3c16e79
3 changed files with 57 additions and 67 deletions
@@ -1,6 +1,4 @@
use crate::adjust::Adjust; use crate::adjust::Adjust;
#[cfg(feature = "std")]
use core_types::list::List;
use no_std_types::Ctx; use no_std_types::Ctx;
use no_std_types::blending::BlendMode; use no_std_types::blending::BlendMode;
use no_std_types::color::{Color, Pixel}; use no_std_types::color::{Color, Pixel};
@@ -179,12 +177,12 @@ fn mix<T: Blend<Color> + Clone + Send + Sync + core_types::CacheHash + 'static>(
} }
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn color_overlay<T: Adjust<Color>>( fn color_overlay<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
+20 -41
View File
@@ -1,7 +1,6 @@
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use core_types::color::{Alpha, Color, Pixel, RGB}; use core_types::color::{Alpha, Color, Pixel, RGB};
use core_types::context::Ctx; use core_types::context::Ctx;
use core_types::list::List;
use core_types::registry::types::PixelLength; use core_types::registry::types::PixelLength;
use raster_types::Image; use raster_types::Image;
use raster_types::{Bitmap, BitmapMut}; use raster_types::{Bitmap, BitmapMut};
@@ -90,7 +89,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Imag
fn blur( fn blur(
_: impl Ctx, _: impl Ctx,
/// The image to be blurred. /// The image to be blurred.
image_frame: List<Raster<CPU>>, image_frame: Raster<CPU>,
/// The radius of the blur kernel. /// The radius of the blur kernel.
#[range] #[range]
#[hard(0..)] #[hard(0..)]
@@ -100,26 +99,16 @@ fn blur(
box_blur: bool, box_blur: bool,
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software. /// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
gamma: bool, gamma: bool,
) -> List<Raster<CPU>> { ) -> Raster<CPU> {
image_frame // Run blur algorithm
.into_iter() if radius < 0.1 {
.map(|mut row| { // Minimum blur radius
let image = row.element().clone(); image_frame
} else if box_blur {
// Run blur algorithm Raster::new_cpu(box_blur_algorithm(image_frame.into_data(), radius, gamma))
let blurred_image = if radius < 0.1 { } else {
// Minimum blur radius Raster::new_cpu(gaussian_blur_algorithm(image_frame.into_data(), radius, gamma))
image.clone() }
} else if box_blur {
Raster::new_cpu(box_blur_algorithm(image.into_data(), radius, gamma))
} else {
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
};
*row.element_mut() = blurred_image;
row
})
.collect()
} }
/// Applies a median filter to reduce noise while preserving edges. /// Applies a median filter to reduce noise while preserving edges.
@@ -127,30 +116,20 @@ fn blur(
fn median_filter( fn median_filter(
_: impl Ctx, _: impl Ctx,
/// The image to be filtered. /// The image to be filtered.
image_frame: List<Raster<CPU>>, image_frame: Raster<CPU>,
/// The radius of the filter kernel. Larger values remove more noise but may blur fine details. /// The radius of the filter kernel. Larger values remove more noise but may blur fine details.
#[range] #[range]
#[hard(0..)] #[hard(0..)]
#[soft(..50)] #[soft(..50)]
radius: PixelLength, radius: PixelLength,
) -> List<Raster<CPU>> { ) -> Raster<CPU> {
image_frame // Apply median filter
.into_iter() if radius < 0.5 {
.map(|mut row| { // Minimum filter radius
let image = row.element().clone(); image_frame
} else {
// Apply median filter Raster::new_cpu(median_filter_algorithm(image_frame.into_data(), radius as u32))
let filtered_image = if radius < 0.5 { }
// Minimum filter radius
image.clone()
} else {
Raster::new_cpu(median_filter_algorithm(image.into_data(), radius as u32))
};
*row.element_mut() = filtered_image;
row
})
.collect()
} }
// 1D gaussian kernel // 1D gaussian kernel
@@ -1,16 +1,16 @@
use core_types::color::Color; use core_types::color::Color;
use core_types::context::Ctx; use core_types::context::{Ctx, ExtractIndex, InjectIndex};
use core_types::list::{Item, List}; use core_types::gpoll::{GraphError, Interrupt};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
fn image_color_palette( fn image_color_palette(
_: impl Ctx, ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
image: &List<Raster<CPU>>, image: IList<Raster<CPU>>,
#[default(4)] #[default(4)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
) -> List<Color> { ) -> Result<IList<Color>, Interrupt> {
const GRID: f32 = 3.; const GRID: f32 = 3.;
let bins = GRID * GRID * GRID; let bins = GRID * GRID * GRID;
@@ -19,7 +19,8 @@ fn image_color_palette(
// Each bin stores `(red, green, blue, alpha)` tuples in sRGB gamma space; averaging in gamma space gives perceptually-uniform binning. // Each bin stores `(red, green, blue, alpha)` tuples in sRGB gamma space; averaging in gamma space gives perceptually-uniform binning.
let mut color_bins: Vec<Vec<[f32; 4]>> = vec![Vec::new(); (bins + 1.) as usize]; let mut color_bins: Vec<Vec<[f32; 4]>> = vec![Vec::new(); (bins + 1.) as usize];
for element in image.iter_element_values() { for row in 0..image.len() {
let element = image.element_ref(row);
for pixel in element.data.iter() { for pixel in element.data.iter() {
let r = pixel.r() * GRID; let r = pixel.r() * GRID;
let g = pixel.g() * GRID; let g = pixel.g() * GRID;
@@ -34,7 +35,7 @@ fn image_color_palette(
let shorted = histogram.iter().enumerate().filter(|&(_, &count)| count > 0).map(|(i, _)| i).collect::<Vec<usize>>(); let shorted = histogram.iter().enumerate().filter(|&(_, &count)| count > 0).map(|(i, _)| i).collect::<Vec<usize>>();
shorted let palette: Vec<Color> = shorted
.iter() .iter()
.take(count as usize) .take(count as usize)
.flat_map(|&i| { .flat_map(|&i| {
@@ -54,9 +55,11 @@ fn image_color_palette(
// Reject NaN/out-of-range averages, then lift the gamma-space bin centroid to linear-light // Reject NaN/out-of-range averages, then lift the gamma-space bin centroid to linear-light
let in_gamut = a <= 1. && ![r, g, b, a].iter().any(|c| c.is_sign_negative() || !c.is_finite()); let in_gamut = a <= 1. && ![r, g, b, a].iter().any(|c| c.is_sign_negative() || !c.is_finite());
in_gamut.then(|| Color::from_gamma_srgb_channels(r, g, b, a)).map(Item::new_from_element).into_iter() in_gamut.then(|| Color::from_gamma_srgb_channels(r, g, b, a)).into_iter()
}) })
.collect() .collect();
palette.get(ctx.innermost_index() as usize).copied().ok_or_else(|| GraphError::past_end().into())
} }
#[cfg(test)] #[cfg(test)]
@@ -67,16 +70,26 @@ mod test {
#[test] #[test]
fn test_image_color_palette() { fn test_image_color_palette() {
let result = image_color_palette( core_types::record::stack::reserve(1 << 16);
&(), let arena = core_types::arena::Arena::new(1 << 22).unwrap();
&List::new_from_element(Raster::new_cpu(Image { let generations = [];
width: 100, let scope = core_types::context::EvalScope::new(None, None, None, &generations, &arena);
height: 100, let ctx = core_types::context::ContextImpl::root(&scope);
data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
base64_string: None, let raster = Raster::new_cpu(Image {
})), width: 100,
1, height: 100,
); data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
assert_eq!(result, List::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap())); base64_string: None,
});
let source = core_types::value::LeveledValueSource::new(vec![raster]);
let core_types::record::LevelStatus::Batch(batch, _) = core_types::record::materialize_level(&source, &ctx, &arena) else {
panic!("materialize failed")
};
let image = unsafe { core_types::node::List::<Raster<CPU>>::new(batch) };
// The root context addresses lane 0, the palette's first color
let color = image_color_palette(&ctx, image, 1).unwrap();
assert_eq!(color, Color::from_rgbaf32(0., 0., 0., 1.).unwrap());
} }
} }