From 7071aabba8cf1739d67229cd659cb88f45c277f5 Mon Sep 17 00:00:00 2001 From: isiko Date: Thu, 16 Feb 2023 01:11:31 +0100 Subject: [PATCH] Add Levels adjustment node (#1028) * Add Levels Node * Fix naming and algorithm --------- Co-authored-by: Keavon Chambers --- .../document_node_types.rs | 39 ++++++++++++++ .../node_properties.rs | 16 ++++++ node-graph/gcore/src/raster/adjustments.rs | 41 +++++++++++++++ .../interpreted-executor/src/node_registry.rs | 51 +++++++++++-------- 4 files changed, 127 insertions(+), 20 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index f68f5a394..56b4d720f 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -192,6 +192,45 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], properties: |_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"), }, + DocumentNodeType { + name: "Levels", + category: "Image Adjustments", + identifier: NodeImplementation::proto("graphene_core::raster::LevelsNode<_, _, _, _, _>"), + inputs: vec![ + DocumentInputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::value(TaggedValue::Image(Image::empty()), true), + }, + DocumentInputType { + name: "Shadows", + data_type: FrontendGraphDataType::Number, + default: NodeInput::value(TaggedValue::F64(0.), false), + }, + DocumentInputType { + name: "Midtones", + data_type: FrontendGraphDataType::Number, + default: NodeInput::value(TaggedValue::F64(50.), false), + }, + DocumentInputType { + name: "Highlights", + data_type: FrontendGraphDataType::Number, + default: NodeInput::value(TaggedValue::F64(100.), false), + }, + DocumentInputType { + name: "Output Minimums", + data_type: FrontendGraphDataType::Number, + default: NodeInput::value(TaggedValue::F64(0.), false), + }, + DocumentInputType { + name: "Output Maximums", + data_type: FrontendGraphDataType::Number, + default: NodeInput::value(TaggedValue::F64(100.), false), + }, + ], + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::levels_properties, + }, DocumentNodeType { name: "Grayscale", category: "Image Adjustments", diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 42f446e5f..ae6e1e818 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -199,6 +199,22 @@ pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, _contex vec![LayoutGroup::Row { widgets: vec![information] }, LayoutGroup::Row { widgets: vec![refresh_button] }] } +pub fn levels_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let input_shadows = number_widget(document_node, node_id, 1, "Shadows", NumberInput::default().min(0.).max(100.).unit("%"), true); + let input_midtones = number_widget(document_node, node_id, 2, "Midtones", NumberInput::default().min(0.).max(100.).unit("%"), true); + let input_highlights = number_widget(document_node, node_id, 3, "Highlights", NumberInput::default().min(0.).max(100.).unit("%"), true); + let output_minimums = number_widget(document_node, node_id, 4, "Output Minimums", NumberInput::default().min(0.).max(100.).unit("%"), true); + let output_maximums = number_widget(document_node, node_id, 5, "Output Maximums", NumberInput::default().min(0.).max(100.).unit("%"), true); + + vec![ + LayoutGroup::Row { widgets: input_shadows }, + LayoutGroup::Row { widgets: input_midtones }, + LayoutGroup::Row { widgets: input_highlights }, + LayoutGroup::Row { widgets: output_minimums }, + LayoutGroup::Row { widgets: output_maximums }, + ] +} + pub fn grayscale_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { const MIN: f64 = -200.; const MAX: f64 = 300.; diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 9c4e0f1b5..ee1976233 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -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 { + 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: Tint, diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 400a9b97a..804985a74 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -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 = 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 = 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, 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: []),