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
Twenty — minutes — maybe — more.Naomichoose four wordsmore quotes

red: color


Scientific graphical abstracts — design guidelines


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

Music for the Moon: Flunk's 'Down Here / Moon Above'

Sat 29-05-2021

The Sanctuary Project is a Lunar vault of science and art. It includes two fully sequenced human genomes, sequenced and assembled by us at Canada's Michael Smith Genome Sciences Centre.

The first disc includes a song composed by Flunk for the (eventual) trip to the Moon.

But how do you send sound to space? I describe the inspiration, process and art behind the work.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
The song 'Down Here / Moon Above' from Flunk's new album History of Everything Ever is our song for space. It appears on the Sanctuary genome discs, which aim to send two fully sequenced human genomes to the Moon. (more)

Browse the genome discs.

Happy 2021 `\pi` Day—
A forest of digits

Sun 14-03-2021

Celebrate `\pi` Day (March 14th) and finally see the digits through the forest.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
The 26th tree in the digit forest of `\pi`. Why is there a flower on the ground?. (details)

This year is full of botanical whimsy. A Lindenmayer system forest – deterministic but always changing. Feel free to stop and pick the flowers from the ground.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
The first 46 digits of `\pi` in 8 trees. There are so many more. (details)

And things can get crazy in the forest.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A forest of the digits of '\pi`, by ecosystem. (details)

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 and 2019 `\pi` Day.

Testing for rare conditions

Sun 30-05-2021

All that glitters is not gold. —W. Shakespeare

The sensitivity and specificity of a test do not necessarily correspond to its error rate. This becomes critically important when testing for a rare condition — a test with 99% sensitivity and specificity has an even chance of being wrong when the condition prevalence is 1%.

We discuss the positive predictive value (PPV) and how practices such as screen can increase it.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Nature Methods Points of Significance column: Testing for rare conditions. (read)

Altman, N. & Krzywinski, M. (2021) Points of significance: Testing for rare conditions. Nature Methods 18:224–225.

Standardization fallacy

Tue 09-02-2021

We demand rigidly defined areas of doubt and uncertainty! —D. Adams

A popular notion about experiments is that it's good to keep variability in subjects low to limit the influence of confounding factors. This is called standardization.

Unfortunately, although standardization increases power, it can induce unrealistically low variability and lead to results that do not generalize to the population of interest. And, in fact, may be irreproducible.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Nature Methods Points of Significance column: Standardization fallacy. (read)

Not paying attention to these details and thinking (or hoping) that standardization is always good is the "standardization fallacy". In this column, we look at how standardization can be balanced with heterogenization to avoid this thorny issue.

Voelkl, B., Würbel, H., Krzywinski, M. & Altman, N. (2021) Points of significance: Standardization fallacy. Nature Methods 18:5–6.

Graphical Abstract Design Guidelines

Fri 13-11-2020

Clear, concise, legible and compelling.

Making a scientific graphical abstract? Refer to my practical design guidelines and redesign examples to improve organization, design and clarity of your graphical abstracts.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Graphical Abstract Design Guidelines — Clear, concise, legible and compelling.