Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca - contact me Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca on Twitter Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca - Lumondo Photography Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca - Pi Art Martin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca - Hilbertonians - Creatures on the Hilbert CurveMartin Krzywinski / Canada's Michael Smith Genome Sciences Centre / mkweb.bcgsc.ca - Pi Day 2020 - Piku
Trance opera—Spente le Stellebe dramaticmore quotes

PNAS Cover: Earth BioGenome Project


visualization + design
If you are interested in color, explore my other color tools, Brewer palettes resources, color blindness palettes and math and an exhausting list of 10,000 color names for all those times you couldn't distinguish between tan hide, sea buckthorn, orange peel, west side, sunshade, california and pizzaz.

Designing for Color Blindess

Color choices and transformations for deuteranopia and other afflictions

Here, I help you understand color blindness and describe a process by which you can make good color choices when designing for accessibility.

The opposite of colorblindness is seeing all the colors and I can help you find 1,000 (or more) maximally distinct colors.

You can also delve into the mathematics behind the color blindness simulations and learn about copunctal points (the invisible color!) and lines of confusion.

color blindness R code

R code for converting an RGB color for color blindness. For details see the math tab and the resources section for background reading.

---
title: 'RGB color correction for color blindess: protanopia, deuteranopia, tritanopia'
author: 'Martin Krzywinski'
web: http://mkweb.bcgsc.ca/colorblind
---

```{r}
gamma = 2.4

###############################################
# Linear RGB to XYZ
# https://en.wikipedia.org/wiki/SRGB
XYZ = matrix(c(0.4124564, 0.3575761, 0.1804375,
               0.2126729, 0.7151522, 0.0721750,
               0.0193339, 0.1191920, 0.9503041),
               byrow=TRUE,nrow=3)

SA = matrix(c(0.2126,0.7152,0.0722,
              0.2126,0.7152,0.0722,
              0.2126,0.7152,0.0722),byrow=TRUE,nrow=3)

###############################################
# XYZ to LMS, normalized to D65
# https://en.wikipedia.org/wiki/LMS_color_space
# Hunt, Normalized to D65
LMSD65 = matrix(c( 0.4002, 0.7076, -0.0808,
                   -0.2263, 1.1653,  0.0457,
                    0     , 0     ,  0.9182),
                   byrow=TRUE,nrow=3)
# Hunt, equal-energy illuminants
LMSEQ = matrix(c( 0.38971, 0.68898,-0.07868,
                 -0.22981, 1.18340, 0.04641,
                  0      , 0      , 1      ),
                byrow=TRUE,nrow=3)
# CIECAM97
SMSCAM97 = matrix(c(  0.8951,  0.2664, -0.1614,
                     -0.7502,  1.7135,  0.0367,
                      0.0389, -0.0685,  1.0296),
                  byrow=TRUE,nrow=3)
# CIECAM02
LMSCAM02 = matrix(c( 0.7328, 0.4296, -0.1624,
                    -0.7036, 1.6975,  0.0061,
                     0.0030, 0.0136,  0.9834),
                  byrow=TRUE,nrow=3)

###############################################
# Determine the color blindness correction in LMS space
# under the condition that the correction does not
# alter the appearance of white as well as 
# blue (for protanopia/deuteranopia) or red (for tritanopia).
# For achromatopsia, greyscale conversion is applied
# to the linear RGB values.
getcorrection = function(LMS,type="p",g=gamma) {
  red = matrix(c(255,0,0),nrow=3)
  blue = matrix(c(0,0,255),nrow=3)
  white = matrix(c(255,255,255),nrow=3)
  LMSr = LMS %*% XYZ %*% apply(red,1:2,linearize,g)
  LMSb = LMS %*% XYZ %*% apply(blue,1:2,linearize,g)
  LMSw = LMS %*% XYZ %*% apply(white,1:2,linearize,g)
  if(type == "p") {
    x = matrix(c(LMSb[2,1],LMSb[3,1],
                  LMSw[2,1],LMSw[3,1]),byrow=T,nrow=2)
    y = matrix(c(LMSb[1,1],LMSw[1,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(0,ab[1,1],ab[2,1],0,1,0,0,0,1),byrow=T,nrow=3)
  } else if (type == "d") {
    x = matrix(c(LMSb[1,1],LMSb[3,1],
                  LMSw[1,1],LMSw[3,1]),byrow=T,nrow=2)
    y = matrix(c(LMSb[2,1],LMSw[2,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(1,0,0,ab[1,1],0,ab[2,1],0,0,1),byrow=T,nrow=3)
  } else if (type == "t") {
    x = matrix(c(LMSr[1,1],LMSr[2,1],
                  LMSw[1,1],LMSw[2,1]),byrow=T,nrow=2)
    y = matrix(c(LMSr[3,1],LMSw[3,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(1,0,0,0,1,0,ab[1,1],ab[2,1],0),byrow=T,nrow=3)
  } else if (type == "a" | type == "g") {
    C = matrix(c(0.2126,0.7152,0.0722,
                 0.2126,0.7152,0.0722,
                 0.2126,0.7152,0.0722),byrow=TRUE,nrow=3)
  }
  return(C)
}

# rgb is a column vector
convertcolor = function(rgb,LMS=LMSD65,type="d",g=gamma) {
  C = getcorrection(LMS,type)
  if(type == "a" | type == "g") {
    T = SA
  } else {
    M = LMS %*% XYZ
    Minv = solve(M)
    T = Minv %*% C %*% M
  }
  print(T)
  rgb_converted = T %*% apply(rgb,1:2,linearize,g)
  return(apply(rgb_converted,1:2,delinearize,g))
}

# This function implements the method by Vienot, Brettel, Mollon 1999.
# The approach is the same, just the values are different.
# http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf
convertcolor2 = function(rgb,type="d",g=2.2) {
  xyz = matrix(c(40.9568, 35.5041, 17.9167,
                 21.3389, 70.6743, 7.98680,
                 1.86297, 11.4620, 91.2367),byrow=T,nrow=3)
  lms = matrix(c(0.15514, 0.54312, -0.03286,
                 -0.15514, 0.45684,0.03286,
                 0,0,0.01608),byrow=T,nrow=3)
  rgb = (rgb/255)**g
  if(type=="p") {
    S = matrix(c(0,2.02344,-2.52581,0,1,0,0,0,1),byrow=T,nrow=3)
    rgb = 0.992052*rgb+0.003974
  } else if(type=="d") {
    S = matrix(c(1,0,0,0.494207,0,1.24827,0,0,1),byrow=T,nrow=3)
    rgb = 0.957237*rgb+0.0213814
  } else {
    stop("Only type p,d defined for this function.")
  }
  M = lms %*% xyz
  T = solve(M) %*% S %*% M
  print(T)
  rgb = T %*% rgb
  rgb = 255*rgb**(1/g)
  return(rgb)
}

###############################################
# RGB to Lab
rgb2lab = function(rgb,g=gamma) {
  rgb = apply(rgb,1:2,linearize,g)
  xyz = XYZ %*% rgb
  delta = 6/29
  xyz = xyz / (c(95.0489,100,108.8840)/100)
  f = function(t) {
    if(t > delta**3) {
      return(t**(1/3))
    } else {
      return (t/(3*delta**2) + 4/29)
    }
  }
  L = 116*f(xyz[2]) - 16
  a = 500*(f(xyz[1]) - f(xyz[2]))
  b = 200*(f(xyz[2]) - f(xyz[3]))
  return(matrix(c(L,a,b),nrow=3))
}

# CIE76 (https://en.wikipedia.org/wiki/Color_difference)
deltaE = function(rgb1,rgb2) {
  lab1 = rgb2lab(rgb1)
  lab2 = rgb2lab(rgb2)
  return(sqrt(sum((lab1-lab2)**2)))
}

clip = function(v) {
  return(max(min(v,1),0))
}

###############################################
# RGB to/from linear RGB
#https://en.wikipedia.org/wiki/SRGB
linearize = function(v,g=gamma) {
  if(v <= 0.04045) {
    return(v/255/12.92)
  } else {
    return(((v/255 + 0.055)/1.055)**g)
  }
}

delinearize = function(v,g=gamma) {
  if(v <= 0.003130805) {
    return(255*12.92*clip(v))
  } else {
    return(255*clip(1.055*(clip(v)**(1/g))-0.055))
  }
}
pretty = function(x) {
  noquote(formatC(x,digits=10,format="f",width=9))
}

# a dark red
rgb1 = matrix(c(0,209,253),nrow=3)
# dark green
rgb2 = matrix(c(60,135,0),nrow=3)
# simulate deuteranopia
convertcolor(rgb1,type="d")
convertcolor(rgb2,type="d")
# get color distance before and after simulation
deltaE(rgb1,rgb2)
deltaE(convertcolor(rgb1,type="d"),convertcolor(rgb2,type="d"))
# transformation matrices for each color blindness type
M = LMSD65 %*% XYZ
pretty(solve(M) %*% getcorrection(LMSD65,"p") %*% M)
pretty(solve(M) %*% getcorrection(LMSD65,"d") %*% M)
pretty(solve(M) %*% getcorrection(LMSD65,"t") %*% M)
pretty(SA)
# method by Vienot, Brettel, Mollon, 1999
convertcolor2(rgb1,type="d",g=2.2)
convertcolor2(rgb2,type="d",g=2.2)
```

# a dark red
rgb1 = matrix(c(225,0,30),nrow=3)

# dark green
rgb2 = matrix(c(60,135,0),nrow=3)

# simulate deuteranopia
convertcolor(rgb1,type="d")
         [,1]
[1,] 136.7002
[2,] 136.7002
[3,]   0.0000

convertcolor(rgb2,type="d")
          [,1]
[1,] 116.76071
[2,] 116.76071
[3,]  16.73263

# get color distance before and after simulation
deltaE(rgb1,rgb2)
[1] 116.9496

deltaE(convertcolor(rgb1,type="d"),convertcolor(rgb2,type="d"))
[1] 12.72204

# transformation matrices for each color blindness type
M = LMSD65 %*% XYZ

pretty(solve(M) %*% getcorrection(LMSD65,"p") %*% M)
     [,1]          [,2]         [,3]         
[1,] 0.1705569911  0.8294430089 0.0000000000 
[2,] 0.1705569911  0.8294430089 -0.0000000000
[3,] -0.0045171442 0.0045171442 1.0000000000 

pretty(solve(M) %*% getcorrection(LMSD65,"d") %*% M)
     [,1]          [,2]         [,3]         
[1,] 0.3306600735  0.6693399265 -0.0000000000
[2,] 0.3306600735  0.6693399265 0.0000000000 
[3,] -0.0278553826 0.0278553826 1.0000000000 

pretty(solve(M) %*% getcorrection(LMSD65,"t") %*% M)
     [,1]          [,2]         [,3]         
[1,] 1.0000000000  0.1273988634 -0.1273988634
[2,] -0.0000000000 0.8739092990 0.1260907010 
[3,] 0.0000000000  0.8739092990 0.1260907010 

pretty(SA)
     [,1]         [,2]         [,3]        
[1,] 0.2126000000 0.7152000000 0.0722000000
[2,] 0.2126000000 0.7152000000 0.0722000000
[3,] 0.2126000000 0.7152000000 0.0722000000

# method by Vienot, Brettel, Mollon, 1999
convertcolor2(rgb1,type="d",g=2.2)
            [,1]       [,2]          [,3]
[1,]  0.29275003 0.70724967 -2.978356e-08
[2,]  0.29275015 0.70724997  1.232823e-08
[3,] -0.02233659 0.02233658  1.000000e+00
          [,1]
[1,] 131.81223
[2,] 131.81226
[3,]  36.37274

convertcolor2(rgb2,type="d",g=2.2)
            [,1]       [,2]          [,3]
