Skip to content

Instantly share code, notes, and snippets.

@Lokno
Last active July 30, 2023 01:02
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lokno/df7c3bfdc9ad32558bb7 to your computer and use it in GitHub Desktop.
Save Lokno/df7c3bfdc9ad32558bb7 to your computer and use it in GitHub Desktop.
Color Blindness Matrices
// JSON of 3x3 matrices which transform RGB colors into colorspace which
// simulate the imparement of various color blindness deficiency.
//
// Used by Coblis: http://www.color-blindness.com/Coblis-color-blindness-simulator/
//
// The original website posting the matrices has been taken down:
// http://www.colorjack.com/labs/colormatrix/
//
// RGB transform matrices generated by Michael of www.colorjack.com
// Which were created using code by Matthew Wickline and the
// Human-Computer Interaction Resource Network ( http://hcirn.com/ )
//
// The original matrices were 5x5, for full homogeneous coordinates of RGBA
// They have been similified here to 3x3 matrices, because the additional
// dimensions were simple identity.
//
// These are very inaccurate so consider other methods. See comments on this gist.
var colorMats = {'Normal':[1,0,0,
0,1,0,
0,0,1],
// Red-Blind
'Protanopia': [0.567,0.433,0.000,
0.558,0.442,0.000,
0.000,0.242,0.758],
// Red-Weak
'Protanomaly': [0.817,0.183,0.000,
0.333,0.667,0.000,
0.000,0.125,0.875],
// Green-Blind
'Deuteranopia': [0.625,0.375,0.000,
0.700,0.300,0.000,
0.000,0.300,0.700],
// Green-Weak
'Deuteranomaly':[0.800,0.200,0.000,
0.258,0.742,0.000,
0.000,0.142,0.858],
// Blue-Blind
'Tritanopia': [0.950,0.050,0.000,
0.000,0.433,0.567,
0.000,0.475,0.525],
// Blue-Weak
'Tritanomaly': [0.967,0.033,0.00,
0.00,0.733,0.267,
0.00,0.183,0.817],
// Monochromacy
'Achromatopsia':[0.299,0.587,0.114,
0.299,0.587,0.114,
0.299,0.587,0.114],
// Blue Cone Monochromacy
'Achromatomaly':[0.618,0.320,0.062,
0.163,0.775,0.062,
0.163,0.320,0.516]};
@Lokno
Copy link
Author

Lokno commented Sep 17, 2014

Coblis performs it's transform server-side, so I can't access it's source. However, Coblis links to a dead page that used to contain these matrices (I recovered the page via the Wayback Machine). I wrote a go-lang script that performs the transforms and compared the results visually to Coblis output on their test image, and they seem to match.

@ProfJski
Copy link

Thanks to much for these! It is surprisingly hard to find the simple RGB-in to RGB-out transforms instead of all the stages to transform RGB into LMS space, etc. The simple transforms make it much easier to include code that helps folks with normal vision accommodate the colorblind.

@Lokno
Copy link
Author

Lokno commented Apr 14, 2021

Thanks to much for these! It is surprisingly hard to find the simple RGB-in to RGB-out transforms instead of all the stages to transform RGB into LMS space, etc. The simple transforms make it much easier to include code that helps folks with normal vision accommodate the colorblind.

I'm glad you've found them useful. It can be a useful tool to look at images of this nature. I also developed an experimental color picker in JS that might interest you, which lets you interact the colors in the XYZ space and see what colors are confused by various types of color blindness. https://www.rabbitfury.com/colorpicker/

@SzieberthAdam
Copy link

SzieberthAdam commented May 11, 2021

Coblis runs on client side: https://www.color-blindness.com/coblis-color-blindness-simulator/
JS file: https://www.color-blindness.com/coblis2/js/coblis-compressed.js
Make the JS human readable with some converter.

You will see that the simulation process is a mess (part of the code):

