From 875f2a5cd1a8ad53c41e6757965addb481b25dff Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 8 Feb 2023 17:48:08 -0800 Subject: [PATCH] Improve Threshold node, move Gamma into Exposure node --- .../utility_types/widgets/input_widgets.rs | 12 ++++ .../document_node_types.rs | 20 ++---- .../node_properties.rs | 23 ++++--- node-graph/gcore/src/raster/adjustments.rs | 60 +++++++++-------- node-graph/gcore/src/raster/color.rs | 64 +++++++++++++++++++ .../interpreted-executor/src/node_registry.rs | 3 +- 6 files changed, 128 insertions(+), 54 deletions(-) diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 493ef299..80950330 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -278,6 +278,18 @@ impl NumberInput { self.mode = NumberInputMode::Range; self } + pub fn mode_range(mut self) -> Self { + self.mode = NumberInputMode::Range; + self + } + pub fn mode_increment(mut self) -> Self { + self.mode = NumberInputMode::Increment; + self + } + pub fn increment_step(mut self, step: f64) -> Self { + self.step = step; + self + } pub fn percentage(self) -> Self { self.min(0.).max(100.).unit("%").display_decimal_places(2) } 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 70dcd432..d6f3c5e5 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 @@ -252,17 +252,6 @@ static STATIC_NODES: &[DocumentNodeType] = &[ outputs: &[FrontendGraphDataType::Raster], properties: node_properties::brighten_image_properties, }, - DocumentNodeType { - name: "Gamma", - category: "Image Adjustments", - identifier: NodeImplementation::proto("graphene_core::raster::GammaNode<_>", &[concrete!("Image"), concrete!("f64")]), - inputs: &[ - DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), - DocumentInputType::new("Gamma", TaggedValue::F64(1.), false), - ], - outputs: &[FrontendGraphDataType::Raster], - properties: node_properties::adjust_gamma_properties, - }, DocumentNodeType { name: "Threshold", category: "Image Adjustments", @@ -310,10 +299,15 @@ static STATIC_NODES: &[DocumentNodeType] = &[ DocumentNodeType { name: "Exposure", category: "Image Adjustments", - identifier: NodeImplementation::proto("graphene_core::raster::ExposureNode<_>", &[concrete!("Image"), concrete!("f64")]), + identifier: NodeImplementation::proto( + "graphene_core::raster::ExposureNode<_, _, _>", + &[concrete!("Image"), concrete!("f64"), concrete!("f64"), concrete!("f64")], + ), inputs: &[ DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), - DocumentInputType::new("Value", TaggedValue::F64(0.), false), + DocumentInputType::new("Exposure", TaggedValue::F64(0.), false), + DocumentInputType::new("Offset", TaggedValue::F64(0.), false), + DocumentInputType::new("Gamma Correction", TaggedValue::F64(1.), false), ], outputs: &[FrontendGraphDataType::Raster], properties: node_properties::exposure_properties, 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 484a115c..e3de5fb6 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 @@ -183,12 +183,6 @@ pub fn blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _con vec![LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: sigma }] } -pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let gamma = number_widget(document_node, node_id, 1, "Gamma", NumberInput::default().min(0.01), true); - - vec![LayoutGroup::Row { widgets: gamma }] -} - pub fn adjust_threshold_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let thereshold = number_widget(document_node, node_id, 1, "Threshold", NumberInput::default().min(0.).max(1.), true); @@ -228,9 +222,22 @@ pub fn quantize_properties(document_node: &DocumentNode, node_id: NodeId, _conte vec![LayoutGroup::Row { widgets: value }, LayoutGroup::Row { widgets: index }] } pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let value = number_widget(document_node, node_id, 1, "Value", NumberInput::default().min(-3.).max(3.), true); + let exposure = number_widget(document_node, node_id, 1, "Exposure", NumberInput::default().min(-20.).max(20.), true); + let offset = number_widget(document_node, node_id, 2, "Offset", NumberInput::default().min(-0.5).max(0.5), true); + let gamma_correction = number_widget( + document_node, + node_id, + 3, + "Gamma Correction", + NumberInput::default().min(0.01).max(9.99).mode_increment().increment_step(0.1), + true, + ); - vec![LayoutGroup::Row { widgets: value }] + vec![ + LayoutGroup::Row { widgets: exposure }, + LayoutGroup::Row { widgets: offset }, + LayoutGroup::Row { widgets: gamma_correction }, + ] } pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index c14239c1..7e779964 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -7,22 +7,16 @@ use core::fmt::Debug; pub struct GrayscaleNode; #[node_macro::node_fn(GrayscaleNode)] -fn grayscale_color_node(input: Color) -> Color { - let avg = (input.r() + input.g() + input.b()) / 3.0; - map_rgb(input, |_| avg) -} +fn grayscale_color_node(color: Color) -> Color { + // TODO: Remove conversion to linear when the whole node graph uses linear color + let color = color.to_linear_srgb(); -#[derive(Debug)] -pub struct GammaNode { - gamma: Gamma, -} + let luminance = color.luminance(); -// https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/ -#[node_macro::node_fn(GammaNode)] -fn gamma_color_node(color: Color, gamma: f64) -> Color { - let inverse_gamma = 1. / gamma; - let per_channel = |channel: f32| channel.powf(inverse_gamma as f32); - map_rgb(color, per_channel) + // TODO: Remove conversion to linear when the whole node graph uses linear color + let luminance = Color::linear_to_srgb(luminance); + + color.map_rgb(|_| luminance) } #[cfg(not(target_arch = "spirv"))] @@ -56,7 +50,7 @@ pub struct InvertRGBNode; #[node_macro::node_fn(InvertRGBNode)] fn invert_image(color: Color) -> Color { - map_rgb(color, |c| 1. - c) + color.map_rgb(|c| 1. - c) } #[derive(Debug, Clone, Copy)] @@ -66,8 +60,12 @@ pub struct ThresholdNode { #[node_macro::node_fn(ThresholdNode)] fn threshold_node(color: Color, threshold: f64) -> Color { - let avg = (color.r() + color.g() + color.b()) / 3.0; - if avg >= threshold as f32 { + let threshold = Color::srgb_to_linear(threshold as f32); + + // TODO: Remove conversion to linear when the whole node graph uses linear color + let color = color.to_linear_srgb(); + + if color.luminance() >= threshold { Color::WHITE } else { Color::BLACK @@ -100,7 +98,7 @@ fn adjust_image_brightness_and_contrast(color: Color, brightness: f64, contrast: let (brightness, contrast) = (brightness as f32, contrast as f32); let factor = (259. * (contrast + 255.)) / (255. * (259. - contrast)); let channel = |channel: f32| ((factor * (channel * 255. + brightness - 128.) + 128.) / 255.).clamp(0., 1.); - map_rgb(color, channel) + color.map_rgb(channel) } #[derive(Debug, Clone, Copy)] @@ -126,25 +124,25 @@ fn posterize(color: Color, posterize_value: f64) -> Color { let number_of_areas = posterize_value.recip(); let size_of_areas = (posterize_value - 1.).recip(); let channel = |channel: f32| (channel / number_of_areas).floor() * size_of_areas; - map_rgb(color, channel) + color.map_rgb(channel) } #[derive(Debug, Clone, Copy)] -pub struct ExposureNode { - exposure: E, +pub struct ExposureNode { + exposure: Exposure, + offset: Offset, + gamma_correction: GammaCorrection, } // Based on https://stackoverflow.com/questions/12166117/what-is-the-math-behind-exposure-adjustment-on-photoshop #[node_macro::node_fn(ExposureNode)] -fn exposure(color: Color, exposure: f64) -> Color { +fn exposure(color: Color, exposure: f64, offset: f64, gamma_correction: f64) -> Color { let multiplier = 2_f32.powf(exposure as f32); - let channel = |channel: f32| channel * multiplier; - map_rgb(color, channel) -} - -pub fn map_rgba f32>(color: Color, f: F) -> Color { - Color::from_rgbaf32_unchecked(f(color.r()), f(color.g()), f(color.b()), f(color.a())) -} -pub fn map_rgb f32>(color: Color, f: F) -> Color { - Color::from_rgbaf32_unchecked(f(color.r()), f(color.g()), f(color.b()), color.a()) + color + // TODO: Fix incorrect behavior of offset + .map_rgb(|channel: f32| channel + offset as f32) + // TODO: Fix incorrect behavior of exposure + .map_rgb(|channel: f32| channel * multiplier) + // TODO: While gamma correction is correct on its own, determine and implement the correct order of these three operations + .gamma(gamma_correction as f32) } diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 09e555b1..750585b0 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -196,6 +196,22 @@ impl Color { self.alpha } + // From https://stackoverflow.com/a/56678483/775283 + pub fn luminance(&self) -> f32 { + 0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue + } + + // From https://stackoverflow.com/a/56678483/775283 + pub fn perceptual_luminance(&self) -> f32 { + let luminance = self.luminance(); + + if luminance <= 0.008856 { + (luminance * 903.3) / 100. + } else { + (luminance.powf(1. / 3.) * 116. - 16.) / 100. + } + } + /// Return the all components as a tuple, first component is red, followed by green, followed by blue, followed by alpha. /// /// # Examples @@ -334,6 +350,54 @@ impl Color { self.alpha + ((other.alpha - self.alpha) * t), ) } + + pub fn gamma(&self, gamma: f32) -> Color { + // From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/ + let inverse_gamma = 1. / gamma; + let per_channel = |channel: f32| channel.powf(inverse_gamma); + self.map_rgb(per_channel) + } + + pub fn to_linear_srgb(&self) -> Self { + Self { + red: Self::srgb_to_linear(self.red), + green: Self::srgb_to_linear(self.green), + blue: Self::srgb_to_linear(self.blue), + alpha: self.alpha, + } + } + + pub fn to_gamma_srgb(&self) -> Self { + Self { + red: Self::linear_to_srgb(self.red), + green: Self::linear_to_srgb(self.green), + blue: Self::linear_to_srgb(self.blue), + alpha: self.alpha, + } + } + + pub fn srgb_to_linear(channel: f32) -> f32 { + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } + } + + pub fn linear_to_srgb(channel: f32) -> f32 { + if channel <= 0.0031308 { + channel * 12.92 + } else { + 1.055 * channel.powf(1. / 2.4) - 0.055 + } + } + + pub fn map_rgba f32>(&self, f: F) -> Self { + Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), f(self.a())) + } + pub fn map_rgb f32>(&self, f: F) -> Self { + Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a()) + } } #[test] diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9a47f4f7..1d96f105 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -70,10 +70,9 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[ raster_node!(graphene_core::raster::ThresholdNode<_>, params: [f64]), raster_node!(graphene_core::raster::VibranceNode<_>, params: [f64]), raster_node!(graphene_core::raster::BrightnessContrastNode< _, _>, params: [f64, f64]), - raster_node!(graphene_core::raster::GammaNode<_>, params: [f64]), raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]), raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f64]), - raster_node!(graphene_core::raster::ExposureNode<_>, params: [f64]), + raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f64, f64, f64]), (NodeIdentifier::new("graphene_core::structural::MapImageNode", &[]), |args| { let map_fn: DowncastBothNode = DowncastBothNode::new(args[0]); let node = graphene_std::raster::MapImageNode::new(ValueNode::new(map_fn));