Skip to content

Commit

Permalink
Implement the Delta E algorithm to improve color perception (#11095)
Browse files Browse the repository at this point in the history
- Implements the Delta E algorithm
- Uses the Delta E algorithm to precalculate adjusted foreground values based on possible foreground/background color pairs in the color table
- Adds a setting to use the adjusted foreground values when applicable

## PR Checklist
* [x] Closes #2638
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [ ] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [ ] Schema updated.
* [x] I work here

## Validation Steps Performed
Before:
<img width="759" alt="color before" src="https://user-images.githubusercontent.com/26824113/131576768-b3b9eebd-5933-45de-8da8-88a985070312.png">

After (note dark blue):
<img width="760" alt="color after" src="https://user-images.githubusercontent.com/26824113/133158807-4e63198f-8a49-4d03-914e-55a5ad57d725.png">
  • Loading branch information
PankajBhojwani authored Oct 7, 2021
1 parent bc4f410 commit dd5dbb2
Show file tree
Hide file tree
Showing 23 changed files with 453 additions and 9 deletions.
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"
},
"adjustIndistinguishableColors": {
"description": "When set to true, we will (when necessary) adjust the foreground color to make it more visible, based on the background color.",
"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 @@ -1973,6 +1977,10 @@
}
]
},
"adjustIndistinguishableColors": {
"description": "When set to true, we will (when necessary) adjust 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
228 changes: 228 additions & 0 deletions src/cascadia/TerminalCore/ColorFix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// 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"

#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)
{
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 AdjustIndistinguishableColors;
};
}
8 changes: 7 additions & 1 deletion src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Terminal::Terminal() :
_taskbarState{ 0 },
_taskbarProgress{ 0 },
_trimBlockSelection{ false },
_intenseIsBright{ true }
_intenseIsBright{ true },
_adjustIndistinguishableColors{ true }
{
auto dispatch = std::make_unique<TerminalDispatch>(*this);
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
Expand Down Expand Up @@ -175,11 +176,16 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
_defaultBg = newBackgroundColor.with_alpha(0);
_defaultFg = appearance.DefaultForeground();
_intenseIsBright = appearance.IntenseIsBright();
_adjustIndistinguishableColors = appearance.AdjustIndistinguishableColors();

for (int i = 0; i < 16; i++)
{
_colorTable.at(i) = til::color{ appearance.GetColorTableEntry(i) };
}
if (_adjustIndistinguishableColors)
{
_MakeAdjustedColorArray();
}

CursorType cursorShape = CursorType::VerticalBar;
switch (appearance.CursorShape())
Expand Down
Loading

0 comments on commit dd5dbb2

Please sign in to comment.