function blindMK(a, b) {
    var d = .312713,
        e = .329016,
        f = .358271,
        g = a[2],
        h = a[1],
        i = a[0],
        j = powGammaLookup[i],
        k = powGammaLookup[h],
        l = powGammaLookup[g],
        m = .430574 * j + .34155 * k + .178325 * l,
        n = .222015 * j + .706655 * k + .07133 * l,
        o = .020183 * j + .129553 * k + .93918 * l,
        p = m + n + o,
        q = 0,
        r = 0;
    0 != p && (q = m / p, r = n / p);
    var u, s = d * n / e,
        t = f * n / e,
        v = 0;
    u = q < rBlind[b].cpu ? (rBlind[b].cpv - r) / (rBlind[b].cpu - q) : (r - rBlind[b].cpv) / (q - rBlind[b].cpu);
    var w = r - q * u,
        x = (rBlind[b].ayi - w) / (u - rBlind[b].am),
        y = u * x + w,
        z = x * n / y,
        A = n,
        B = (1 - (x + y)) * n / y,
        C = 3.063218 * z - 1.393325 * A - .475802 * B,
        D = -.969243 * z + 1.875966 * A + .041555 * B,
        E = .067871 * z - .228834 * A + 1.069251 * B,
        F = s - z,
        G = t - B;
    dr = 3.063218 * F - 1.393325 * v - .475802 * G, dg = -.969243 * F + 1.875966 * v + .041555 * G, db = .067871 * F - .228834 * v + 1.069251 * G;
    var H = dr ? ((C < 0 ? 0 : 1) - C) / dr : 0,
        I = dg ? ((D < 0 ? 0 : 1) - D) / dg : 0,
        J = db ? ((E < 0 ? 0 : 1) - E) / db : 0,
        K = Math.max(H > 1 || H < 0 ? 0 : H, I > 1 || I < 0 ? 0 : I, J > 1 || J < 0 ? 0 : J);
    return C += K * dr, D += K * dg, E += K * db, [inversePow(C), inversePow(D), inversePow(E)]
}

function inversePow(a) {
    return 255 * (a <= 0 ? 0 : a >= 1 ? 1 : Math.pow(a, 1 / 2.2))
}

function anomylize(a, b) {
    var c = 1.75,
        d = 1 * c + 1;
    return [(c * b[0] + 1 * a[0]) / d, (c * b[1] + 1 * a[1]) / d, (c * b[2] + 1 * a[2]) / d]
}

function monochrome(a) {
    var b = Math.round(.299 * a[0] + .587 * a[1] + .114 * a[2]);
    return [b, b, b]
}
var ColorMatrixMatrixes = {
        Normal: {
            R: [100, 0, 0],
            G: [0, 100, 0],
            B: [0, 0, 100]
        },
        Protanopia: {
            R: [56.667, 43.333, 0],
            G: [55.833, 44.167, 0],
            B: [0, 24.167, 75.833]
        },
        Protanomaly: {
            R: [81.667, 18.333, 0],
            G: [33.333, 66.667, 0],
            B: [0, 12.5, 87.5]
        },
        Deuteranopia: {
            R: [62.5, 37.5, 0],
            G: [70, 30, 0],
            B: [0, 30, 70]
        },
        Deuteranomaly: {
            R: [80, 20, 0],
            G: [25.833, 74.167, 0],
            B: [0, 14.167, 85.833]
        },
        Tritanopia: {
            R: [95, 5, 0],
            G: [0, 43.333, 56.667],
            B: [0, 47.5, 52.5]
        },
        Tritanomaly: {
            R: [96.667, 3.333, 0],
            G: [0, 73.333, 26.667],
            B: [0, 18.333, 81.667]
        },
        Achromatopsia: {
            R: [29.9, 58.7, 11.4],
            G: [29.9, 58.7, 11.4],
            B: [29.9, 58.7, 11.4]
        },
        Achromatomaly: {
            R: [61.8, 32, 6.2],
            G: [16.3, 77.5, 6.2],
            B: [16.3, 32, 51.6]
        }
    },
    colorMatrixFilterFunctions = {};
for (var t in ColorMatrixMatrixes) ColorMatrixMatrixes.hasOwnProperty(t) && (colorMatrixFilterFunctions[t] = matrixFunction(ColorMatrixMatrixes[t]));
var imageCache = {},
    urlCache = {},
    loadingIndicator = document.getElementById("loadingIndicator");
NProgress.configure({
        parent: "#progressBar"
    }),
    function() {
        var b, a = document.querySelectorAll('input[name = "colorblindType"]');
        for (b = 0; b < a.length; b++) a[b].onclick = filterOrImageChanged;
        for (a = document.querySelectorAll('input[name = "lens"]'), b = 0; b < a.length; b++) a[b].onclick = lensChanged
    }();
var fileInput = document.getElementById("fileInput"),
    currentImage;
