mirror of
				https://github.com/hyperion-project/hyperion.ng.git
				synced 2025-03-01 10:33:28 +00:00 
			
		
		
		
	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 <lordgrey.emmel@gmail.com>
This commit is contained in:
		| @@ -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. | ||||
|   | ||||
							
								
								
									
										16
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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 <br> `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 <br> `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) | ||||
|  | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -56,10 +56,20 @@ $(document).ready(function () { | ||||
|         }); | ||||
|       } | ||||
|       else { | ||||
|         if (sColor[key].key == "brightness" || sColor[key].key == "brightnessCompensation" || sColor[key].key == "backlightThreshold") | ||||
|           property = '<div class="input-group"><input id="cr_' + sColor[key].key + '" type="number" class="form-control" min="0" max="100" step="10" value="' + value + '"/><span class="input-group-addon">' + $.i18n("edt_append_percent") + '</span></div>'; | ||||
|         else | ||||
|         if (sColor[key].key == "brightness" || | ||||
|           sColor[key].key == "brightnessCompensation" || | ||||
|           sColor[key].key == "backlightThreshold" || | ||||
|           sColor[key].key == "saturationGain" || | ||||
|           sColor[key].key == "brightnessGain") { | ||||
|  | ||||
|           property = '<input id="cr_' + sColor[key].key + '" type="number" class="form-control" min="' + sColor[key].minimum + '" max="' + sColor[key].maximum + '" step="' + sColor[key].step + '" value="' + value + '"/>'; | ||||
|           if (sColor[key].append === "edt_append_percent") { | ||||
|             property = '<div class="input-group">' + property + '<span class="input-group-addon">' + $.i18n("edt_append_percent") + '</span></div>'; | ||||
|           } | ||||
|         } | ||||
|         else { | ||||
|           property = '<input id="cr_' + sColor[key].key + '" type="number" class="form-control" min="0.1" max="4.0" step="0.1" value="' + value + '"/>'; | ||||
|         } | ||||
|  | ||||
|         $('.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') + '  ' + '<div style="width:18px; height:18px; border-radius:20px; margin-bottom:-4px; border:1px grey solid; background-color: rgb(' + value + '); display:inline-block" title="RGB: (' + value + ')"></div>'; | ||||
|           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 += '<br/><span style="font-size:80%; color:grey;">' + $.i18n('remote_input_duration') + ' ' + duration.toFixed(0) + $.i18n('edt_append_s') + '</span>'; | ||||
|  | ||||
| @@ -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(); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,9 @@ | ||||
| 				"backlightThreshold"  : 0, | ||||
| 				"backlightColored"  : false, | ||||
| 				"brightness" : 100, | ||||
| 				"brightnessCompensation" : 100 | ||||
| 				"brightnessCompensation" : 100, | ||||
| 				"saturationGain" : 1.0, | ||||
| 				"brightnessGain" : 1.0 | ||||
| 			} | ||||
| 		] | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										708
									
								
								dependencies/include/oklab/ok_color.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										708
									
								
								dependencies/include/oklab/ok_color.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 <cfloat> | ||||
| #include <cmath> | ||||
|  | ||||
| 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<double>(0.0 < x) - static_cast<double>(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 | ||||
| @@ -1,4 +1,5 @@ | ||||
| #pragma once | ||||
| #ifndef COLORADJUSTMENT_H | ||||
| #define COLORADJUSTMENT_H | ||||
|  | ||||
| // STL includes | ||||
| #include <QString> | ||||
| @@ -6,6 +7,7 @@ | ||||
| // Utils includes | ||||
| #include <utils/RgbChannelAdjustment.h> | ||||
| #include <utils/RgbTransform.h> | ||||
| #include <utils/OkhsvTransform.h> | ||||
|  | ||||
| class ColorAdjustment | ||||
| { | ||||
| @@ -31,4 +33,7 @@ public: | ||||
| 	RgbChannelAdjustment _rgbYellowAdjustment; | ||||
|  | ||||
| 	RgbTransform _rgbTransform; | ||||
| 	OkhsvTransform _okhsvTransform; | ||||
| }; | ||||
|  | ||||
| #endif // COLORADJUSTMENT_H | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #pragma once | ||||
| #ifndef COLORSYS_H | ||||
| #define COLORSYS_H | ||||
|  | ||||
| // STL includes | ||||
| #include <cstdint> | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										63
									
								
								include/utils/OkhsvTransform.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								include/utils/OkhsvTransform.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| #ifndef OKHSVTRANSFORM_H | ||||
| #define OKHSVTRANSFORM_H | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| /// | ||||
| /// 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 | ||||
| @@ -81,6 +81,14 @@ namespace hyperion { | ||||
| 		return RgbTransform(gammaR, gammaG, gammaB, backlightThreshold, backlightColored, static_cast<uint8_t>(brightness), static_cast<uint8_t>(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; | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -112,8 +112,14 @@ void MultiColorAdjustment::applyAdjustment(std::vector<ColorRgb>& 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); | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #include <utils/ColorSys.h> | ||||
|  | ||||
| #include <QColor> | ||||
| #include <oklab/ok_color.h> | ||||
|  | ||||
| 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<float>(red)   / 255.F, | ||||
| 													static_cast<float>(green) / 255.F, | ||||
| 													static_cast<float>(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<float>(hue), static_cast<float>(saturation), static_cast<float>(value) }); | ||||
| 	red = static_cast<uint8_t>(std::lround(color.r * 255)); | ||||
| 	green = static_cast<uint8_t>(std::lround(color.g * 255)); | ||||
| 	blue = static_cast<uint8_t>(std::lround(color.b * 255)); | ||||
| } | ||||
|   | ||||
							
								
								
									
										69
									
								
								libsrc/utils/OkhsvTransform.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								libsrc/utils/OkhsvTransform.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| #include <algorithm> | ||||
|  | ||||
| #include <utils/OkhsvTransform.h> | ||||
| #include <utils/ColorSys.h> | ||||
|  | ||||
| /// 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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user