mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Replace the 'Luminance' node with a 'Desaturate' node with a better selection of desaturation methods (#4529)
* Remove the 'Threshold' node's luminance calculation dropdown so it always compares the Rec. 601 luma * Find the HSL lightness extremes before encoding and drop a stale luminance TODO
This commit is contained in:
@@ -16,8 +16,8 @@ use graphene_std::list::{Item, List, NodeIdPath};
|
||||
use graphene_std::math::float_noise::round_away_float_noise;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::raster::{
|
||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||
TonalRange,
|
||||
};
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::text::TextAlign;
|
||||
@@ -238,11 +238,12 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
List<ExtrudeJoiningAlgorithm>,
|
||||
List<PointSpacingType>,
|
||||
List<StringCapitalization>,
|
||||
List<LuminanceCalculation>,
|
||||
List<DesaturateMethod>,
|
||||
List<RedGreenBlue>,
|
||||
List<RedGreenBlueAlpha>,
|
||||
List<RelativeAbsolute>,
|
||||
List<SelectiveColorChoice>,
|
||||
List<TonalRange>,
|
||||
List<AdjustmentChannel>,
|
||||
List<XY>,
|
||||
List<ScaleType>,
|
||||
@@ -295,11 +296,12 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
Item<ExtrudeJoiningAlgorithm>,
|
||||
Item<PointSpacingType>,
|
||||
Item<StringCapitalization>,
|
||||
Item<LuminanceCalculation>,
|
||||
Item<DesaturateMethod>,
|
||||
Item<RedGreenBlue>,
|
||||
Item<RedGreenBlueAlpha>,
|
||||
Item<RelativeAbsolute>,
|
||||
Item<SelectiveColorChoice>,
|
||||
Item<TonalRange>,
|
||||
Item<AdjustmentChannel>,
|
||||
Item<XY>,
|
||||
Item<ScaleType>,
|
||||
@@ -1065,11 +1067,12 @@ impl_table_item_layout_for_choice_enum!(
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
DesaturateMethod,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
TonalRange,
|
||||
AdjustmentChannel,
|
||||
XY,
|
||||
ScaleType,
|
||||
@@ -1284,11 +1287,12 @@ macro_rules! known_item_types {
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
DesaturateMethod,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
TonalRange,
|
||||
AdjustmentChannel,
|
||||
XY,
|
||||
ScaleType,
|
||||
|
||||
@@ -20,7 +20,7 @@ use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::raster::{
|
||||
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
SelectiveColorChoice, TonalRange,
|
||||
};
|
||||
use graphene_std::raster_types::Image;
|
||||
@@ -332,7 +332,7 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if id_is::<PointSpacingType>(x) => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<BooleanOperation>(x) => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<CentroidType>(x) => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<LuminanceCalculation>(x) => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<DesaturateMethod>(x) => enum_choice::<DesaturateMethod>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<QRCodeErrorCorrectionLevel>(x) => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<ScaleType>(x) => enum_choice::<ScaleType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<InterpolationDistribution>(x) => enum_choice::<InterpolationDistribution>().for_socket(default_info).property_row(),
|
||||
|
||||
@@ -591,8 +591,9 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::raster_nodes::adjustments::luminance::IDENTIFIER,
|
||||
node: graphene_std::raster_nodes::adjustments::desaturate::IDENTIFIER,
|
||||
aliases: &[
|
||||
"raster_nodes::adjustments::LuminanceNode",
|
||||
"graphene_raster_nodes::adjustments::LuminanceNode",
|
||||
"graphene_core::raster::adjustments::LuminanceNode",
|
||||
"graphene_core::raster::LuminanceNode",
|
||||
|
||||
@@ -548,7 +548,8 @@ tagged_value! {
|
||||
// ENUM TYPES
|
||||
// ==========
|
||||
BlendMode(core_types::blending::BlendMode),
|
||||
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
||||
#[serde(alias = "LuminanceCalculation")]
|
||||
DesaturateMethod(raster_nodes::adjustments::DesaturateMethod),
|
||||
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
||||
XY(graphene_core::extract_xy::XY),
|
||||
StringCapitalization(text_nodes::StringCapitalization),
|
||||
|
||||
@@ -351,7 +351,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
DesaturateMethod,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
|
||||
@@ -45,7 +45,6 @@ impl Luminance for RGBA16F {
|
||||
type LuminanceChannel = f32;
|
||||
#[inline(always)]
|
||||
fn luminance(&self) -> f32 {
|
||||
// TODO: verify this is correct for sRGB
|
||||
0.2126 * self.red() + 0.7152 * self.green() + 0.0722 * self.blue()
|
||||
}
|
||||
}
|
||||
@@ -541,37 +540,36 @@ impl Color {
|
||||
}
|
||||
|
||||
/// Relative luminance using Rec.709 / sRGB-primary weights, computed on linear-light RGB.
|
||||
// From https://stackoverflow.com/a/56678483/775283
|
||||
#[inline(always)]
|
||||
pub fn luminance_rec_709(&self) -> f32 {
|
||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||
0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue
|
||||
}
|
||||
|
||||
/// Luma using Rec.601 SDTV coefficients.
|
||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||
#[inline(always)]
|
||||
pub fn luminance_rec_601(&self) -> f32 {
|
||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||
0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
|
||||
}
|
||||
|
||||
/// Luma using rounded Rec.601 coefficients (`0.3 / 0.59 / 0.11`), as used by some legacy image processing.
|
||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||
#[inline(always)]
|
||||
pub fn luminance_rec_601_rounded(&self) -> f32 {
|
||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||
0.3 * self.red + 0.59 * self.green + 0.11 * self.blue
|
||||
}
|
||||
|
||||
/// Perceptual lightness (CIE L*) of the Rec.709 luminance, normalized to 0..1.
|
||||
// From https://stackoverflow.com/a/56678483/775283
|
||||
/// Perceptual lightness (OkLab L) of the linear-light RGB, 0..1.
|
||||
#[inline(always)]
|
||||
pub fn luminance_perceptual(&self) -> f32 {
|
||||
let luminance = self.luminance_rec_709();
|
||||
pub fn lightness_oklab(&self) -> f32 {
|
||||
// From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
|
||||
if luminance <= 0.008856 {
|
||||
(luminance * 903.3) / 100.
|
||||
} else {
|
||||
(luminance.cbrt() * 116. - 16.) / 100.
|
||||
}
|
||||
let long = 0.41222147 * self.red + 0.53633254 * self.green + 0.05144599 * self.blue;
|
||||
let medium = 0.2119035 * self.red + 0.6806995 * self.green + 0.10739696 * self.blue;
|
||||
let short = 0.08830246 * self.red + 0.28171884 * self.green + 0.6299787 * self.blue;
|
||||
|
||||
0.21045426 * long.cbrt() + 0.7936178 * medium.cbrt() - 0.004072047 * short.cbrt()
|
||||
}
|
||||
|
||||
/// Construct an opaque grayscale color where R = G = B = `luminance`.
|
||||
@@ -1062,6 +1060,15 @@ impl Color {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn oklab_lightness_spans_black_to_white() {
|
||||
assert!(Color::BLACK.lightness_oklab().abs() < 1e-4);
|
||||
assert!((Color::WHITE.lightness_oklab() - 1.).abs() < 1e-4);
|
||||
// A gray keeps L at the cube root of its linear value, since the three cone responses sum to it
|
||||
assert!((Color::from_luminance(0.18).lightness_oklab() - 0.18_f32.cbrt()).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hsl_roundtrip() {
|
||||
for (red, green, blue) in [
|
||||
|
||||
@@ -35,24 +35,52 @@ use vector_types::Gradient;
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6
|
||||
|
||||
/// Conversion from a color to grayscale.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
|
||||
#[widget(Dropdown)]
|
||||
#[repr(u32)]
|
||||
pub enum LuminanceCalculation {
|
||||
pub enum DesaturateMethod {
|
||||
/// Light level of the color, the Y (luminance) of Rec. 709, which weights the linear-light RGB channels by `0.2126, 0.7152, 0.0722`.
|
||||
///
|
||||
/// Accessibility contrast ratios and SVG luminance masks use this.
|
||||
#[default]
|
||||
#[label("sRGB")]
|
||||
SRGB,
|
||||
Perceptual,
|
||||
AverageChannels,
|
||||
MinimumChannels,
|
||||
MaximumChannels,
|
||||
#[label("Luminance (Rec. 709)")]
|
||||
#[cfg_attr(feature = "serde", serde(alias = "SRGB"))]
|
||||
LuminanceRec709,
|
||||
/// Light level approximation for the color, the Y′ (luma) of Rec. 709, which weights the gamma-encoded RGB channels by `0.2126, 0.7152, 0.0722`.
|
||||
///
|
||||
/// CSS filter functions such as `grayscale()` use this.
|
||||
#[label("Luma (Rec. 709)")]
|
||||
LumaRec709,
|
||||
/// Light level approximation for the color, the Y′ (luma) of Rec. 601, which weights the gamma-encoded RGB channels by `0.299, 0.587, 0.114`.
|
||||
#[label("Luma (Rec. 601)")]
|
||||
LumaRec601,
|
||||
/// Perceptually uniform scale from black to white, the L (lightness) of OkLab.
|
||||
#[label("Lightness (OkLab)")]
|
||||
#[cfg_attr(feature = "serde", serde(alias = "Perceptual"))]
|
||||
LightnessOkLab,
|
||||
/// Mean of the three linear-light RGB channels.
|
||||
#[menu_separator]
|
||||
#[cfg_attr(feature = "serde", serde(alias = "AverageChannels"))]
|
||||
ChannelsAverage,
|
||||
/// Smallest of the three linear-light RGB channels.
|
||||
#[cfg_attr(feature = "serde", serde(alias = "MinimumChannels"))]
|
||||
ChannelsMinimum,
|
||||
/// Largest of the three linear-light RGB channels, the V (value) of HSV.
|
||||
#[cfg_attr(feature = "serde", serde(alias = "MaximumChannels"))]
|
||||
ChannelsMaximum,
|
||||
/// Midpoint of the largest and smallest gamma-encoded RGB channels, the L (lightness) of HSL.
|
||||
///
|
||||
/// The classic "Desaturate" command of many image editors uses this.
|
||||
#[label("Lightness (HSL)")]
|
||||
LightnessHsl,
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn luminance<T: Adjust<Color>>(
|
||||
fn desaturate<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
Raster<CPU>,
|
||||
@@ -61,18 +89,38 @@ fn luminance<T: Adjust<Color>>(
|
||||
)]
|
||||
#[gpu_image]
|
||||
input: Item<T>,
|
||||
luminance_calc: Item<LuminanceCalculation>,
|
||||
method: Item<DesaturateMethod>,
|
||||
) -> Item<T> {
|
||||
let mut input = input;
|
||||
let luminance_calc = luminance_calc.into_element();
|
||||
let method = method.into_element();
|
||||
|
||||
input.element_mut().adjust(|color| {
|
||||
let luminance = match luminance_calc {
|
||||
LuminanceCalculation::SRGB => color.luminance_rec_709(),
|
||||
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
||||
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
||||
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
||||
LuminanceCalculation::MaximumChannels => color.maximum_rgb_channels(),
|
||||
// Gamma-encoded formulas are decoded as if they were a gray
|
||||
let gamma = || color.to_gamma_srgb_channels();
|
||||
let luminance = match method {
|
||||
DesaturateMethod::LuminanceRec709 => color.luminance_rec_709(),
|
||||
DesaturateMethod::LumaRec709 => {
|
||||
let [r, g, b, _] = gamma();
|
||||
srgb_to_linear(0.2126 * r + 0.7152 * g + 0.0722 * b)
|
||||
}
|
||||
DesaturateMethod::LumaRec601 => {
|
||||
let [r, g, b, _] = gamma();
|
||||
srgb_to_linear(0.299 * r + 0.587 * g + 0.114 * b)
|
||||
}
|
||||
DesaturateMethod::LightnessOkLab => {
|
||||
// A gray's OkLab lightness is the cube root of its linear value, so cubing gives the gray of equal lightness
|
||||
let lightness = color.lightness_oklab();
|
||||
lightness * lightness * lightness
|
||||
}
|
||||
DesaturateMethod::ChannelsAverage => color.average_rgb_channels(),
|
||||
DesaturateMethod::ChannelsMinimum => color.minimum_rgb_channels(),
|
||||
DesaturateMethod::ChannelsMaximum => color.maximum_rgb_channels(),
|
||||
DesaturateMethod::LightnessHsl => {
|
||||
// The transfer curve is monotonic, so the extremes are found first and only they are encoded
|
||||
let max = linear_to_srgb(color.maximum_rgb_channels());
|
||||
let min = linear_to_srgb(color.minimum_rgb_channels());
|
||||
srgb_to_linear((max + min) / 2.)
|
||||
}
|
||||
};
|
||||
color.map_rgb(|_| luminance)
|
||||
});
|
||||
@@ -1401,11 +1449,11 @@ fn color_balance<T: Adjust<Color>>(
|
||||
#[cfg(feature = "std")]
|
||||
mod _graphene_hash_impls {
|
||||
use super::{
|
||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
SelectiveColorChoice, TonalRange,
|
||||
};
|
||||
graphene_hash::impl_via_hash!(
|
||||
LuminanceCalculation,
|
||||
DesaturateMethod,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
NoiseType,
|
||||
|
||||
Reference in New Issue
Block a user