From 2fb2fc9dd7d19c4217e78e1cfdd3d57cffac4610 Mon Sep 17 00:00:00 2001 From: xkns <81481761+xkns@users.noreply.github.com> Date: Wed, 17 Aug 2022 23:26:19 +0200 Subject: [PATCH] Saturation & Brightness/Value Gain using Oklab color space (#1477) * Imported Oklab reference implementation * Add Okhsv conversions * Fixed formatting error * Add saturation and value gain to schemas * Add english translation for saturation, value gain * Created OkhsvTransform * Make OkhsvTransform configurable * Apply OkhvsTransform * Clamped values during transform * Precalculate isIdentity in OkhsvTransform * Skip OkhsvTransform if it is the identity function * Added changelog message * Allow for full desaturation * Imported recommended changes by LordGrey * Fixed typo in constant * Fixed anti-pattern in ok_color.h * Correct indentions * Correct remote-control * Limited maximum gain settings to practical range * Renane valueGain to brightnessGain for clarity and understanding Co-authored-by: LordGrey --- 3RD_PARTY_LICENSES | 24 + CHANGELOG.md | 16 +- assets/webconfig/i18n/en.json | 16 +- assets/webconfig/js/content_remote.js | 23 +- config/hyperion.config.json.default | 4 +- dependencies/include/oklab/ok_color.h | 708 ++++++++++++++++++ include/hyperion/ColorAdjustment.h | 7 +- include/utils/ColorSys.h | 33 +- include/utils/OkhsvTransform.h | 63 ++ include/utils/hyperion.h | 9 + .../api/JSONRPC_schema/schema-adjustment.json | 12 + libsrc/api/JsonAPI.cpp | 15 +- libsrc/hyperion/MultiColorAdjustment.cpp | 8 +- libsrc/hyperion/schema/schema-color.json | 36 +- libsrc/utils/CMakeLists.txt | 2 + libsrc/utils/ColorSys.cpp | 20 + libsrc/utils/OkhsvTransform.cpp | 69 ++ 17 files changed, 1033 insertions(+), 32 deletions(-) create mode 100644 dependencies/include/oklab/ok_color.h create mode 100644 include/utils/OkhsvTransform.h create mode 100644 libsrc/utils/OkhsvTransform.cpp diff --git a/3RD_PARTY_LICENSES b/3RD_PARTY_LICENSES index 86fd7a01..a862512b 100644 --- a/3RD_PARTY_LICENSES +++ b/3RD_PARTY_LICENSES @@ -1874,3 +1874,27 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS + +========================================== +Oklab Color Space Reference Implementation +========================================== + +Copyright (c) 2021 Björn Ottosson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/CHANGELOG.md b/CHANGELOG.md index 285f4087..6e5aae3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added saturation gain and brightness/value gain as new color processing settings + ### Changed ### Fixed @@ -22,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow to build a "light" version of Hyperion, i.e. no grabbers, or services like flat-/proto buffers, boblight, CEC - Allow to restart Hyperion via Systray - mDNS support for all platforms inkl. Windows (#740) -- Forwarder: mDNS discovery support and ease of configuration of other Hyperion instances +- Forwarder: mDNS discovery support and ease of configuration of other Hyperion instances - Grabber: mDNS discovery for standalone grabbers - Grabber: Dynamic loading of the Dispmanx Grabber (#1418) - Flatbuffer/Protobuf are now able to receive RGBA data @@ -30,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New language: Japanese ##### LED-Devices -- Support retry attempts enabling devices, e.g. to open devices after network or a device itself got available (#1302). Fixes that devices got "stuck", if initial open failed e.g. for WLED, Hue +- Support retry attempts enabling devices, e.g. to open devices after network or a device itself got available (#1302). Fixes that devices got "stuck", if initial open failed e.g. for WLED, Hue - New UDP-DDP (Distributed Display Protocol) device to overcome the 490 LEDs limitation of UDP-RAW - mDNS discovery support and ease of configuration (Cololight, Nanoleaf, Philips-Hue, WLED, Yeelight); removes the need to configure IP-Address, as address is resolved automatically. - Allow to disable switching LEDs on during startup (#1390) @@ -116,7 +118,7 @@ Note: Existing configurations are migrated to new structures automatically - Fixed hyperion-remote when sending multiple Hex-Colors with "Set Color" option - UI: Fixed "Selected Hyperion instance isn't running" issue (#1357) - Fixed Database migration version handling -- Fixed Python ModuleNotFoundError (#1109) +- Fixed Python ModuleNotFoundError (#1109) ### Technical @@ -135,7 +137,7 @@ We did not weaken security, but provide you with an easy to use script to switch ### Added: - Script to change the user Hyperion is executed with. To run Hyperion with root privileges (e.g. for WS281x) execute
`sudo updateHyperionUser -u root` -- Gif effects can source Gifs via URLs in addition to local files as input +- Gif effects can source Gifs via URLs in addition to local files as input - System info screen: Added used config path and "is run under root/admin" - LED-Device enhancements @@ -167,7 +169,7 @@ To run Hyperion with root privileges (e.g. for WS281x) execute
`sudo update - Escape XSS payload to avoid execution (#1292) - Include libqt5sql5-sqlite packaging dependency - Fixed embedded Python location (#1109) - + - LED-Devices - Fixed Philips Hue wizard (#1276) - Fixed AtmoOrb wizard @@ -193,12 +195,12 @@ The refined color coding in the user-interfaces, helps you to quickly identify i Of course, the release brings new features (e.g. USB Capture on Windows), as well as minor enhancements and a good number of fixes. -Note: +Note: - **IMPORTANT:** Due to the rework of the grabbers, both screen- and video grabbers are disabled after the upgrade to the new version. Please, re-enable the grabber of choice via the UI, validate the configuration and save the setup. The grabber should the restart. -- Hyperion packages can now be installed under Ubuntu (x64) and Debian (amd64/armhf) (incl. Raspberry Pi OS) via our own APT server. +- Hyperion packages can now be installed under Ubuntu (x64) and Debian (amd64/armhf) (incl. Raspberry Pi OS) via our own APT server. Details about the installation can be found in the [installation.md](https://github.com/hyperion-project/hyperion.ng/blob/master/Installation.md) and at [apt.hyperion-project.org](apt.hyperion-project.org). - Find here more details on [supported platforms and configuration sets](https://github.com/hyperion-project/hyperion.ng/blob/master/doc/development/SupportedPlatforms.md) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index eec08e8a..bb8efdd3 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -88,16 +88,16 @@ "conf_leds_layout_cl_leftbottom": "Left 50% - 100% Bottom", "conf_leds_layout_cl_leftmiddle": "Left 25% - 75% Middle", "conf_leds_layout_cl_lefttop": "Left 0% - 50% Top", - "conf_leds_layout_cl_lightPosBottomLeft14": "Bottom: 0 - 25% from Left", + "conf_leds_layout_cl_lightPosBottomLeft14": "Bottom: 0 - 25% from Left", "conf_leds_layout_cl_lightPosBottomLeft12": "Bottom: 25 - 50% from Left", "conf_leds_layout_cl_lightPosBottomLeft34": "Bottom: 50 - 75% from Left", "conf_leds_layout_cl_lightPosBottomLeft11": "Bottom: 75 - 100% from Left", - "conf_leds_layout_cl_lightPosBottomLeft112": "Bottom: 0 - 50% from Left", + "conf_leds_layout_cl_lightPosBottomLeft112": "Bottom: 0 - 50% from Left", "conf_leds_layout_cl_lightPosBottomLeft121": "Bottom: 50 - 100% from Left", "conf_leds_layout_cl_lightPosBottomLeftNewMid": "Bottom: 25 - 75% from Left", - "conf_leds_layout_cl_lightPosTopLeft112": "Top: 0 - 50% from Left", + "conf_leds_layout_cl_lightPosTopLeft112": "Top: 0 - 50% from Left", "conf_leds_layout_cl_lightPosTopLeft121": "Top: 50 - 100% from Left", - "conf_leds_layout_cl_lightPosTopLeftNewMid": "Top: 25 - 75% from Left", + "conf_leds_layout_cl_lightPosTopLeftNewMid": "Top: 25 - 75% from Left", "conf_leds_layout_cl_overlap": "Overlap", "conf_leds_layout_cl_reversdir": "Reverse direction", "conf_leds_layout_cl_right": "Right", @@ -284,6 +284,10 @@ "edt_conf_color_magenta_title": "Magenta", "edt_conf_color_red_expl": "The calibrated red value.", "edt_conf_color_red_title": "Red", + "edt_conf_color_saturationGain_expl": "Adjusts the saturation of colors. 1.0 means no change, over 1.0 increases saturation, under 1.0 desaturates.", + "edt_conf_color_saturationGain_title": "Saturation gain", + "edt_conf_color_brightnessGain_expl": "Adjusts the brightness of colors. 1.0 means no change, over 1.0 increases brightness, under 1.0 decreases brightness.", + "edt_conf_color_brightnessGain_title": "Brightness gain", "edt_conf_color_white_expl": "The calibrated white value.", "edt_conf_color_white_title": "White", "edt_conf_color_yellow_expl": "The calibrated yellow value.", @@ -551,7 +555,7 @@ "edt_dev_spec_brightnessOverwrite_title": "Overwrite brightness", "edt_dev_spec_brightnessThreshold_title": "Signal detection brightness minimum", "edt_dev_spec_brightness_title": "Brightness", - "edt_dev_spec_candyGamma_title" : "'Candy' mode (double gamma correction)", + "edt_dev_spec_candyGamma_title" : "'Candy' mode (double gamma correction)", "edt_dev_spec_chanperfixture_title": "Channels per Fixture", "edt_dev_spec_cid_title": "CID", "edt_dev_spec_clientKey_title": "Clientkey", @@ -862,7 +866,7 @@ "general_speech_fr": "French", "general_speech_hu": "Hungarian", "general_speech_it": "Italian", - "general_speech_ja": "Japanese", + "general_speech_ja": "Japanese", "general_speech_nb": "Norwegian (Bokmål)", "general_speech_nl": "Dutch", "general_speech_pl": "Polish", diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index c6a0b6d4d..d0b21285 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -56,10 +56,20 @@ $(document).ready(function () { }); } else { - if (sColor[key].key == "brightness" || sColor[key].key == "brightnessCompensation" || sColor[key].key == "backlightThreshold") - property = '
' + $.i18n("edt_append_percent") + '
'; - else + if (sColor[key].key == "brightness" || + sColor[key].key == "brightnessCompensation" || + sColor[key].key == "backlightThreshold" || + sColor[key].key == "saturationGain" || + sColor[key].key == "brightnessGain") { + + property = ''; + if (sColor[key].append === "edt_append_percent") { + property = '
' + property + '' + $.i18n("edt_append_percent") + '
'; + } + } + else { property = ''; + } $('.crtbody').append(createTableRow([title, property], false, true)); $('#cr_' + sColor[key].key).off().on('change', function (e) { @@ -134,7 +144,7 @@ $(document).ready(function () { owner = $.i18n('remote_color_label_color') + ' ' + '
'; break; case "IMAGE": - owner = $.i18n('remote_effects_label_picture') + (owner !== undefined ? (' ' + owner): ""); + owner = $.i18n('remote_effects_label_picture') + (owner !== undefined ? (' ' + owner) : ""); break; case "GRABBER": owner = $.i18n('general_comp_GRABBER') + ': (' + owner + ')'; @@ -153,8 +163,7 @@ $(document).ready(function () { break; } - if (!(duration && duration < 0)) - { + if (!(duration && duration < 0)) { if (duration && compId != "GRABBER" && compId != "FLATBUFSERVER" && compId != "PROTOSERVER") owner += '
' + $.i18n('remote_input_duration') + ' ' + duration.toFixed(0) + $.i18n('edt_append_s') + ''; @@ -386,7 +395,6 @@ $(document).ready(function () { $('#effect_row').hide(); } - // interval updates $(window.hyperion).on('components-updated', function (e, comp) { @@ -415,3 +423,4 @@ $(document).ready(function () { removeOverlay(); }); + diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index e6e0c9af..c54cb28f 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -46,7 +46,9 @@ "backlightThreshold" : 0, "backlightColored" : false, "brightness" : 100, - "brightnessCompensation" : 100 + "brightnessCompensation" : 100, + "saturationGain" : 1.0, + "brightnessGain" : 1.0 } ] }, diff --git a/dependencies/include/oklab/ok_color.h b/dependencies/include/oklab/ok_color.h new file mode 100644 index 00000000..203794c5 --- /dev/null +++ b/dependencies/include/oklab/ok_color.h @@ -0,0 +1,708 @@ +#ifndef OKCOLOR_H +#define OKCOLOR_H +// Copyright(c) 2021 Björn Ottosson + Modifications by Hyperion Project +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files(the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and /or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions : +// The above copyright noticeand this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include +#include + +namespace ok_color +{ + +struct Lab { double L; double a; double b; }; +struct RGB { double r; double g; double b; }; + +struct HSV { double h; double s; double v; }; +struct HSL { double h; double s; double l; }; +struct LC { double L; double C; }; + +// Alternative representation of (L_cusp, C_cusp) +// Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp) +// The maximum value for C in the triangle is then found as fmin(S*L, T*(1-L)), for a given L +struct ST { double S; double T; }; + +constexpr double pi = 3.1415926535897932384626433832795028841971693993751058209749445923078164062; + +double clamp(double x, double min, double max) +{ + if (x < min) { + return min; + } + if (x > max) { + return max; + } + + return x; +} + +double sgn(double x) +{ + return static_cast(0.0 < x) - static_cast(x < 0.0); +} + +double srgb_transfer_function(double a) +{ + return .0031308 >= a ? 12.92 * a : 1.055 * pow(a, .4166666666666667) - .055; +} + +double srgb_transfer_function_inv(double a) +{ + return .04045< a ? pow((a + .055) / 1.055, 2.4) : a / 12.92; +} + +Lab linear_srgb_to_oklab(RGB c) +{ + double l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b; + double m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b; + double s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b; + + double l_ = cbrt(l); + double m_ = cbrt(m); + double s_ = cbrt(s); + + return { + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + }; +} + +RGB oklab_to_linear_srgb(Lab c) +{ + double l_ = c.L + 0.3963377774 * c.a + 0.2158037573 * c.b; + double m_ = c.L - 0.1055613458 * c.a - 0.0638541728 * c.b; + double s_ = c.L - 0.0894841775 * c.a - 1.2914855480 * c.b; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + return { + +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + }; +} + +// Finds the maximum saturation possible for a given hue that fits in sRGB +// Saturation here is defined as S = C/L +// a and b must be normalized so a^2 + b^2 == 1 +double compute_max_saturation(double a, double b) +{ + // Max saturation will be when one of r, g or b goes below zero. + + // Select different coefficients depending on which component goes below zero first + double k0; + double k1; + double k2; + double k3; + double k4; + double wl; + double wm; + double ws; + + if (-1.88170328 * a - 0.80936493 * b > 1) + { + // Red component + k0 = +1.19086277; k1 = +1.76576728; k2 = +0.59662641; k3 = +0.75515197; k4 = +0.56771245; + wl = +4.0767416621; wm = -3.3077115913; ws = +0.2309699292; + } + else + if (1.81444104 * a - 1.19445276 * b > 1) + { + // Green component + k0 = +0.73956515; k1 = -0.45954404; k2 = +0.08285427; k3 = +0.12541070; k4 = +0.14503204; + wl = -1.2684380046; wm = +2.6097574011; ws = -0.3413193965; + } + else + { + // Blue component + k0 = +1.35733652; k1 = -0.00915799; k2 = -1.15130210; k3 = -0.50559606; k4 = +0.00692167; + wl = -0.0041960863; wm = -0.7034186147; ws = +1.7076147010; + } + + // Approximate max saturation using a polynomial: + double S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; + + // Do one step Halley's method to get closer + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite + // this should be sufficient for most applications, otherwise do two/three steps + + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; + + { + double l_ = 1.0 + S * k_l; + double m_ = 1.0 + S * k_m; + double s_ = 1.0 + S * k_s; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + double l_dS = 3.0 * k_l * l_ * l_; + double m_dS = 3.0 * k_m * m_ * m_; + double s_dS = 3.0 * k_s * s_ * s_; + + double l_dS2 = 6.0 * k_l * k_l * l_; + double m_dS2 = 6.0 * k_m * k_m * m_; + double s_dS2 = 6.0 * k_s * k_s * s_; + + double f = wl * l + wm * m + ws * s; + double f1 = wl * l_dS + wm * m_dS + ws * s_dS; + double f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; + + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + } + + return S; +} + +// finds L_cusp and C_cusp for a given hue +// a and b must be normalized so a^2 + b^2 == 1 +LC find_cusp(double a, double b) +{ + // First, find the maximum saturation (saturation S = C/L) + double S_cusp = compute_max_saturation(a, b); + + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: + RGB rgb_at_max = oklab_to_linear_srgb({ 1, S_cusp * a, S_cusp * b }); + double L_cusp = cbrt(1.0 / fmax(fmax(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b)); + double C_cusp = L_cusp * S_cusp; + + return { L_cusp , C_cusp }; +} + +// Finds intersection of the line defined by +// L = L0 * (1 - t) + t * L1; +// C = t * C1; +// a and b must be normalized so a^2 + b^2 == 1 +double find_gamut_intersection(double a, double b, double L1, double C1, double L0, LC cusp) +{ + // Find the intersection for upper and lower half seprately + double t; + if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.0) + { + // Lower half + + t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); + } + else + { + // Upper half + + // First intersect with triangle + t = cusp.C * (L0 - 1.0) / (C1 * (cusp.L - 1.0) + cusp.C * (L0 - L1)); + + // Then one step Halley's method + { + double dL = L1 - L0; + double dC = C1; + + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; + + double l_dt = dL + dC * k_l; + double m_dt = dL + dC * k_m; + double s_dt = dL + dC * k_s; + + + // If higher accuracy is required, 2 or 3 iterations of the following block can be used: + { + double L = L0 * (1.0 - t) + t * L1; + double C = t * C1; + + double l_ = L + C * k_l; + double m_ = L + C * k_m; + double s_ = L + C * k_s; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + double ldt = 3 * l_dt * l_ * l_; + double mdt = 3 * m_dt * m_ * m_; + double sdt = 3 * s_dt * s_ * s_; + + double ldt2 = 6 * l_dt * l_dt * l_; + double mdt2 = 6 * m_dt * m_dt * m_; + double sdt2 = 6 * s_dt * s_dt * s_; + + double r0 = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1; + double r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; + double r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + + double u_r = r1 / (r1 * r1 - 0.5 * r0 * r2); + double t_r = -r0 * u_r; + + double g0 = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1; + double g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; + double g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + + double u_g = g1 / (g1 * g1 - 0.5 * g0 * g2); + double t_g = -g0 * u_g; + + double b0 = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1; + double b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; + double b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; + + double u_b = b1 / (b1 * b1 - 0.5 * b0 * b2); + double t_b = -b0 * u_b; + + t_r = u_r >= 0.0 ? t_r : DBL_MAX; + t_g = u_g >= 0.0 ? t_g : DBL_MAX; + t_b = u_b >= 0.0 ? t_b : DBL_MAX; + + t += fmin(t_r, fmin(t_g, t_b)); + } + } + } + + return t; +} + +double find_gamut_intersection(double a, double b, double L1, double C1, double L0) +{ + // Find the cusp of the gamut triangle + LC cusp = find_cusp(a, b); + + return find_gamut_intersection(a, b, L1, C1, L0, cusp); +} + +RGB gamut_clip_preserve_chroma(RGB rgb) +{ + if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0) + { + return rgb; + } + + Lab lab = linear_srgb_to_oklab(rgb); + + double L = lab.L; + double eps = 0.00001; + double C = fmax(eps, sqrt(lab.a * lab.a + lab.b * lab.b)); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L0 = clamp(L, 0, 1); + + double t = find_gamut_intersection(a_, b_, L, C, L0); + double L_clipped = L0 * (1 - t) + t * L; + double C_clipped = t * C; + + return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ }); +} + +RGB gamut_clip_project_to_0_5(RGB rgb) +{ + if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0) { + return rgb; + } + + Lab lab = linear_srgb_to_oklab(rgb); + + double L = lab.L; + double eps = 0.00001; + double C = fmax(eps, sqrt(lab.a * lab.a + lab.b * lab.b)); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L0 = 0.5; + + double t = find_gamut_intersection(a_, b_, L, C, L0); + double L_clipped = L0 * (1 - t) + t * L; + double C_clipped = t * C; + + return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ }); +} + +RGB gamut_clip_project_to_L_cusp(RGB rgb) +{ + if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0) { + return rgb; + } + + Lab lab = linear_srgb_to_oklab(rgb); + + double L = lab.L; + double eps = 0.00001; + double C = fmax(eps, sqrt(lab.a * lab.a + lab.b * lab.b)); + double a_ = lab.a / C; + double b_ = lab.b / C; + + // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once. + LC cusp = find_cusp(a_, b_); + + double L0 = cusp.L; + + double t = find_gamut_intersection(a_, b_, L, C, L0); + + double L_clipped = L0 * (1 - t) + t * L; + double C_clipped = t * C; + + return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ }); +} + +RGB gamut_clip_adaptive_L0_0_5(RGB rgb, double alpha = 0.05) +{ + if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0) { + return rgb; + } + + Lab lab = linear_srgb_to_oklab(rgb); + + double L = lab.L; + double eps = 0.00001; + double C = fmax(eps, sqrt(lab.a * lab.a + lab.b * lab.b)); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double Ld = L - 0.5; + double e1 = 0.5 + fabs(Ld) + alpha * C; + double L0 = 0.5 * (1.0 + sgn(Ld) * (e1 - sqrt(e1 * e1 - 2.0 * fabs(Ld)))); + + double t = find_gamut_intersection(a_, b_, L, C, L0); + double L_clipped = L0 * (1.0 - t) + t * L; + double C_clipped = t * C; + + return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ }); +} + +RGB gamut_clip_adaptive_L0_L_cusp(RGB rgb, double alpha = 0.05) +{ + if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0) { + return rgb; + } + + Lab lab = linear_srgb_to_oklab(rgb); + + double L = lab.L; + double eps = 0.00001; + double C = fmax(eps, sqrt(lab.a * lab.a + lab.b * lab.b)); + double a_ = lab.a / C; + double b_ = lab.b / C; + + // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once. + LC cusp = find_cusp(a_, b_); + + double Ld = L - cusp.L; + double k = 2.0 * (Ld > 0 ? 1.0 - cusp.L : cusp.L); + + double e1 = 0.5 * k + fabs(Ld) + alpha * C / k; + double L0 = cusp.L + 0.5 * (sgn(Ld) * (e1 - sqrt(e1 * e1 - 2.0 * k * fabs(Ld)))); + + double t = find_gamut_intersection(a_, b_, L, C, L0); + double L_clipped = L0 * (1.0 - t) + t * L; + double C_clipped = t * C; + + return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ }); +} + +double toe(double x) +{ + constexpr double k_1 = 0.206; + constexpr double k_2 = 0.03; + constexpr double k_3 = (1.0 + k_1) / (1.0 + k_2); + return 0.5 * (k_3 * x - k_1 + sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)); +} + +double toe_inv(double x) +{ + constexpr double k_1 = 0.206; + constexpr double k_2 = 0.03; + constexpr double k_3 = (1.0 + k_1) / (1.0 + k_2); + return (x * x + k_1 * x) / (k_3 * (x + k_2)); +} + +ST to_ST(LC cusp) +{ + double L = cusp.L; + double C = cusp.C; + return { C / L, C / (1 - L) }; +} + +// Returns a smooth approximation of the location of the cusp +// This polynomial was created by an optimization process +// It has been designed so that S_mid < S_max and T_mid < T_max +ST get_ST_mid(double a_, double b_) +{ + double S = 0.11516993 + 1.0 / ( + + 7.44778970 + 4.15901240 * b_ + + a_ * (-2.19557347 + 1.75198401 * b_ + + a_ * (-2.13704948 - 10.02301043 * b_ + + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_ + ))) + ); + + double T = 0.11239642 + 1.0 / ( + + 1.61320320 - 0.68124379 * b_ + + a_ * (+0.40370612 + 0.90148123 * b_ + + a_ * (-0.27087943 + 0.61223990 * b_ + + a_ * (+0.00299215 - 0.45399568 * b_ - 0.14661872 * a_ + ))) + ); + + return { S, T }; +} + +struct Cs { double C_0; double C_mid; double C_max; }; +Cs get_Cs(double L, double a_, double b_) +{ + LC cusp = find_cusp(a_, b_); + + double C_max = find_gamut_intersection(a_, b_, L, 1, L, cusp); + ST ST_max = to_ST(cusp); + + // Scale factor to compensate for the curved part of gamut shape: + double k = C_max / fmin((L * ST_max.S), (1 - L) * ST_max.T); + + double C_mid; + { + ST ST_mid = get_ST_mid(a_, b_); + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + double C_a = L * ST_mid.S; + double C_b = (1.0 - L) * ST_mid.T; + C_mid = 0.9 * k * sqrt(sqrt(1.0 / (1.0 / (C_a * C_a * C_a * C_a) + 1.0 / (C_b * C_b * C_b * C_b)))); + } + + double C_0; + { + // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. + double C_a = L * 0.4; + double C_b = (1.0 - L) * 0.8; + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + C_0 = sqrt(1.0 / (1.0 / (C_a * C_a) + 1.0 / (C_b * C_b))); + } + + return { C_0, C_mid, C_max }; +} + +RGB okhsl_to_srgb(HSL hsl) +{ + double h = hsl.h; + double s = hsl.s; + double l = hsl.l; + + if (l == 1.0) + { + return { 1, 1, 1 }; + } + + if (l == 0.0) + { + return { 0, 0, 0 }; + } + + double a_ = cos(2 * pi * h); + double b_ = sin(2 * pi * h); + double L = toe_inv(l); + + Cs cs = get_Cs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; + + double mid = 0.8; + double mid_inv = 1.25; + + double C; + double t; + double k_0; + double k_1; + double k_2; + + if (s < mid) + { + t = mid_inv * s; + + k_1 = mid * C_0; + k_2 = (1.0 - k_1 / C_mid); + + C = t * k_1 / (1.0 - k_2 * t); + } + else + { + t = (s - mid)/ (1 - mid); + + k_0 = C_mid; + k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + k_2 = (1.0 - (k_1) / (C_max - C_mid)); + + C = k_0 + t * k_1 / (1.0 - k_2 * t); + } + + RGB rgb = oklab_to_linear_srgb({ L, C * a_, C * b_ }); + return { + srgb_transfer_function(rgb.r), + srgb_transfer_function(rgb.g), + srgb_transfer_function(rgb.b), + }; +} + +HSL srgb_to_okhsl(RGB rgb) +{ + Lab lab = linear_srgb_to_oklab({ + srgb_transfer_function_inv(rgb.r), + srgb_transfer_function_inv(rgb.g), + srgb_transfer_function_inv(rgb.b) + }); + + double C = sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L = lab.L; + double h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / pi; + + Cs cs = get_Cs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; + + // Inverse of the interpolation in okhsl_to_srgb: + + double mid = 0.8; + double mid_inv = 1.25; + + double s; + if (C < C_mid) + { + double k_1 = mid * C_0; + double k_2 = (1.0 - k_1 / C_mid); + + double t = C / (k_1 + k_2 * C); + s = t * mid; + } + else + { + double k_0 = C_mid; + double k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + double k_2 = (1.0 - (k_1) / (C_max - C_mid)); + + double t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + s = mid + (1.0 - mid) * t; + } + + double l = toe(L); + return { h, s, l }; +} + + +RGB okhsv_to_srgb(HSV hsv) +{ + double h = hsv.h; + double s = hsv.s; + double v = hsv.v; + + double a_ = cos(2.0 * pi * h); + double b_ = sin(2.0 * pi * h); + + LC cusp = find_cusp(a_, b_); + ST ST_max = to_ST(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; + + // first we compute L and V as if the gamut is a perfect triangle: + + // L, C when v==1: + double L_v = 1 - s * S_0 / (S_0 + T_max - T_max * k * s); + double C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); + + double L = v * L_v; + double C = v * C_v; + + // then we compensate for both toe and the curved top part of the triangle: + double L_vt = toe_inv(L_v); + double C_vt = C_v * L_vt / L_v; + + double L_new = toe_inv(L); + C = C * L_new / L; + L = L_new; + + RGB rgb_scale = oklab_to_linear_srgb({ L_vt, a_ * C_vt, b_ * C_vt }); + double scale_L = cbrt(1.0 / fmax(fmax(rgb_scale.r, rgb_scale.g), fmax(rgb_scale.b, 0.0))); + + L = L * scale_L; + C = C * scale_L; + + RGB rgb = oklab_to_linear_srgb({ L, C * a_, C * b_ }); + return { + srgb_transfer_function(rgb.r), + srgb_transfer_function(rgb.g), + srgb_transfer_function(rgb.b), + }; +} + +HSV srgb_to_okhsv(RGB rgb) +{ + Lab lab = linear_srgb_to_oklab({ + srgb_transfer_function_inv(rgb.r), + srgb_transfer_function_inv(rgb.g), + srgb_transfer_function_inv(rgb.b) + }); + + double C = sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L = lab.L; + double h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / pi; + + LC cusp = find_cusp(a_, b_); + ST ST_max = to_ST(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; + + // first we find L_v, C_v, L_vt and C_vt + + double t = T_max / (C + L * T_max); + double L_v = t * L; + double C_v = t * C; + + double L_vt = toe_inv(L_v); + double C_vt = C_v * L_vt / L_v; + + // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: + RGB rgb_scale = oklab_to_linear_srgb({ L_vt, a_ * C_vt, b_ * C_vt }); + double scale_L = cbrt(1.0 / fmax(fmax(rgb_scale.r, rgb_scale.g), fmax(rgb_scale.b, 0.0))); + + L = L / scale_L; + //C = C / scale_L; + + //C = C * toe(L) / L; + L = toe(L); + + // we can now compute v and s: + + double v = L / L_v; + double s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); + + return { h, s, v }; +} + +} // namespace ok_color + +#endif // OKCOLOR_H diff --git a/include/hyperion/ColorAdjustment.h b/include/hyperion/ColorAdjustment.h index 128dc828..3e9b8dc8 100644 --- a/include/hyperion/ColorAdjustment.h +++ b/include/hyperion/ColorAdjustment.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef COLORADJUSTMENT_H +#define COLORADJUSTMENT_H // STL includes #include @@ -6,6 +7,7 @@ // Utils includes #include #include +#include class ColorAdjustment { @@ -31,4 +33,7 @@ public: RgbChannelAdjustment _rgbYellowAdjustment; RgbTransform _rgbTransform; + OkhsvTransform _okhsvTransform; }; + +#endif // COLORADJUSTMENT_H diff --git a/include/utils/ColorSys.h b/include/utils/ColorSys.h index 7aa1a065..63fb74cb 100644 --- a/include/utils/ColorSys.h +++ b/include/utils/ColorSys.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef COLORSYS_H +#define COLORSYS_H // STL includes #include @@ -76,4 +77,34 @@ public: /// @param[out] green The green RGB-component /// @param[out] blue The blue RGB-component static void yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t & r, uint8_t & g, uint8_t & b); + + /// + /// Translates an RGB (red, green, blue) color to an Okhsv (hue, saturation, value) color + /// + /// @param[in] red The red RGB-component + /// @param[in] green The green RGB-component + /// @param[in] blue The blue RGB-component + /// @param[out] hue The hue Okhsv-component + /// @param[out] saturation The saturation Okhsv-component + /// @param[out] value The value Okhsv-component + /// + /// @note See https://bottosson.github.io/posts/colorpicker/#okhsv + /// + static void rgb2okhsv(uint8_t red, uint8_t green, uint8_t blue, double & hue, double & saturation, double & value); + + /// + /// Translates an Okhsv (hue, saturation, value) color to an RGB (red, green, blue) color + /// + /// @param[in] hue The hue Okhsv-component + /// @param[in] saturation The saturation Okhsv-component + /// @param[in] value The value Okhsv-component + /// @param[out] red The red RGB-component + /// @param[out] green The green RGB-component + /// @param[out] blue The blue RGB-component + /// + /// @note See https://bottosson.github.io/posts/colorpicker/#okhsv + /// + static void okhsv2rgb(double hue, double saturation, double value, uint8_t & red, uint8_t & green, uint8_t & blue); }; + +#endif // COLORSYS_H diff --git a/include/utils/OkhsvTransform.h b/include/utils/OkhsvTransform.h new file mode 100644 index 00000000..a33d499a --- /dev/null +++ b/include/utils/OkhsvTransform.h @@ -0,0 +1,63 @@ +#ifndef OKHSVTRANSFORM_H +#define OKHSVTRANSFORM_H + +#include + +/// +/// Color transformation to adjust the saturation and value of Okhsv colors +/// +class OkhsvTransform +{ +public: + /// + /// Default constructor + /// + OkhsvTransform(); + + /// + /// Constructor + /// + /// @param saturationGain gain factor to apply to saturation + /// @param brightnessGain gain factor to apply to value/brightness + /// + OkhsvTransform(double saturationGain, double brightnessGain); + + /// @return The current saturation gain value + double getSaturationGain() const; + + /// @param saturationGain new saturation gain + void setSaturationGain(double saturationGain); + + /// @return The current brightness gain value + double getBrightnessGain() const; + + /// @param brightnessGain new value/brightness gain + void setBrightnessGain(double brightnessGain); + + /// @return true if the current gain settings result in an identity transformation + bool isIdentity() const; + + /// + /// Apply the transform the the given RGB values. + /// + /// @param red The red color component + /// @param green The green color component + /// @param blue The blue color component + /// + /// @note The values are updated in place. + /// + void transform(uint8_t & red, uint8_t & green, uint8_t & blue) const; + +private: + /// Sets _isIdentity to true if both gain values are at their neutral setting + void updateIsIdentity(); + + /// Gain settings + double _saturationGain; + double _brightnessGain; + + /// Is true if the gain settings result in an identity transformation + bool _isIdentity; +}; + +#endif // OKHSVTRANSFORM_H diff --git a/include/utils/hyperion.h b/include/utils/hyperion.h index 1a39b65b..5349d38d 100644 --- a/include/utils/hyperion.h +++ b/include/utils/hyperion.h @@ -81,6 +81,14 @@ namespace hyperion { return RgbTransform(gammaR, gammaG, gammaB, backlightThreshold, backlightColored, static_cast(brightness), static_cast(brightnessComp)); } + OkhsvTransform createOkhsvTransform(const QJsonObject& colorConfig) + { + const double saturationGain = colorConfig["saturationGain"].toDouble(1.0); + const double brightnessGain = colorConfig["brightnessGain"].toDouble(1.0); + + return OkhsvTransform(saturationGain, brightnessGain); + } + RgbChannelAdjustment createRgbChannelAdjustment(const QJsonObject& colorConfig, const QString& channelName, int defaultR, int defaultG, int defaultB) { const QJsonArray& channelConfig = colorConfig[channelName].toArray(); @@ -107,6 +115,7 @@ namespace hyperion { adjustment->_rgbMagentaAdjustment = createRgbChannelAdjustment(adjustmentConfig, "magenta", 255, 0,255); adjustment->_rgbYellowAdjustment = createRgbChannelAdjustment(adjustmentConfig, "yellow" , 255,255, 0); adjustment->_rgbTransform = createRgbTransform(adjustmentConfig); + adjustment->_okhsvTransform = createOkhsvTransform(adjustmentConfig); return adjustment; } diff --git a/libsrc/api/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json index c095c09c..b8856ef9 100644 --- a/libsrc/api/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -134,6 +134,18 @@ "required" : false, "minimum" : 0, "maximum" : 100 + }, + "saturationGain" : { + "type" : "number", + "required" : false, + "minimum" : 0.0, + "maximum": 10.0 + }, + "brightnessGain" : { + "type" : "number", + "required" : false, + "minimum" : 0.1, + "maximum": 10.0 } }, "additionalProperties": false diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 411e6398..dfb47440 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -508,6 +508,9 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString adjustment["gammaGreen"] = colorAdjustment->_rgbTransform.getGammaG(); adjustment["gammaBlue"] = colorAdjustment->_rgbTransform.getGammaB(); + adjustment["saturationGain"] = colorAdjustment->_okhsvTransform.getSaturationGain(); + adjustment["brightnessGain"] = colorAdjustment->_okhsvTransform.getBrightnessGain(); + adjustmentArray.append(adjustment); } @@ -700,7 +703,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString transform["id"] = transformId; transform["saturationGain"] = 1.0; - transform["valueGain"] = 1.0; + transform["brightnessGain"] = 1.0; transform["saturationLGain"] = 1.0; transform["luminanceGain"] = 1.0; transform["luminanceMinimum"] = 0.0; @@ -917,6 +920,16 @@ void JsonAPI::handleAdjustmentCommand(const QJsonObject &message, const QString colorAdjustment->_rgbTransform.setBrightnessCompensation(adjustment["brightnessCompensation"].toInt()); } + if (adjustment.contains("saturationGain")) + { + colorAdjustment->_okhsvTransform.setSaturationGain(adjustment["saturationGain"].toDouble()); + } + + if (adjustment.contains("brightnessGain")) + { + colorAdjustment->_okhsvTransform.setBrightnessGain(adjustment["brightnessGain"].toDouble()); + } + // commit the changes _hyperion->adjustmentsUpdated(); diff --git a/libsrc/hyperion/MultiColorAdjustment.cpp b/libsrc/hyperion/MultiColorAdjustment.cpp index 1eca8a1e..dabdac0c 100644 --- a/libsrc/hyperion/MultiColorAdjustment.cpp +++ b/libsrc/hyperion/MultiColorAdjustment.cpp @@ -112,8 +112,14 @@ void MultiColorAdjustment::applyAdjustment(std::vector& ledColors) uint8_t ored = color.red; uint8_t ogreen = color.green; uint8_t oblue = color.blue; - uint8_t B_RGB = 0, B_CMY = 0, B_W = 0; + uint8_t B_RGB = 0; + uint8_t B_CMY = 0; + uint8_t B_W = 0; + if (!adjustment->_okhsvTransform.isIdentity()) + { + adjustment->_okhsvTransform.transform(ored, ogreen, oblue); + } adjustment->_rgbTransform.transform(ored,ogreen,oblue); adjustment->_rgbTransform.getBrightnessComponents(B_RGB, B_CMY, B_W); diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index b899afb3..abe39560 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -158,6 +158,17 @@ "maxItems" : 3, "propertyOrder" : 10 }, + "saturationGain" : + { + "type" : "number", + "title" : "edt_conf_color_saturationGain_title", + "required" : true, + "minimum" : 0.0, + "maximum": 10.0, + "default" : 1.0, + "step" : 0.1, + "propertyOrder" : 11 + }, "backlightThreshold" : { "type" : "integer", @@ -167,7 +178,7 @@ "maximum": 100, "default" : 0, "append" : "edt_append_percent", - "propertyOrder" : 11 + "propertyOrder" : 12 }, "backlightColored" : { @@ -175,7 +186,7 @@ "title" : "edt_conf_color_backlightColored_title", "required" : true, "default" : false, - "propertyOrder" : 12 + "propertyOrder" : 13 }, "brightness" : { @@ -186,7 +197,7 @@ "maximum": 100, "default" : 100, "append" : "edt_append_percent", - "propertyOrder" : 13 + "propertyOrder" : 14 }, "brightnessCompensation" : { @@ -197,7 +208,18 @@ "maximum": 100, "default" : 0, "append" : "edt_append_percent", - "propertyOrder" : 14 + "propertyOrder" : 15 + }, + "brightnessGain" : + { + "type" : "number", + "title" : "edt_conf_color_brightnessGain_title", + "required" : true, + "minimum" : 0.1, + "maximum": 10.0, + "default" : 1.0, + "step" : 0.1, + "propertyOrder" : 16 }, "gammaRed" : { @@ -208,7 +230,7 @@ "maximum": 100.0, "default" : 2.2, "step" : 0.1, - "propertyOrder" : 15 + "propertyOrder" : 17 }, "gammaGreen" : { @@ -219,7 +241,7 @@ "maximum": 100.0, "default" : 2.2, "step" : 0.1, - "propertyOrder" : 16 + "propertyOrder" : 18 }, "gammaBlue" : { @@ -230,7 +252,7 @@ "maximum": 100.0, "default" : 2.2, "step" : 0.1, - "propertyOrder" : 17 + "propertyOrder" : 19 } }, "additionalProperties" : false diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index b56a65b0..e51fcb5d 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -19,6 +19,8 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/utils) FILE ( GLOB_RECURSE Utils_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) +list(APPEND Utils_SOURCES "${CMAKE_SOURCE_DIR}/dependencies/include/oklab/ok_color.h") + if ( NOT ENABLE_PROFILER ) LIST ( REMOVE_ITEM Utils_SOURCES ${CURRENT_HEADER_DIR}/Profiler.h ${CURRENT_SOURCE_DIR}/Profiler.cpp ) endif() diff --git a/libsrc/utils/ColorSys.cpp b/libsrc/utils/ColorSys.cpp index c29683fc..0b3f2d61 100644 --- a/libsrc/utils/ColorSys.cpp +++ b/libsrc/utils/ColorSys.cpp @@ -1,6 +1,7 @@ #include #include +#include inline uint8_t clamp(int x) { @@ -56,3 +57,22 @@ void ColorSys::yuv2rgb(uint8_t y, uint8_t u, uint8_t v, uint8_t &r, uint8_t &g, g = clamp((298 * c - 100 * d - 208 * e + 128) >> 8); b = clamp((298 * c + 516 * d + 128) >> 8); } + +void ColorSys::rgb2okhsv(uint8_t red, uint8_t green, uint8_t blue, double & hue, double & saturation, double & value) +{ + ok_color::HSV color = ok_color::srgb_to_okhsv({ static_cast(red) / 255.F, + static_cast(green) / 255.F, + static_cast(blue) / 255.F + }); + hue = color.h; + saturation = color.s; + value = color.v; +} + +void ColorSys::okhsv2rgb(double hue, double saturation, double value, uint8_t & red, uint8_t & green, uint8_t & blue) +{ + ok_color::RGB color = ok_color::okhsv_to_srgb({ static_cast(hue), static_cast(saturation), static_cast(value) }); + red = static_cast(std::lround(color.r * 255)); + green = static_cast(std::lround(color.g * 255)); + blue = static_cast(std::lround(color.b * 255)); +} diff --git a/libsrc/utils/OkhsvTransform.cpp b/libsrc/utils/OkhsvTransform.cpp new file mode 100644 index 00000000..4cdcbdb9 --- /dev/null +++ b/libsrc/utils/OkhsvTransform.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include + +/// Clamps between 0.f and 1.f. Should generally be branchless +double clamp(double value) +{ + return std::max(0.0, std::min(value, 1.0)); +} + +OkhsvTransform::OkhsvTransform() +{ + _saturationGain = 1.0; + _brightnessGain = 1.0; + _isIdentity = true; +} + +OkhsvTransform::OkhsvTransform(double saturationGain, double brightnessGain) +{ + _saturationGain = saturationGain; + _brightnessGain = brightnessGain; + updateIsIdentity(); +} + +double OkhsvTransform::getSaturationGain() const +{ + return _saturationGain; +} + +void OkhsvTransform::setSaturationGain(double saturationGain) +{ + _saturationGain = saturationGain; + updateIsIdentity(); +} + +double OkhsvTransform::getBrightnessGain() const +{ + return _brightnessGain; +} + +void OkhsvTransform::setBrightnessGain(double brightnessGain) +{ + _brightnessGain= brightnessGain; + updateIsIdentity(); +} + +bool OkhsvTransform::isIdentity() const +{ + return _isIdentity; +} + +void OkhsvTransform::transform(uint8_t & red, uint8_t & green, uint8_t & blue) const +{ + double hue; + double saturation; + double brightness; + ColorSys::rgb2okhsv(red, green, blue, hue, saturation, brightness); + + saturation = clamp(saturation * _saturationGain); + brightness = clamp(brightness * _brightnessGain); + + ColorSys::okhsv2rgb(hue, saturation, brightness, red, green, blue); +} + +void OkhsvTransform::updateIsIdentity() +{ + _isIdentity = _saturationGain == 1.0 && _brightnessGain == 1.0; +}