From 046992fed01eb5f9a49907bd83d01a6f67b48fac Mon Sep 17 00:00:00 2001 From: My Name Date: Wed, 24 Jun 2026 13:13:24 +0200 Subject: [PATCH] Add daltonization correction filters (fixes #3) The Protanopia/Deuteranopia/Tritanopia filters were colour-blindness *simulation* matrices: they collapse colours onto the confusion axis to show how a deficient observer sees, rather than helping them distinguish colours. Applying them made things worse for the deutan user in #3, while Microsoft's filter (which daltonizes) scored perfectly. Add proper correction filters. The Magnification API only takes a single linear 5x5 matrix, so a LAB LUT (as in daltoLAB) is not usable directly, but daltonization is linear and collapses to one matrix: Correction = I + (I - S) * E where S is the simulation matrix and E redistributes the lost red-green (or blue-yellow) signal onto channels the observer can still perceive. - Transform.cs: add Add/Subtract/Identity/Daltonize helpers - Matrices.cs: add Protanopia/Deuteranopia/TritanopiaCorrection - Program.cs: register the new filters; extend the menu letter list - greys.csproj: bump net6.0 (EOL) -> net8.0-windows Co-Authored-By: Claude Opus 4.8 (1M context) --- greys/Matrices.cs | 41 +++++++++++++++++++++++++++++++++ greys/Program.cs | 14 +++++++++++- greys/Transform.cs | 56 ++++++++++++++++++++++++++++++++++++++++++++++ greys/greys.csproj | 2 +- 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/greys/Matrices.cs b/greys/Matrices.cs index c7e47f1..904acaa 100644 --- a/greys/Matrices.cs +++ b/greys/Matrices.cs @@ -37,6 +37,17 @@ public static class BuiltinMatrices public static float[,] Tritanopia { get; private set; } public static float[,] Tritanomaly { get; private set; } + /// + /// Daltonization (correction) filters. Unlike the simulation matrices + /// above, these are assistive: they redistribute the colour information + /// a deficient observer loses onto channels they can still perceive, + /// making confusable colours easier to tell apart. This is the same kind + /// of filter as the Windows "colour blindness" accessibility filters. + /// + public static float[,] ProtanopiaCorrection { get; private set; } + public static float[,] DeuteranopiaCorrection { get; private set; } + public static float[,] TritanopiaCorrection { get; private set; } + /// /// simple colors transformations /// @@ -127,6 +138,36 @@ static BuiltinMatrices() { 0, 0, 0, 0, 1 } }; + // Error-shift matrices used to daltonize, in the GDI/Magnification + // row-vector convention (v_out = v_in * M), i.e. the transpose of + // the classic column-vector error matrices. + // + // Red-green deficiencies (protan/deutan): the lost red-green signal + // is pushed onto the green and blue channels. + // column-vector form -> row-vector (stored) form below + // [0 0 0] [0 .7 .7] + // [.7 1 0] [0 1 0] + // [.7 0 1] [0 0 1] + float[,] redGreenErrorShift = new float[,] { + { 0f, 0.7f, 0.7f, 0f, 0f }, + { 0f, 1.0f, 0.0f, 0f, 0f }, + { 0f, 0.0f, 1.0f, 0f, 0f }, + { 0f, 0.0f, 0.0f, 1f, 0f }, + { 0f, 0.0f, 0.0f, 0f, 1f } + }; + // Blue-yellow deficiency (tritan): the lost blue-yellow signal is + // pushed onto the red and green channels. + float[,] blueYellowErrorShift = new float[,] { + { 1.0f, 0.0f, 0f, 0f, 0f }, + { 0.0f, 1.0f, 0f, 0f, 0f }, + { 0.7f, 0.7f, 0f, 0f, 0f }, + { 0.0f, 0.0f, 0f, 1f, 0f }, + { 0.0f, 0.0f, 0f, 0f, 1f } + }; + ProtanopiaCorrection = Transform.Daltonize(Protanopia, redGreenErrorShift); + DeuteranopiaCorrection = Transform.Daltonize(Deuteranopia, redGreenErrorShift); + TritanopiaCorrection = Transform.Daltonize(Tritanopia, blueYellowErrorShift); + Negative = new float[,] { { 0.75f, 0.25f, 0.25f, 0, 0 }, { 0.25f, 1.25f, 0.25f, 0, 0 }, diff --git a/greys/Program.cs b/greys/Program.cs index 4116db9..5238533 100644 --- a/greys/Program.cs +++ b/greys/Program.cs @@ -44,6 +44,10 @@ static void Main(string[] args) "Tritanopia", "Tritanomaly", + "ProtanopiaCorrection", + "DeuteranopiaCorrection", + "TritanopiaCorrection", + "Negative", "GrayScale", "NegativeGrayScale", @@ -66,6 +70,10 @@ static void Main(string[] args) float[] Tritanopia = BuiltinMatrices.Tritanopia.Cast().ToArray(); float[] Tritanomaly = BuiltinMatrices.Tritanomaly.Cast().ToArray(); + float[] ProtanopiaCorrection = BuiltinMatrices.ProtanopiaCorrection.Cast().ToArray(); + float[] DeuteranopiaCorrection = BuiltinMatrices.DeuteranopiaCorrection.Cast().ToArray(); + float[] TritanopiaCorrection = BuiltinMatrices.TritanopiaCorrection.Cast().ToArray(); + float[] Negative = BuiltinMatrices.Negative.Cast().ToArray(); float[] GrayScale = BuiltinMatrices.GrayScale.Cast().ToArray(); float[] NegativeGrayScale = BuiltinMatrices.NegativeGrayScale.Cast().ToArray(); @@ -88,6 +96,10 @@ static void Main(string[] args) Tritanopia, Tritanomaly, + ProtanopiaCorrection, + DeuteranopiaCorrection, + TritanopiaCorrection, + Negative, GrayScale, NegativeGrayScale, @@ -110,7 +122,7 @@ static void Main(string[] args) int optionsCount = colorFiltersStr.Count; int selected = 0; bool done = false; - List alphabet = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; + List alphabet = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v" }; while (!done) diff --git a/greys/Transform.cs b/greys/Transform.cs index 73a2387..53947f6 100644 --- a/greys/Transform.cs +++ b/greys/Transform.cs @@ -43,6 +43,62 @@ public static class Transform return c; } + public static float[,] Add(float[,] a, float[,] b) + { + if (a.GetLength(0) != b.GetLength(0) || a.GetLength(1) != b.GetLength(1)) + { + throw new Exception("Add: dimensions mismatch"); + } + float[,] c = new float[a.GetLength(0), a.GetLength(1)]; + for (int i = 0; i < c.GetLength(0); i++) + for (int j = 0; j < c.GetLength(1); j++) + c[i, j] = a[i, j] + b[i, j]; + return c; + } + + public static float[,] Subtract(float[,] a, float[,] b) + { + if (a.GetLength(0) != b.GetLength(0) || a.GetLength(1) != b.GetLength(1)) + { + throw new Exception("Subtract: dimensions mismatch"); + } + float[,] c = new float[a.GetLength(0), a.GetLength(1)]; + for (int i = 0; i < c.GetLength(0); i++) + for (int j = 0; j < c.GetLength(1); j++) + c[i, j] = a[i, j] - b[i, j]; + return c; + } + + public static float[,] Identity(int size) + { + float[,] m = new float[size, size]; + for (int i = 0; i < size; i++) + m[i, i] = 1f; + return m; + } + + /// + /// Builds a daltonization (correction) matrix from a colour-blindness + /// *simulation* matrix. Where a simulation matrix collapses colours onto + /// the confusion axis (showing what the user cannot distinguish), the + /// correction matrix takes the information lost by the deficiency and + /// redistributes it onto channels the user *can* still perceive, making + /// confusable colours easier to tell apart. + /// + /// Using the GDI/Magnification row-vector convention (v_out = v_in * M), + /// the daltonized correction is: + /// Correction = I + (I - S) * E + /// where S is the simulation matrix and E is the error-shift matrix. + /// + public static float[,] Daltonize(float[,] simulation, float[,] errorShift) + { + int size = simulation.GetLength(0); + float[,] identity = Identity(size); + float[,] lost = Subtract(identity, simulation); // information the deficiency removes + float[,] redistributed = Multiply(lost, errorShift); + return Add(identity, redistributed); + } + public static List Interpolate(float[,] A, float[,] B) { const int STEPS = 10; diff --git a/greys/greys.csproj b/greys/greys.csproj index e48bed8..c8ca362 100644 --- a/greys/greys.csproj +++ b/greys/greys.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0-windows enable enable