I believe I converted between sRGB and linear RGB correctly, so why do the results look worse for dark colors?

476 Views Asked by At

After a study of the Wikipedia entry on sRGB I implemented a set of functions to help with color conversions:

import "math"

// https://en.wikipedia.org/wiki/SRGB#Transformation

var byteDecoded [256]float32 = func() (floats [256]float32) {
    for i := 0; i < 256; i++ {
        floats[i] = float32(i) / 255
    }
    return floats
}()

// Standard returns the sRGB color space value in range [0.0-1.0] for v, assuming v is in linear RGB in range [0.0-1.0].
func Standard(v float32) float32 {
    if v <= 0.0031308 {
        return v * 12.92
    }
    return float32(1.055*math.Pow(float64(v), 1.0/2.4) - 0.055)
}

// Standardb returns the sRGB color space value in range [0-255] for v, assuming v is in linear RGB in range [0.0-1.0].
func Standardb(v float32) uint8 {
    if v >= 1 {
        return 255
    }
    if v <= 0 {
        return 0
    }
    return uint8(Standard(v)*255 + 0.5)
}

// Linear returns the linear RGB color space value in range [0.0-1.0] for v, assuming v is in sRGB in range [0.0-1.0].
func Linear(v float32) float32 {
    if v <= 0.04045 {
        return v * (1.0 / 12.92)
    }
    return float32(math.Pow((float64(v)+0.055)/1.055, 2.4))
}

// Linearb returns the linear RGB color space value in range [0.0-1.0] for b, assuming b is in sRGB in range [0-255].
func Linearb(b uint8) float32 {
    return Linear(byteDecoded[b])
}

I then played with some results.

log.Printf("Half of sRGB 255 calculated in linear RGB is %d", Standardb(Linearb(255)/2))
prints Half of sRGB 255 calculated in linear RGB is 188.

I then made this:

enter image description here

Top half: checkerboarded red (255, 0, 0) and green (0, 255, 0) pixels.
Lower left: naive mixdown by division with 2 (128, 128, 0).
Lower right: (188, 188, 0)

The lower half shows two different attempts at what the top half could look like when scaled down by 50% on both axes. Since the top half is interleaved full green and full red pixels, a downscale would have to add half red and half green together, the value for which is what I calculated earlier (188).

The lower right matches the top half quite exactly on my plain consumer display monitor when crossing my eyes, so it seems like this whole conversion math is working out.

But what about darker colors?

log.Printf("Half of sRGB 64 calculated in linear RGB is %d", Standardb(Linearb(64)/2))
prints Half of sRGB 64 calculated in linear RGB is 44.

I do the same as before:

enter image description here

Top half: checkerboarded dark red (64, 0, 0) and dark green (0, 64, 0) pixels.
Lower left: naive mixdown by division with 2 (32, 32, 0).
Lower right: (44, 44, 0)

This time, on my display, the naive (incorrect) method matches the upper half almost perfectly, while the value that I went through the effort to calculate in the lower right looks way too bright.

Did I make a mistake? Or is this just the extent of error to expect on consumer display devices?

2

There are 2 best solutions below

0
Zyl On BEST ANSWER

Did I make a mistake?

Yes and no. Your code is correct, but your testing methodology has an oversight:

is this just the extent of error to expect on consumer display devices?

Yes. In particular, this likely is caused by the display panel's control driver. See someone making a similar observation here: https://electronics.stackexchange.com/questions/401617/lcd-pixels-how-chess-board-pixel-fill-patterns-are-called

The intensity of the problem can be reduced by going from a checkerboard pixel pattern to alternatingly colored horizontal pixel lines. The math comes out the same, but the results can look wildly different.

First image with horizontal lines:

enter image description here

Second image with horizontal lines:

enter image description here

Bonus: second image with vertical lines (try squinting; it looks even more accurate for me):

enter image description here

Also note that "plain consumer monitor" is already a wide range in terms of the display's ability to be accurate. If I drag this here very Stackoverflow window over to my cheaper second monitor I am completely unable to confirm any of the assertions you are making due to how wildly inaccurate its color output is.

0
Myndex On

Short Answer

While your code is mostly correct as in "by the book", unfortunately, this does not necessarily describe an actual real-world display.

Longer Answer

  1. Blending needs to be off
    I want to point out that many browsers will blend small checker patterns like this, and the image tag needs this style added:

img {
  image-rendering: crisp-edges;
}

In order to ensure the checker pattern is not blended, if viewing the image here, in a browser (depending on the browser).

  1. Gamma
    The monitor's gamma or transfer curve has a lot to do with how well this will match. Most displays are set at a straight 2.2 gamma, but this is affected by user adjustments, and by monitor calibration and profile. And it's all affected by color management.

And all this is going to vary, depending on the OS you are using.

  1. Color spaces and profiles
    sRGB is the standard profile for web images. But the transfer curve (gamma) is not exactly the same as a typical display, and the discrepancy can be most noticeable in very dark areas.

And maddeningly, some color management systems may—or may not—correct these colors.

  1. Some examples

For the brighter image, I found the best match at rgb(191,191,0)

For the darker one, rgb(44,44,0) which seems to match your calculation.

But this does not solve the discrepancy you were finding.

And I was unable to recreate what you described you were seeing, but what it sounds like is the black level of your uncalibrated display was lifted, affecting the gamma. A lifted black level in the display could makergb(32,32,0) match, and rgb(44,44,0) seem too bright.

The black level is the most sensitive area in terms of monitor adjustment (it is sometimes labeled as "brightness").

Recommendation

I suggest taking the time to calibrate and profile your display. MacOS has a built in calibration tool:

SystemPreferences ⇨ Display ⇨ Color ⇨ Calibrate...

For the expert mode, hold down "option" when you click the calibrate button.

Windows has something similar.

More ideally, get an hardware calibration tool. XRite i1-Display Pro is reasonable and a useful tool to keep around if you are concerned about color at all.

Summary

The main issues are:

  • monitor is not calibrated, and
  • the sRGB math, while correct "by the book" for the piecewise transform, does not necessarily describe the actual monitor EOTF characteristics (and your mileage may vary on this).