fileInput.onchange = function(a) {
    var b = a.target || window.event.srcElement,
        c = b.files;
    readFile(c)
};
var canvasDiv = document.getElementById("canvasDiv");
canvasDiv.addEventListener("drop", function(a) {
    a.stopPropagation(), a.preventDefault(), readFile(a.dataTransfer.files)
}, !1), canvasDiv.addEventListener("dragover", function(a) {
    a.stopPropagation(), a.preventDefault(), a.dataTransfer.dropEffect = "copy"
}, !1), canvasDiv.addEventListener("dragleave", function(a) {}, !1), document.onpaste = function(a) {
    for (var b = (a.clipboardData || a.originalEvent.clipboardData).items, c = null, d = 0; d < b.length; d++) 0 === b[d].type.indexOf("image") && (c = b[d].getAsFile());
    null !== c && readFile([c])
};
var rBlind = {
        protan: {
            cpu: .735,
            cpv: .265,
            am: 1.273463,
            ayi: -.073894
        },
        deutan: {
            cpu: 1.14,
            cpv: -.14,
            am: .968437,
            ayi: .003331
        },
        tritan: {
            cpu: .171,
            cpv: -.003,
            am: .062921,
            ayi: .292119
        }
    },
    fBlind = {
        Normal: function(a) {
            return a
        },
        Protanopia: function(a) {
            return blindMK(a, "protan")
        },
        Protanomaly: function(a) {
            return anomylize(a, blindMK(a, "protan"))
        },
        Deuteranopia: function(a) {
            return blindMK(a, "deutan")
        },
        Deuteranomaly: function(a) {
            return anomylize(a, blindMK(a, "deutan"))
        },
        Tritanopia: function(a) {
            return blindMK(a, "tritan")
        },
        Tritanomaly: function(a) {
            return anomylize(a, blindMK(a, "tritan"))
        },
        Achromatopsia: function(a) {
            return monochrome(a)
        },
        Achromatomaly: function(a) {
            return anomylize(a, monochrome(a))
        }
    };

For achromatopsia, Coblis seems not to linearize RGB values before the transformation. That makes the whole simulation questionable for me.

@nburrus
Copy link

nburrus commented Oct 28, 2021

Coblis V2 takes his Javascript code from https://github.com/MaPePeR/jsColorblindSimulator . It now uses the "HCIRN Color Blind Simulation function", which is ok but not as accurate as other methods like:

  • "Computerized simulation of color appearance for dichromats" by Brettel, Viénot and Mollon (1997)
  • "Digital video colourmaps for checking the legibility of displays by dichromats" by Viénot, Brettel and Mollon (1999)
  • Or more recently "A Physiologically-based Model for Simulation of Color Vision Deficiency" by Machado, Oliveira & Fernandes (2009)

If you are interested libDaltonLens is a minimalistic public domain implementation of the first two in C, otherwise the Machado precomputed matrices can be found on their website.

In any case, please don't use the "ColorMatrix" from colorjack, the author himself said that it was a very inaccurate one-night hack and that he should probably take his website down, which apparently he did!

@Lokno
Copy link
Author

Lokno commented Oct 28, 2021

Coblis V2 takes his Javascript code from https://github.com/MaPePeR/jsColorblindSimulator . It now uses the "HCIRN Color Blind Simulation function", which is ok but not as accurate as other methods like:

  • "Computerized simulation of color appearance for dichromats" by Brettel, Viénot and Mollon (1997)
  • "Digital video colourmaps for checking the legibility of displays by dichromats" by Viénot, Brettel and Mollon (1999)
  • Or more recently "A Physiologically-based Model for Simulation of Color Vision Deficiency" by Machado, Oliveira & Fernandes (2009)

If you are interested libDaltonLens is a minimalistic public domain implementation of the first two in C, otherwise the Machado precomputed matrices can be found on their website.

In any case, please don't use the "ColorMatrix" from colorjack, the author himself said that it was a very inaccurate one-night hack and that he should probably take his website down, which apparently he did!

Thank you for your detailed comment. I'll leave this code up as a curiosity, but I've updated the header comment directing interested parties to review the comments.

@mikionakajima
Copy link

Thank you for providing these matrices. It makes me possible to create a html-js for selecting a color blind friendly color set. A power point file should be color blind friendly, yes absolutely. But, we have other problems.

  1. If we know that there are only normal and D-type, shall we consider about T-type?
  2. We have pre-selected color sets those are provided by several professors. Even though a student wants to change, he/she can not. They don't have enough knowledge.

