Skip to content

Instantly share code, notes, and snippets.

@Lokno
Last active Oct 28, 2021
Embed
What would you like to do?
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

ProfJski 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.

@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.

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