Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the Delta E algorithm to improve color perception #11095

Merged
27 commits merged into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/actions/spelling/allow/math.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
atan
CPrime
HBar
HPrime
isnan
LPrime
LStep
powf
RSub
sqrtf
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect/web.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
http
www
easyrgb
php
ecma
rapidtables
WCAG
Expand Down
37 changes: 37 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,43 @@ DEALINGS IN THE SOFTWARE.

```

## ConEmu
**Source**: [https://github.com/Maximus5/ConEmu](https://github.com/Maximus5/ConEmu)

### License

```
BSD 3-Clause License

Copyright (c) 2009-2017, Maximus5 <ConEmu.Maximus5@gmail.com>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

# Microsoft Open Source

This product also incorporates source code from other Microsoft open source projects, all licensed under the MIT license.
Expand Down
8 changes: 8 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@
],
"type": "string"
},
"perceptualColorNudging": {
"description": "When set to true, we will (when necessary) 'nudge' the foreground color to make it more visible, based on the background color.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"adjust" perhaps instead of "nudge"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"increasedColorContrast" ?

Copy link
Contributor Author

@PankajBhojwani PankajBhojwani Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm does that sound very similar to high contrast mode? Also I wonder if people will confuse that to mean 'this feature always increases contrast' instead of 'this feature will move the foreground slightly only if the foreground is very similar to the background'

"type": "boolean"
},
"experimental.retroTerminalEffect": {
"description": "When set to true, enable retro terminal effects when unfocused. This is an experimental feature, and its continued existence is not guaranteed.",
"type": "boolean"
Expand Down Expand Up @@ -1972,6 +1976,10 @@
}
]
},
"perceptualColorNudging": {
"description": "When set to true, we will (when necessary) 'nudge' the foreground color to make it more visible, based on the background color.",
"type": "boolean"
},
"scrollbarState": {
"default": "visible",
"description": "Defines the visibility of the scrollbar.",
Expand Down
233 changes: 233 additions & 0 deletions src/cascadia/TerminalCore/ColorFix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// A lot of code was taken from
// https://github.com/Maximus5/ConEmu/blob/master/src/ConEmu/ColorFix.cpp
// and then adjusted to fit our style guidelines

#include "pch.h"
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved

PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
#include <Windows.h>
#include "ColorFix.hpp"

static constexpr double gMinThreshold = 12.0;
static constexpr double gExpThreshold = 20.0;
static constexpr double gLStep = 5.0;

static constexpr double rad006 = 0.104719755119659774;
static constexpr double rad025 = 0.436332312998582394;
static constexpr double rad030 = 0.523598775598298873;
static constexpr double rad060 = 1.047197551196597746;
static constexpr double rad063 = 1.099557428756427633;
static constexpr double rad180 = 3.141592653589793238;
static constexpr double rad275 = 4.799655442984406336;
static constexpr double rad360 = 6.283185307179586476;

ColorFix::ColorFix(COLORREF color)
{
rgb = color;
_ToLab();
}

// Method Description:
// - Helper function to calculate HPrime
double ColorFix::_GetHPrimeFn(double x, double y)
{
if (x == 0 && y == 0)
{
return 0;
}

const auto hueAngle = atan2(x, y);
return hueAngle >= 0 ? hueAngle : hueAngle + rad360;
}

// Method Description:
// - Given 2 colors, computes the DeltaE value between them
// Arguments:
// - x1: the first color
// - x2: the second color
// Return Value:
// - The DeltaE value between x1 and x2
double ColorFix::_GetDeltaE(ColorFix x1, ColorFix x2)
{
constexpr double kSubL = 1;
constexpr double kSubC = 1;
constexpr double kSubH = 1;

// Delta L Prime
const double deltaLPrime = x2.L - x1.L;

// L Bar
const double lBar = (x1.L + x2.L) / 2;

// C1 & C2
const double c1 = sqrt(pow(x1.A, 2) + pow(x1.B, 2));
const double c2 = sqrt(pow(x2.A, 2) + pow(x2.B, 2));

// C Bar
const double cBar = (c1 + c2) / 2;

// A Prime 1
const double aPrime1 = x1.A + (x1.A / 2) * (1 - sqrt(pow(cBar, 7) / (pow(cBar, 7) + pow(25.0, 7))));

// A Prime 2
const double aPrime2 = x2.A + (x2.A / 2) * (1 - sqrt(pow(cBar, 7) / (pow(cBar, 7) + pow(25.0, 7))));

// C Prime 1
const double cPrime1 = sqrt(pow(aPrime1, 2) + pow(x1.B, 2));

// C Prime 2
const double cPrime2 = sqrt(pow(aPrime2, 2) + pow(x2.B, 2));

// C Bar Prime
const double cBarPrime = (cPrime1 + cPrime2) / 2;

// Delta C Prime
const double deltaCPrime = cPrime2 - cPrime1;

// S sub L
const double sSubL = 1 + ((0.015 * pow(lBar - 50, 2)) / sqrt(20 + pow(lBar - 50, 2)));

// S sub C
const double sSubC = 1 + 0.045 * cBarPrime;

// h Prime 1
const double hPrime1 = _GetHPrimeFn(x1.B, aPrime1);

// h Prime 2
const double hPrime2 = _GetHPrimeFn(x2.B, aPrime2);

// Delta H Prime
const double deltaHPrime = 0 == c1 || 0 == c2 ? 0 : 2 * sqrt(cPrime1 * cPrime2) * sin(abs(hPrime1 - hPrime2) <= rad180 ? hPrime2 - hPrime1 : (hPrime2 <= hPrime1 ? hPrime2 - hPrime1 + rad360 : hPrime2 - hPrime1 - rad360) / 2);

// H Bar Prime
const double hBarPrime = (abs(hPrime1 - hPrime2) > rad180) ? (hPrime1 + hPrime2 + rad360) / 2 : (hPrime1 + hPrime2) / 2;

// T
const double t = 1 - 0.17 * cos(hBarPrime - rad030) + 0.24 * cos(2 * hBarPrime) + 0.32 * cos(3 * hBarPrime + rad006) - 0.20 * cos(4 * hBarPrime - rad063);

// S sub H
const double sSubH = 1 + 0.015 * cBarPrime * t;

// R sub T
const double rSubT = -2 * sqrt(pow(cBarPrime, 7) / (pow(cBarPrime, 7) + pow(25.0, 7))) * sin(rad060 * exp(-pow((hBarPrime - rad275) / rad025, 2)));

// Put it all together!
const double lightness = deltaLPrime / (kSubL * sSubL);
const double chroma = deltaCPrime / (kSubC * sSubC);
const double hue = deltaHPrime / (kSubH * sSubH);

return sqrt(pow(lightness, 2) + pow(chroma, 2) + pow(hue, 2) + rSubT * chroma * hue);
}

// Method Description:
// - Populates our L, A, B values, based on our r, g, b values
// - Converts a color in rgb format to a color in lab format
// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1
void ColorFix::_ToLab()
{
double var_R = r / 255.0;
double var_G = g / 255.0;
double var_B = b / 255.0;

var_R = var_R > 0.04045 ? pow(((var_R + 0.055) / 1.055), 2.4) : var_R / 12.92;
var_G = var_G > 0.04045 ? pow(((var_G + 0.055) / 1.055), 2.4) : var_G / 12.92;
var_B = var_B > 0.04045 ? pow(((var_B + 0.055) / 1.055), 2.4) : var_B / 12.92;

var_R = var_R * 100.;
var_G = var_G * 100.;
var_B = var_B * 100.;

//Observer. = 2 degrees, Illuminant = D65
const double X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805;
const double Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722;
const double Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505;

double var_X = X / 95.047; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65)
double var_Y = Y / 100.000; //ref_Y = 100.000
double var_Z = Z / 108.883; //ref_Z = 108.883

var_X = var_X > 0.008856 ? pow(var_X, (1. / 3.)) : (7.787 * var_X) + (16. / 116.);
var_Y = var_Y > 0.008856 ? pow(var_Y, (1. / 3.)) : (7.787 * var_Y) + (16. / 116.);
var_Z = var_Z > 0.008856 ? pow(var_Z, (1. / 3.)) : (7.787 * var_Z) + (16. / 116.);

L = (116. * var_Y) - 16.;
A = 500. * (var_X - var_Y);
B = 200. * (var_Y - var_Z);
}

