Sorting By Color in the LCH Color Space

When organizing reveals a very pleasant rabbit hole

Sorting By Color in the LCH Color Space

The surprisingly complicated world of organizing things by color

A few months ago, I decided to organize my closet and bookshelves by color. ROYGBIV is the easiest sorting heuristic; however, ROYGBIV is too simplistic for an organizing task of any real size.

ROYBGIV.png
ROYGBIV: Red Orange Yellow Green Blue Indigo Violet

Most systems of describing color exist along more than 1 axis, which helps capture the real-world complexity of color that ROYGBIV misses. Instead, I'm using the LCH color space to organize my closet and bookshelves.[1]

LCH is an acronym for:

Luminance
egL.png
A shade of red with a range of Luminance values

Chroma
egC.png
A shade of red with a range of Chroma values

and Hue.
egH400.png
Three color samples with shifting Hue values

Together, Luminance, Chroma, and Hue create a three-dimensional color space. You can read more about LCH here.

When organizing things by color, the tough question is how to deconstruct a multi-dimensional space...
ybg.png
Colors in a three-dimensional space

... and then sort the individual colors along a single line in a way that looks nice and feels self-explanatory.

singlelines.png
Many attempts at putting colors in a single line

My method for gathering color data
I'm using a device by Nix to grab the colors of my clothing and books. It returns data that looks like this:
Sort5.png

The data is available as a CSV file, and the rest of the work happens in Excel.
egcsvdata.png

My current sorting method
After many iterations, here's where I've currently landed for sorting.

Sort Level 0: Unsorted Colors
These are some unsorted colors.
Sort0.png

 

Sort Level 1: Separate greys, blacks, whites
Any color with a Chroma value lower than 4.1 gets pushed to the front or back of the sort.[2]
Sort1.png

Sort1Grey.png
All these colors have a Chroma value of 4.1 and are right on the border of grey, in my system

 

Sort Level 2: Hue
Hue values have been rounded up to increments of 20[3] and then sorted from smallest to largest.

Sort2.png

egH200.png
Hue reminder

 

Sort Level 3: Luminance
Luminance values have been rounded to the nearest 15[4] and then sorted in reverse order, from largest to smallest.

Sort3.png

egL200.png
Luminance reminder

 

Sort Level 4: Chroma
All other things being equal, Chroma sorts from smallest to largest.

Sort4.png

egC350.png
Chroma reminder

Notes
Rounding
Rounding is necessary for the sort to work at all... the Nix device returns values with 4+ decimal points. Without rounding those precise values into big round numbers, the sorting method wouldn't move beyond Hue.

Excel VBA
To help visualize the color space and get an intuitive sense of how I wanted to group and sort colors, I used VBA to create a grid of LCH colors.

Since Excel doesn't use CIELCH or CIELAB to display colors, I had to convert from LCH ➡️ LAB ➡️ XYZ ➡️ RGB. It was annoying to figure out; here's my code and some websites that helped in case it's helpful for anyone else. (Or, likely, just me in the future.)

Sub FillLCHVariants()
    Dim i As Integer, j As Integer
    Dim lLch, cLch, hLch As Double
    Dim rRGB As Double
    Dim gRGB As Double
    Dim bRGB As Double
    Dim label As Double

    lLch = 0
    cLch = 0
    hLch = 301.36
    
    
    
    
    ' Set Column/Row Labels
    label = 0
    For i = 2 To 12
        Cells(i, 1).Value = "Luminance " & label
        label = label + 10
    Next i
    
    label = 0
    For j = 2 To 19
        Cells(1, j).Value = "Chroma " & label
        label = label + 10
    Next j
    
    
    
    ' Loop through rows and columns
    For i = 2 To 12
        For j = 2 To 19
            
            ' Convert LCH to RGB
            Call LCHtoRGB(lLch, cLch, hLch, rRGB, gRGB, bRGB)
'            Call LCHtoRGB(50, 50, hLch, rRGB, gRGB, bRGB)
            
            ' Set background color of the cell
            Cells(i, j).Interior.Color = RGB(rRGB, gRGB, bRGB)
            cLch = cLch + 10
        Next j
        lLch = lLch + 10
        cLch = 0
    Next i
End Sub




