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;
+}