I write a html-js for my studens.

<!DOCTYPE html>
<html lang="jp">
<head>
  <meta charset="utf-8">
  <style>
.content {
  max-width: 1500px;
  padding: 10px;
}
tr {height: 50px;}
td {width:  10%; border: 1px solid black;}
</style>
</head>
<body>
<div class="content">
  <table id="aTable">
    <thead id="aThead"></thead>
    <tbody id="aTbody"></tbody> 
  </table>
I removed all the Japanese sentences. It just explains how to use and something so on. You can see the Japanese version <a href="https://docs.0machi.com/CBF.html">here</a>. I included the link to this page in the HTML.
<script>
const CBOrder = ['Normal','Deuteranopia','Protanopia','Normal','Deuteranomaly','Protanomaly','Tritanopia','Tritanomaly','Achromatopsia','Achromatomaly'];
const InitialColor = ['#FF0000','#FFFF00','#0000FF','#FF0080','#00FF00'];

function CBColor(col, CB)
{
  let colorMats = 
{
// Normal
'Normal':
  [1,0,0,
  0,1,0,
  0,0,1],
// Green-Blind
'Deuteranopia': 
  [0.625,0.375,0.000,
  0.700,0.300,0.000,
  0.000,0.300,0.700],
// Red-Blind
'Protanopia':   
  [0.567,0.433,0.000,
  0.558,0.442,0.000,
  0.000,0.242,0.758],
// Green-Weak
'Deuteranomaly':
  [0.800,0.200,0.000,
  0.258,0.742,0.000,
  0.000,0.142,0.858],
// Red-Weak
'Protanomaly':  
  [0.817,0.183,0.000,
  0.333,0.667,0.000,
  0.000,0.125,0.875],
// Blue-Blind
'Tritanopia':   
  [0.950,0.050,0.000,
  0.000,0.433,0.567,
  0.000,0.475,0.525],
// Blue-Weak
'Tritanomaly':  
  [0.967,0.033,0.00,
  0.00,0.733,0.267,
  0.00,0.183,0.817],
// Monochromacy
'Achromatopsia':
  [0.299,0.587,0.114,
  0.299,0.587,0.114,
  0.299,0.587,0.114],
// Blue Cone Monochromacy
'Achromatomaly':
  [0.618,0.320,0.062,
  0.163,0.775,0.062,
  0.163,0.320,0.516]
};
  let r, g, b, r2, g2, b2;
  let mat = colorMats[CB];
  r = parseInt(col.substring(1,3), 16); 
  g = parseInt(col.substring(3,5), 16); 
  b = parseInt(col.substring(5,7), 16);
  r2 = Math.round(mat[0]*r+mat[1]*g+mat[2]*b);
  g2 = Math.round(mat[3]*r+mat[4]*g+mat[5]*b);
  b2 = Math.round(mat[6]*r+mat[7]*g+mat[8]*b);
  return(`#`+r2.toString(16).padStart(2, '0')+g2.toString(16).padStart(2, '0')+b.toString(16).padStart(2, '0'));
}
function paint1Line(line, col)
{
  let cells = line.cells;
  for (let i=1;i<cells.length;i++)
  {
      cells[i].style.backgroundColor=CBColor(col, CBOrder[i-1]);
  }
}
let thead = document.getElementById('aThead');
let tr = document.createElement('tr');
let td = document.createElement('td');
tr.appendChild(td);
for (const element of CBOrder) {
  let td = document.createElement('td');
  td.innerText = element;
  tr.appendChild(td);
}
thead.appendChild(tr);

let tbody = document.getElementById('aTbody');
for (const aColor of InitialColor) {
  let tr = document.createElement('tr');
  let td = document.createElement('td');
  let input = document.createElement('input');
  input.setAttribute("type", "color");
  td.appendChild(input);
  tr.appendChild(td);
  input.addEventListener("input", onColorBoxClick); 
  for (let j = 0; j < CBOrder.length; j++) {
    let td = document.createElement('td');
    tr.appendChild(td);
  }
  tbody.appendChild(tr);
  input.value = aColor;
  paint1Line(tr, aColor);
}

  function onColorBoxClick(){
  paint1Line(event.target.parentElement.parentElement, event.target.value)
}
</script>
</div>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment