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 15 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
8 changes: 8 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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 @@ -1590,6 +1594,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
25 changes: 22 additions & 3 deletions src/buffer/out/TextAttribute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,34 @@ std::pair<COLORREF, COLORREF> TextAttribute::CalculateRgbColors(const std::array
const COLORREF defaultBgColor,
const bool reverseScreenMode,
const bool blinkingIsFaint,
const bool boldIsBright) const noexcept
const bool boldIsBright,
const std::optional<std::array<std::array<COLORREF, 18>, 18>>& adjustedForegroundColors) const noexcept
{
auto fg = _foreground.GetColor(colorTable, defaultFgColor, boldIsBright && IsBold());
COLORREF fg;
auto bg = _background.GetColor(colorTable, defaultBgColor);
bool reversed{ false };
if (adjustedForegroundColors.has_value() &&
(_background.IsDefault() || _background.IsLegacy()) &&
(_foreground.IsDefault() || _foreground.IsLegacy()))
{
auto bgIndex = _background.IsDefault() ? 16 : _background.GetIndex();
auto fgIndex = _foreground.IsDefault() ? 17 : _foreground.GetIndex();
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
fg = adjustedForegroundColors.value()[bgIndex][fgIndex];
if (IsReverseVideo() ^ reverseScreenMode)
{
std::swap(fg, bg);
reversed = true;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
}
}
else
{
fg = _foreground.GetColor(colorTable, defaultFgColor, boldIsBright && IsBold());
}
if (IsFaint() || (IsBlinking() && blinkingIsFaint))
{
fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
}
if (IsReverseVideo() ^ reverseScreenMode)
if (IsReverseVideo() ^ reverseScreenMode && !reversed)
{
std::swap(fg, bg);
}
Expand Down
3 changes: 2 additions & 1 deletion src/buffer/out/TextAttribute.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class TextAttribute final
const COLORREF defaultBgColor,
const bool reverseScreenMode = false,
const bool blinkingIsFaint = false,
const bool boldIsBright = true) const noexcept;
const bool boldIsBright = true,
const std::optional<std::array<std::array<COLORREF, 18>, 18>>& adjustedForegroundColors = std::nullopt) const noexcept;

bool IsLeadingByte() const noexcept;
bool IsTrailingByte() const noexcept;
Expand Down
280 changes: 280 additions & 0 deletions src/cascadia/TerminalCore/ColorFix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
#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 <math.h>
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
#include "ColorFix.hpp"

const double gMinThreshold = 12.0;
const double gExpThreshold = 20.0;
const double gLStep = 5.0;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved

constexpr double rad006 = 0.104719755119659774;
constexpr double rad025 = 0.436332312998582394;
constexpr double rad030 = 0.523598775598298873;
constexpr double rad060 = 1.047197551196597746;
constexpr double rad063 = 1.099557428756427633;
constexpr double rad180 = 3.141592653589793238;
constexpr double rad275 = 4.799655442984406336;
constexpr double rad360 = 6.283185307179586476;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved

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;

if (var_R > 0.04045)
var_R = pow(((var_R + 0.055) / 1.055), 2.4);
else
var_R = var_R / 12.92;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
if (var_G > 0.04045)
var_G = pow(((var_G + 0.055) / 1.055), 2.4);
else
var_G = var_G / 12.92;
if (var_B > 0.04045)
var_B = pow(((var_B + 0.055) / 1.055), 2.4);
else
var_B = var_B / 12.92;

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

//Observer. = 2 degrees, Illuminant = D65
double X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805;
double Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722;
double Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved

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

if (var_X > 0.008856)
var_X = pow(var_X, (1. / 3.));
else
var_X = (7.787 * var_X) + (16. / 116.);
if (var_Y > 0.008856)
var_Y = pow(var_Y, (1. / 3.));
else
var_Y = (7.787 * var_Y) + (16. / 116.);
if (var_Z > 0.008856)
var_Z = pow(var_Z, (1. / 3.));
else
var_Z = (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.;

if (pow(var_Y, 3) > 0.008856)
var_Y = pow(var_Y, 3);
else
var_Y = (var_Y - 16. / 116.) / 7.787;
if (pow(var_X, 3) > 0.008856)
var_X = pow(var_X, 3);
else
var_X = (var_X - 16. / 116.) / 7.787;
if (pow(var_Z, 3) > 0.008856)
var_Z = pow(var_Z, 3);
else
var_Z = (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;

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

r = _Clamp(var_R * 255.);
g = _Clamp(var_G * 255.);
b = _Clamp(var_B * 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);
double de1 = GetDeltaE(frontLab, backLab);
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
if (de1 < gMinThreshold)
{
for (int i = 0; i <= 1; i++)
{
double step = (i == 0) ? gLStep : -gLStep;
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
frontLab.L += step;

while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0)))
{
double de2 = GetDeltaE(frontLab, backLab);
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
if (de2 >= gExpThreshold)
{
frontLab._ToRGB();
return frontLab.rgb;
}
frontLab.L += step;
}
}
}
return frontLab.rgb;
}

// Method Description:
// - Clamps the given value to be between 0-255 inclusive
// - Converts the result to BYTE
// Arguments:
// - v: the value to clamp
// Return Value:
// - The clamped value
BYTE ColorFix::_Clamp(double v)
PankajBhojwani marked this conversation as resolved.
Show resolved Hide resolved
{
if (v <= 0)
return 0;
else if (v >= 255)
return 255;
else
return (BYTE)v;
}
Loading