Search code examples
color-spacegammasrgb

sRGB constant luminance stripe


I want to create a color spectrum of constant perceived luminance.

This is my attempt so far (here's the codesandbox):

enter image description here

The code

  • goes through 8-bit RGB values of increasing hue and constant (and irrelevant) lightness,
  • transforms the triples to the corresponding linear values (un-"gamma", code for that taken from here),
  • calculates the luminance by forming the scalar product with the sRGB luminance values,
  • normalized the color by dividing by the luma and finally
  • convert back to 8-bit RGB (re-"gamma").

As I annotated in the image, the second stripe from the bottom has a rather bright blue if you ask me though. Now that could be because

  • my screen is off of sRGB (although my phone agrees),
  • my eyeballs are off the human average,
  • sRGB luminance values don't reflect luminance perception to begin with

I think it's more likely I've made some mistake or haven't understood something here.

I tweaked the sRGB luminance values slightly to get the bottom stripe that is on the verge of being what I would expect (perhaps still a bit bright that blue though).

So my question:

  • What do you guys see on your screens, subjectively? Which of the bottom two stripes do you think is closer to perceived constant brightness?
  • Presuming I'm not the only one, what's wrong here?

Solution

  • RGB colourspaces are not perceptually uniform spaces. Generating a perceptually uniform hue stripe requires using a perceptually uniform colourspace or colour appearance model such as ICtCp or CAM16.

    With Colour, it could be achieved as follows:

    import colour
    import numpy as np
    
    
    def colour_stripe(S=1, samples=360):
        H = np.linspace(0, 1, samples)
    
        HSV = colour.utilities.tstack([H, np.ones(samples) * S, np.ones(samples)])
        RGB = colour.HSV_to_RGB(HSV)
     
        return RGB[np.newaxis, ...]
    
    
    RGB = np.resize(colour_stripe(), [36, 360, 3])
    
    colour.plotting.plot_image(colour.cctf_encoding(RGB * 0.5));
    
    CAM16 = colour.convert(RGB, 'RGB', 'CAM16')
    CAM16_UL = colour.CAM16_Specification(
        np.full(CAM16.J.shape, 0.5), CAM16.C, CAM16.h)
    
    RGB_PU = colour.convert(CAM16_UL, 'CAM16', 'RGB')
    
    colour.plotting.plot_image(colour.cctf_encoding(RGB_PU));
    

    RGB RGB PU

    Keep in mind that the assumptions here are sRGB display calibration and viewing conditions.