Sub LCHtoRGB(ByVal l As Double, ByVal c As Double, ByVal h As Double, ByRef rRGB As Double, ByRef gRGB As Double, ByRef bRGB As Double)
    Dim aLab, bLab As Double
    Dim fx, fy, fz As Double
    Dim x, y, z As Double
    Dim tempx, tempy, tempz As Double
    Dim EPSILON As Double
    Dim KAPPA As Double

    EPSILON = 216 / 24389
    KAPPA = 24389 / 27

    ' Convert LCH To LAB
    aLab = Math.Cos(h * 0.01745329251) * c
    bLab = Math.Sin(h * 0.01745329251) * c
    

    
    Debug.Print "LAB"
    Debug.Print l
    Debug.Print aLab
    Debug.Print bLab


    ' Convert LAB to XYZ
    fy = (l + 16) / 116
    fx = (aLab / 500) + fy
    fz = fy - (bLab / 200)

    
    Debug.Print "fxyz"
    Debug.Print fx
    Debug.Print fy
    Debug.Print fz
    
    'x = f_inv(fx) * 0.950449218275099

    If fx ^ 3 > EPSILON Then
        x = fx ^ 3
    Else
        x = (116 * fx - 16) / KAPPA
    End If


    If l > 8 Then
        y = fy ^ 3
    Else
        y = l / KAPPA '(24389 / 27) <-- is KAPPA
    End If
    
    
'    z = f_inv(fz) * 1.08891664843047
    If fz ^ 3 > EPSILON Then
        z = fz ^ 3
    Else
        z = (116 * fz - 16) / KAPPA
    End If

    
    Debug.Print "XYZ"
    Debug.Print x
    Debug.Print y
    Debug.Print z
    
    tempx = x * 0.9531874 + y * -0.0265906 + z * 0.0238731
    tempy = x * -0.0382467 + y * 1.0288406 + z * 0.009406
    tempz = x * 0.0026068 + y * -0.0030332 + z * 1.0892565
    
    x = tempx
    y = tempy
    z = tempz
    


    ' Convert XYZ to SRGB
    rRGB = x * 3.24081239889528 - y * 1.53730844562981 - z * 0.49858652290696
    gRGB = x * -0.9692430170086 + y * 1.87596630290857 + z * 0.04155503085668
    bRGB = x * 0.05563839843611 - y * 0.20400746093241 + z * 1.05712957028614


    Debug.Print "preRGB"
    Debug.Print rRGB
    Debug.Print gRGB
    Debug.Print bRGB

    rRGB = gamma_compression(rRGB)
    gRGB = gamma_compression(gRGB)
    bRGB = gamma_compression(bRGB)
    
    Debug.Print "postRGB"
    Debug.Print rRGB
    Debug.Print gRGB
    Debug.Print bRGB
    
    
    
End Sub



' Helper Function
Private Function gamma_compression(ByVal linear As Double) As Byte
    Dim v As Double
    Dim tempGamma As Double
    
    If linear <= 0.0031306684425 Then
'        v = 3294.6 * linear
        v = 12.92 * linear
    Else
'        v = 269.025 * linear ^ (5 / 12) - 14.025
        v = 1.055 * (linear ^ 0.41666667) - 0.055

    End If
    v = v * 255
    
    Debug.Print "Gamma pre Min Max"
    Debug.Print v
    
    tempGamma = WorksheetFunction.RoundDown(WorksheetFunction.Min(v, 255), 0)
    tempGamma = WorksheetFunction.RoundUp(WorksheetFunction.Max(tempGamma, 0), 0)
    gamma_compression = tempGamma
End Function


I referenced Rust code from mina86.com.

I tried using Perplexity.ai to convert Rust to VBA, which very much did not work, but Perplexity is still awesome and worth checking out.[5]

I used these pages from brucelindbloom.com to get the base formulas for conversion, which I referenced to manually rework the VBA code.

I used this page by Atmos for quick, one-off color conversions.

I haven't used this page by Ocean Optics Web Book, yet, but I think it may be useful in the future: From XYZ to RGB


  1. If "RGB" or "CMYK" means anything to you, LCH is like those. ↩︎

  2. Excel formula: =IFS(AND(J2<4.1,I2<50),3,AND(J2<4.1,I2>=50),1,J2>=4.1,2) -- column I contains Luminance data ↩︎

  3. I used CEILING.MATH ↩︎

  4. I'll test rounding Luminance up to increments of 15 in future iterations. ↩︎

  5. Perplexity is like ChatGPT but better, in my opinion. ↩︎