// Method Description:
// - Populates our r, g, b values, based on our L, A, B values
// - Converts a color in lab format to a color in rgb format
// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1
void ColorFix::_ToRGB()
{
double var_Y = (L + 16.) / 116.;
double var_X = A / 500. + var_Y;
double var_Z = var_Y - B / 200.;

var_Y = (pow(var_Y, 3) > 0.008856) ? pow(var_Y, 3) : (var_Y - 16. / 116.) / 7.787;
var_X = (pow(var_X, 3) > 0.008856) ? pow(var_X, 3) : (var_X - 16. / 116.) / 7.787;
var_Z = (pow(var_Z, 3) > 0.008856) ? pow(var_Z, 3) : (var_Z - 16. / 116.) / 7.787;

double X = 95.047 * var_X; //ref_X = 95.047 (Observer= 2 degrees, Illuminant= D65)
double Y = 100.000 * var_Y; //ref_Y = 100.000
double Z = 108.883 * var_Z; //ref_Z = 108.883

var_X = X / 100.; //X from 0 to 95.047 (Observer = 2 degrees, Illuminant = D65)
var_Y = Y / 100.; //Y from 0 to 100.000
var_Z = Z / 100.; //Z from 0 to 108.883

double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986;
double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415;
double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570;

var_R = var_R > 0.0031308 ? 1.055 * pow(var_R, (1 / 2.4)) - 0.055 : var_R = 12.92 * var_R;
var_G = var_G > 0.0031308 ? 1.055 * pow(var_G, (1 / 2.4)) - 0.055 : var_G = 12.92 * var_G;
var_B = var_B > 0.0031308 ? 1.055 * pow(var_B, (1 / 2.4)) - 0.055 : var_B = 12.92 * var_B;

r = (BYTE)std::clamp(var_R * 255., 0., 255.);
g = (BYTE)std::clamp(var_G * 255., 0., 255.);
b = (BYTE)std::clamp(var_B * 255., 0., 255.);
}

