Add Levels adjustment node (#1028)

* Add Levels Node

* Fix naming and algorithm

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
isiko
2023-02-16 01:11:31 +01:00
committed by Keavon Chambers
parent 5dab7de68d
commit 7071aabba8
4 changed files with 127 additions and 20 deletions

View File

@@ -51,6 +51,47 @@ fn luminance_color_node(color: Color, luma_calculation: LuminanceCalculation) ->
color.map_rgb(|_| luminance)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LevelsNode<InputStart, InputMid, InputEnd, OutputStart, OutputEnd> {
input_start: InputStart,
input_mid: InputMid,
input_end: InputEnd,
output_start: OutputStart,
output_end: OutputEnd,
}
// From https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels
#[node_macro::node_fn(LevelsNode)]
fn levels_node(color: Color, input_start: f64, input_mid: f64, input_end: f64, output_start: f64, output_end: f64) -> Color {
// Input Range
let input_shadows = (input_start / 100.) as f32;
let input_midtones = (input_mid / 100.) as f32;
let input_highlights = (input_end / 100.) as f32;
// Output Range
let output_minimums = (output_start / 100.) as f32;
let output_maximums = (output_end / 100.) as f32;
// Midtones interpolation factor between minimums and maximums
let midtones = output_minimums + (output_maximums - output_minimums) * input_midtones;
// Gamma correction
let gamma = if midtones < 0.5 {
(1. + (9. * (1. - midtones * 2.))).min(9.99)
} else {
((1. - midtones) * 2.).max(0.01)
};
// Input levels
let color = color.map_rgb(|channel| (channel - input_shadows) / (input_highlights - input_shadows));
// Midtones
let color = color.map_rgb(|channel| channel.powf(gamma));
// Output levels
color.map_rgb(|channel| channel * (output_maximums - output_minimums) + output_minimums)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GrayscaleNode<Tint, Reds, Yellows, Greens, Cyans, Blues, Magentas> {
tint: Tint,

View File

@@ -30,19 +30,26 @@ macro_rules! register_node {
|args| {
let mut args = args.clone();
args.reverse();
let node = <$path>::new($(graphene_std::any::input_node::<$type>(args.pop().expect("not enough arguments provided to construct node"))),*);
let node = <$path>::new($(
graphene_std::any::input_node::<$type>(
args.pop().expect("not enough arguments provided to construct node")
)
),*);
let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));
Box::pin(any)
},{
},
{
let node = IdNode::new().into_type_erased();
let node = NodeContainer::new(node, vec![]);
let _node = unsafe { node.erase_lifetime().static_ref() };
let node = <$path>::new($(graphene_std::any::input_node::<$type>(_node)),*);
let node = <$path>::new($(
graphene_std::any::input_node::<$type>(_node)
),*);
let params = vec![$(concrete!($type)),*];
let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params);
let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params);
node_io.input = concrete!(<$input as StaticType>::Static);
node_io
}
},
)
};
}
@@ -50,21 +57,24 @@ macro_rules! raster_node {
($path:ty, params: [$($type:ty),*]) => {
(
NodeIdentifier::new(stringify!($path)),
|args| {
let mut args = args.clone();
args.reverse();
let node = <$path>::new($(
graphene_core::value::ClonedNode::new(
graphene_std::any::input_node::<$type>(args.pop().expect("Not enough arguments provided to construct node"))
.eval(()))
),*);
let map_node = graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node));
let any: DynAnyNode<Image, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node));
Box::pin(any)
}, {
let params = vec![$(concrete!($type)),*];
NodeIOTypes::new(concrete!(Image), concrete!(Image), params)
}
|args| {
let mut args = args.clone();
args.reverse();
let node = <$path>::new($(
graphene_core::value::ClonedNode::new(
graphene_std::any::input_node::<$type>(
args.pop().expect("Not enough arguments provided to construct node")
).eval(())
)
),*);
let map_node = graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node));
let any: DynAnyNode<Image, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node));
Box::pin(any)
},
{
let params = vec![$(concrete!($type)),*];
NodeIOTypes::new(concrete!(Image), concrete!(Image), params)
},
)
};
}
@@ -103,6 +113,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
),
// Filters
raster_node!(graphene_core::raster::LuminanceNode<_>, params: [LuminanceCalculation]),
raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f64, f64, f64, f64, f64]),
raster_node!(graphene_core::raster::GrayscaleNode<_, _, _, _, _, _, _>, params: [Color, f64, f64, f64, f64, f64, f64]),
raster_node!(graphene_core::raster::HueSaturationNode<_, _, _>, params: [f64, f64, f64]),
raster_node!(graphene_core::raster::InvertRGBNode, params: []),