[1,]  0.29275003 0.70724967 -2.978356e-08
[2,]  0.29275015 0.70724997  1.232823e-08
[3,] -0.02233659 0.02233658  1.000000e+00
          [,1]
[1,] 122.71798
[2,] 122.71801
[3,]  48.34316

VIEW ALL

news + thoughts

Cancer Cell cover

Sat 23-04-2022

My cover design on the 11 April 2022 Cancer Cell issue depicts depicts cellular heterogeneity as a kaleidoscope generated from immunofluorescence staining of the glial and neuronal markers MBP and NeuN (respectively) in a GBM patient-derived explant.

LeBlanc VG et al. Single-cell landscapes of primary glioblastomas and matched explants and cell lines show variable retention of inter- and intratumor heterogeneity (2022) Cancer Cell 40:379–392.E9.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My Cancer Cell kaleidoscope cover (volume 40, issue 4, 11 April 2022). (more)

Browse my gallery of cover designs.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A catalogue of my journal and magazine cover designs. (more)

Nature Biotechnology cover

Sat 23-04-2022

My cover design on the 4 April 2022 Nature Biotechnology issue is an impression of a phylogenetic tree of over 200 million sequences.

Konno N et al. Deep distributed computing to reconstruct extremely large lineage trees (2022) Nature Biotechnology 40:566–575.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My Nature Biotechnology phylogenetic tree cover (volume 40, issue 4, 4 April 2022). (more)

Browse my gallery of cover designs.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A catalogue of my journal and magazine cover designs. (more)

Nature cover — Gene Genie

Sat 23-04-2022

My cover design on the 17 March 2022 Nature issue depicts the evolutionary properties of sequences at the extremes of the evolvability spectrum.

Vaishnav ED et al. The evolution, evolvability and engineering of gene regulatory DNA (2022) Nature 603:455–463.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My Nature squiggles cover (volume 603, issue 7901, 17 March 2022). (more)

Browse my gallery of cover designs.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A catalogue of my journal and magazine cover designs. (more)

Happy 2022 `\pi` Day—
three one four: a number of notes

Mon 14-03-2022

Celebrate `\pi` Day (March 14th) and finally hear what you've been missing.

“three one four: a number of notes” is a musical exploration of how we think about mathematics and how we feel about mathematics. It tells stories from the very beginning (314…) to the very (known) end of π (...264) as well as math (Wallis Product) and math jokes (Feynman Point), repetition (nn) and zeroes (null).

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Listen to `\pi` in the style of 20th century classical music. (details)

The album is scored for solo piano in the style of 20th century classical music – each piece has a distinct personality, drawn from styles of Boulez, Feldman, Glass, Ligeti, Monk, and Satie.

Each piece is accompanied by a piku (or πku), a poem whose syllable count is determined by a specific sequence of digits from π.

Check out art from previous years: 2013 `\pi` Day and 2014 `\pi` Day, 2015 `\pi` Day, 2016 `\pi` Day, 2017 `\pi` Day, 2018 `\pi` Day, 2019 `\pi` Day, 2020 `\pi` Day and 2021 `\pi` Day.

PNAS Cover — Earth BioGenome Project

Fri 28-01-2022

My design appears on the 25 January 2022 PNAS issue.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My PNAS cover design captures the vision of the Earth BioGenome Project — to sequence everything. (more)

The cover shows a view of Earth that captures the vision of the Earth BioGenome Project — understanding and conserving genetic diversity on a global scale. Continents from the Authagraph projection, which preserves areas and shapes, are represented as a double helix of 32,111 bases. Short sequences of 806 unique species, sequenced as part of EBP-affiliated projects, are mapped onto the double helix of the continent (or ocean) where the species is commonly found. The length of the sequence is the same for each species on a continent (or ocean) and the sequences are separated by short gaps. Individual bases of the sequence are colored by dots. Species appear along the path in alphabetical order (by Latin name) and the first base of the first species is identified by a small black triangle.

Lewin HA et al. The Earth BioGenome Project 2020: Starting the clock. (2022) PNAS 119(4) e2115635118.