// Method Description:
// - Given foreground and background colors, change the foreground color to
// make it more perceivable if necessary
// - Arguments:
// - fg: the foreground color
// - bg: the background color
// - Return Value:
// - The foreground color after performing any necessary changes to make it more perceivable
COLORREF ColorFix::GetPerceivableColor(COLORREF fg, COLORREF bg)
{
// If the colors are the same, don't do any adjusting
if (fg == bg)
{
return fg;
}
ColorFix backLab(bg);
ColorFix frontLab(fg);
const double de1 = _GetDeltaE(frontLab, backLab);
if (de1 < gMinThreshold)
{
for (int i = 0; i <= 1; i++)
{
const double step = (i == 0) ? gLStep : -gLStep;
frontLab.L += step;

while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0)))
{
const double de2 = _GetDeltaE(frontLab, backLab);
if (de2 >= gExpThreshold)
{
frontLab._ToRGB();
return frontLab.rgb;
}
frontLab.L += step;
}
}
}
return frontLab.rgb;
}
49 changes: 49 additions & 0 deletions src/cascadia/TerminalCore/ColorFix.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.

Module Name:
- ColorFix

Abstract:
- Implementation of perceptual color nudging, which allows the Terminal
to slightly shift the foreground color to make it more perceivable on
the current background (for cases where the foreground is very close
to being imperceivable on the background).

Author(s):
- Pankaj Bhojwani - Sep 2021

--*/

#pragma once

struct ColorFix
{
public:
ColorFix(COLORREF color);

static COLORREF GetPerceivableColor(COLORREF fg, COLORREF bg);

// RGB
union
{
struct
{
BYTE r, g, b, dummy;
};
COLORREF rgb;
};

// Lab
struct
{
double L, A, B;
};

private:
static double _GetHPrimeFn(double x, double y);
static double _GetDeltaE(ColorFix x1, ColorFix x2);
void _ToLab();
void _ToRGB();
};
1 change: 1 addition & 0 deletions src/cascadia/TerminalCore/ICoreAppearance.idl
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@ namespace Microsoft.Terminal.Core
CursorStyle CursorShape;
UInt32 CursorHeight;
Boolean IntenseIsBright;
Boolean PerceptualColorNudging;
};
}
Loading