LED Brightness to your eye, Gamma correction – No!

1 year ago

Human perceive brightness change non-linearly

When you want to change the brightness of LED or any light source, one thing you need to consider is how human perceive the brightness. As you see in the following chart, human perceive the brightness change non-linearly. We have better sensitivity at low luminance than high luminance. For example, when we control LED brightness using Arduino PWM, we see big brightness change between analogWrite(9,1) and analogWrite(9,2). We don’t see brightness change between analogWrite(9,244) and analogWrite(9,255). If you didn’t know, just quickly test yourself with Arduino. If you want to control LED brightness linearly to your eye, it require to have some adjustment.

Mis-understanding of Gamma Correction

In Arduino or any microcontroller, a common way to achieve linear brightness change is a lookup table that compensate or correct the value according to the curve. There are a common misunderstand or confusion regarding what curve to use. Many people use so called gamma correction table or equation which is not related with human perception of brightness. The Maxim App note describe “Gamma correction is used to correct for the nonlinear relationship between luminance and brightness” which is simply wrong. The gamma correction is used to correct nonlinear relationship between applied voltage to CRT and luminance of CRT. It is nothing to do with human perception. It is not just Maxim, I could find many implementation of gamma correction to correct luminance and brightness. The cyz_RGB also use the gamma correction.


Why people so easily confuse about it? The gamma correction is necessary for the display application. When movie or image is displayed on LED matrix like a stadium display, you want to have gamma correction since movie and image data itself is already gamma corrected data. It is also useful for LED based LCD backlight. When LED is used for lighting, however gamma correction is irrelevant. A funny thing is co-incidentally gamma correction and human perception of luminance is very similar. http://www.poynton.com/PDFs/SMPTE93_Gamma.pdf Take a look at following chart. Again it is just a co-incidence. So somehow the gamma correction is close approximation of human perception the luminance.

Correction calculation of luminance and brightness describe in CIE 1931 report then used for CIELAB color space.

L* = 116(Y/Yn)^1/3 – 16 , Y/Yn > 0.008856
L* = 903.3(Y/Yn), Y/Yn <= 0.008856

Where L* is lightness, Y/Yn is Luminance ratio.

For the correction curve, you need to inverse the equation.


Due to the similarity between gamma correction and brightness vs luminance, there is only minor differences. If you still want to have more accurate correction, you can use following table for cyz_RGB firmware. Don’t get confused about 16bit valves, your PWM is still 8 bit. It is internally use 16 bit value for conversion.

 2: /*
 3: 16 bits to 8 bit CIE Lightness conversion
 4: L* = 116(Y/Yn)^1/3 - 16 , Y/Yn > 0.008856
 5: L* = 903.3(Y/Yn), Y/Yn <= 0.008856
 6: */
 8: prog_uint16_t pwm_table[] PROGMEM = {
 9:     65535,    65508,    65479,    65451,    65422,    65394,    65365,    65337,
 10:     65308,    65280,    65251,    65223,    65195,    65166,    65138,    65109,
 11:     65081,    65052,    65024,    64995,    64967,    64938,    64909,    64878,
 12:     64847,    64815,    64781,    64747,    64711,    64675,    64637,    64599,
 13:     64559,    64518,    64476,    64433,    64389,    64344,    64297,    64249,
 14:     64200,    64150,    64099,    64046,    63992,    63937,    63880,    63822,
 15:     63763,    63702,    63640,    63577,    63512,    63446,    63379,    63310,
 16:     63239,    63167,    63094,    63019,    62943,    62865,    62785,    62704,
 17:     62621,    62537,    62451,    62364,    62275,    62184,    62092,    61998,
 18:     61902,    61804,    61705,    61604,    61501,    61397,    61290,    61182,
 19:     61072,    60961,    60847,    60732,    60614,    60495,    60374,    60251,
 20:     60126,    59999,    59870,    59739,    59606,    59471,    59334,    59195,
 21:     59053,    58910,    58765,    58618,    58468,    58316,    58163,    58007,
 22:     57848,    57688,    57525,    57361,    57194,    57024,    56853,    56679,
 23:     56503,    56324,    56143,    55960,    55774,    55586,    55396,    55203,
 24:     55008,    54810,    54610,    54408,    54203,    53995,    53785,    53572,
 25:     53357,    53140,    52919,    52696,    52471,    52243,    52012,    51778,
 26:     51542,    51304,    51062,    50818,    50571,    50321,    50069,    49813,
 27:     49555,    49295,    49031,    48764,    48495,    48223,    47948,    47670,
 28:     47389,    47105,    46818,    46529,    46236,    45940,    45641,    45340,
 29:     45035,    44727,    44416,    44102,    43785,    43465,    43142,    42815,
 30:     42486,    42153,    41817,    41478,    41135,    40790,    40441,    40089,
 31:     39733,    39375,    39013,    38647,    38279,    37907,    37531,    37153,
 32:     36770,    36385,    35996,    35603,    35207,    34808,    34405,    33999,
 33:     33589,    33175,    32758,    32338,    31913,    31486,    31054,    30619,
 34:     30181,    29738,    29292,    28843,    28389,    27932,    27471,    27007,
 35:     26539,    26066,    25590,    25111,    24627,    24140,    23649,    23153,
 36:     22654,    22152,    21645,    21134,    20619,    20101,    19578,    19051,
 37:     18521,    17986,    17447,    16905,    16358,    15807,    15252,    14693,
 38:     14129,    13562,    12990,    12415,    11835,    11251,    10662,    10070,
 39:     9473,    8872,    8266,    7657,    7043,    6424,    5802,    5175,
 40:     4543,    3908,    3267,    2623,    1974,    1320,    662,    0
 41: };

Patched cyz_RGB firmware can be found at google code download.


You can test linearity of LED brightness to your eye using following Arduino example code.

Just put any LED on pin 9 with current limiting resistor.

 1: /*
 2:  Change brightness of LED linearly to Human eye
 3:  32 step brightness using 8 bit PWM of Arduino
 4:  brightness step 24 should be twice bright than step 12 to your eye.
 5: */
 7: #include <avr/pgmspace.h>
 8: #define CIELPWM(a) (pgm_read_word_near(CIEL8 + a)) // CIE Lightness loopup table function
 10: /*
 11: 5 bit CIE Lightness to 8 bit PWM conversion
 12: L* = 116(Y/Yn)^1/3 - 16 , Y/Yn > 0.008856
 13: L* = 903.3(Y/Yn), Y/Yn <= 0.008856
 14: */
 16: prog_uint8_t CIEL8[] PROGMEM = {
 17:     0,    1,    2,    3,    4,    5,    7,    9,    12,
 18:     15,    18,    22,    27,    32,    38,    44,    51,    58,
 19:     67,    76,    86,    96,    108,    120,    134,    148,    163,
 20:     180,    197,    216,    235,    255
 21: };
 23: int brightness = 0;    // initial brightness of LED
 24: int fadeAmount = 1;
 26: void setup()  {
 27:   // declare pin 9 to be an output:
 28:   pinMode(9, OUTPUT);
 29: }
 31: void loop()  {
 32:   // set the brightness of pin 9:, 0-31, 5 bit steps of brightness
 33:   analogWrite(9, CIELPWM(brightness));
 35:   // change the brightness for next time through the loop:
 36:   brightness = brightness + fadeAmount;
 38:   // reverse the direction of the fading at the ends of the fade:
 39:   if (brightness == 0 || brightness == 31) {
 40:     fadeAmount = -fadeAmount ;
 41:   }
 42:   // wait for 500 milliseconds to see the bightness change
 43:   delay(500);
 44: }

Posted on November 13, 2012, in Uncategorized. Bookmark the permalink. 26 Comments.

  1. Hi Peter,

    Thanks for this post an interesting read. I’m trying to find good correction curve for a led installation in combination with dithering (https://gist.github.com/kasperkamperman/5df023b43c93112e9cef)

    I only come across gamma calculations, but I’m interested in using the curve you present in the graph. Do you have a formula to calculate the graph? I don’t completely understand how to use the (L* = 116(Y/Yn)^1/3 – 16) formula to come up with a lookup table (my math skills are not of the highest level).

    I’d like to render an 8bit table with an 8bit outcome and also tryout the curve in a python script: https://github.com/raplin/HexaWS2811/blob/master/gamma.py

    If you are interested see some posts in the FastLED group: https://plus.google.com/communities/109127054924227823508

    I hope you can explain.

    • Hi Kaspar,

      Yes, I do but it’s a while ago. Actual I’ve been meaning treatise this article but somehow there are never enough hours in any given day 🙂
      My computer is currently occupied by one of my Kids but I should be able to send you more detailed information within the next couple of days.

      • That would be great! Thanks.

      • Hi Kasper,

        Here is a link to a thread on the Arduino Forum that explains how to calculate a luminance/brightness map.
        With that little bit of math you can calculate your own arbitrary mappings 8 bit, 10 bit etc. For my own purposes I used an open office (well, Neo Office on a mac) spreadsheet but I have to admit that it’s a bit of text conversion work from there to create an Array usable in C/C++.

        However, in your comment you are referring to WS2811 chips and I am assuming that you are going to use individually addressable LED strips that employ a 5050 LED and a WS2811 chip. If you’ve not bought the hardware yet, my first recommendation would be to us LED strips with the 2812b LED. Those use LEDs that basically integrate the chip electronics into a 5050 LED and allow for much more densely populated LED strips.

        My second recommendation would be not to use a “normal” Aduino board as a micro controller. Depending on your projects requirements I’d recommend FadeCandy. The link leads to Adafruit but this little micro controller board is available from a number of distributors. It already includes temporal dithering, I am not sure it includes brightness correction (Gamma, or CIE LAB).

        Originally FadeCady is based on the Teensy 3 micro controller and if more flexblity is needed and perhaps you want to use a large number of LED’s (several thousand) the Teensy 3.1 in combination with the OCTOWS2811 library is the current state of the art. The original OCTOWS2811 library does not include dithering within the library (the movie2serial program does the gamma correction for video streamed to the Teensy), but a user modified it to use 16, instead of 8 channels (HexaWS2811)and it includes dithering and gamma control (not CIE LAB). the thread also contains a python script that generates the array for the dithering and it should not be too difficult to re-write it to use IE LAB.
        Here is the thread explaining the details.

        I hope this is what you are looking for and it should keep you busy for a while 😉

  2. Thank you for your pointer to the Arduino thread. I’ll study it and see if I can render that table (in combination with the HexaWS2811 dithering).

    Yes, I’m familiar with FadeCandy and it’s perfectly working temporal dithering. Actually I’m now in the process running my code entirely on a Teensy 3.0 without the use of FadeCandy. That saves a separate computer and I’m indeed implementing the HexaWS2811 algorithm (so I’ll rewrite that Python script to CIE LAB).

    I have a new favorite chipset over the WS2812b (that I’m currently using), the APA102. Also in one package, no (3.3>5V) level converter needed and much faster than the WS2812b (ok, you only need an extra clock line). This means that implementing dithering is much easier, because you can update faster.

    • I see. Looks like I was praying to the choir 😉
      After venturing to your website I should have anticipated that! I really love all the projects you’ve done. If you get around to re-writing that python script it would be really nice if you could post a link (if you have a GitHub account for example).
      Thanks for the pointer to the APA102. I was not aware of this new LED and. It seems it solves a lot of problems for folks that want to do POV projects.

  3. Hereby the two implementations on Gist.

    I’ve made a Processing script to render one table as a correction curve and I’ve modified the HexaWS2811 dithering script with the curve:
    – luminance_dither_lut.py: https://gist.github.com/kasperkamperman/e7a990d6af49f4cbed32
    – luminance_lut.pde: https://gist.github.com/kasperkamperman/3c3f72208366ed885f2f

  4. The curve is implemented in the original HexaWS2811 code as well now by its creator: http://github.com/raplin/HexaWS2811/blob/master/gamma.py

  5. Hi trippylighting!

    I am a fortunate possessor of two of your shields, since a couple years.
    For a new project, and more generally as temporary controllers, I would like to use them from pure data on a host machine.
    I have some raspberry py, and i keep that in mind for the future, and i really would like to control them via osc (maybe a pure data patch?)

    I have tested pduino on my machine (http://at.or.at/hans/pd/objects.html)

    But is not yet clear how could I integrate firmata and HPRGB_Shield_V2 in an arduino. Has someone already written the code?

    Any suggestion is highly appreciated



    • Hi Fred,

      I apologize for the delay in my answer. Somehow I don’t always get notifications anymore from WordPress when comments are made on the blog

      If you want to talk to the LED shields using OSC, you will need a microcontroller, preferably an Arduino clone as the library is an Arduino compatible library. I personally prefer the as they employ an Arm Cortex m4 MCU and are not only vastly more powerful than your usual Arduino but all a lot smaller and at $20 a lot less expensive. Also as you want to use OSC you’ll need an Ethernet controller. On the PJRC website, the makers of the Teensy boards you can also find a nice Adapter for the WIZ820io embedded Ethernet controller. That takes more or less care of the hardware side.
      Unless of course you want to use the RaspberryPI to talk to the shields. I don’t have any experience with the Raspberry PI and what may be available there as a software layer for OSC.

      The microcontroller will receive the OSC messages from PureData. On the Teensy/Arduino you can receive OSC messages using the Oscuino library. I am not aware of anyone who has written a wrapper that translates OSC messages in commandos for the LED shield, but that should actually be very easy to do and I can provide you with sample code if necessary.
      You don’t need Firmata or pduino for this at all. I am not saying these could not be used to achieve this as well, but to talk to the LED shields from a Computer running PureData they are not needed. PureData can send out OSC message as you are probably already know. There are several tutorials for example how to interface PureData with TouchOSC.

      In my applications using these LED shields I remote control my Lighting systems using TouchOSC and the Teensy hardware described above actually connected to a little WiFi pocket router. Works like a charm!

  6. Thanks for the informative post.

    I’m planning a project using ws2812/neopixel.

    I’m wondering, let’s say I want red, blue and purple all at “50%” perceived brightness.

    50% in the table is about 50/255.

    I know our perception of red and blue is different.

    Would using 50/255 be close for red and blue or would I want some kind of “eye perception” correction?

    For the purple at 50% –

    For the sake of my example, say purple is “half” blue and “half” red.

    Would I want to use 50/255 red and 50/255 blue? I’m guessing not.

    25/255 red and 25/255 blue (half as much “brightness” for each)?

    Or 10/255 red and 10/255 blue (half as much perceived brightness for each)?

    Thanks, Rick

    • Hi Rick,

      Just now saw your post. For some reason WordPress did not notify me. Do you still need an answer for the question ?
      The simplest solution would be to us the FastLED library http://fastled.io
      It supports brightness correction and a lot of other stuff and works with the Neopixels and a whole host of other individually addressable LED strips.

      • Yes, I’m still interested in understanding the nuances of color LEDs.

        In addition to accounting for the varied perception of the human eye, I note that the Neopixel has different max luminance for each color (red=700mcd, green=1700mcd, blue=450mcd), but, of course, they are not in relation to the eye perception.

        So, I’m guessing (still haven’t had a chance to play with them) that 255/255/255 is 700/1700/450, which wouldn’t look white at all.

        The green is almost four times the luminance (I’m assuming photon luminance?) and the eye’s perceived luminance of green is about four times that of blue, so I would think it would be lacking in blue (ditto red)…

        Am I understanding correctly?

        When I see demos, they look white, even when I don’t think they accounted for the dual luminance/perception vagaries…

      • Before coming to a judgement whether they would not would look white at all given these luminance values, perhaps wait until you can play with them and use your eyes for judgement. Chances are they will look fairly white 😉

        The curve explained in the article is not aimed at color correction but simply aimed at providing a more linear ( to the human eye) fading experience. In essence when you have a slider that adjusts frightens from completely off to full on and have it set at 50% then your eyes perceive brightness at 50%.
        With I corrected brightness that’s not the case.

        This is not meant to do color correction. There is a lot more involved in that.

        How much of that you need depends on your project, however, as I already stated the FastLED already includes many functions, including brightness correction and it is also fast enough to run very large LED projects.

  7. Hi Peter!
    I am doing some research about perception of brightness.
    I have two led bublbs with two dimmer swihches. I planned to set some different values of illuminance (meassured with luxmeter) and that participant should set same brightness on the other light with dimmer.
    But i have been told, I should tell participants in research something like this is 100 lux, set what you think would be 200 lux or 150 lux, etc (factor 2 or 1,5)…
    However, according to my quick research about brightness perception (like first picture in your post) this wouldn’t be correct as it is not linear relationship. Am I thinking in a right way? Regards, Nejc.

    • Hi Nejk,

      The unit of lux is already weighted for human perception. The chart shows luminance. Luminance does not take human perception into account but is a purely physical unit.

      • Right, tnx for your responce. Somehow I have managed to learn these things by now and resolve my dilemma. Greetings!

      • Actually, lux is the SI unit for illuminance, whereas nit (cd/m²) is the SI unit for luminance.

        Both luminance and illuminance are weighted for human perception regarding spectral response (e.g. more sensitive to green).

        However, both are physically linear, that is they scale proportionally to power or photon quantities. They are NOT perceptually linear regarding changes in intensity.

        For example, if emitted luminance (nit) or incoming illuminance (lux) is suddenly doubled, then the number of photons per second doubles. However, a human will perceive a somewhat smaller brightness change (1.3x or 1.4x instead of 2.0x)

        In summary:

        – luminance (nit) = cd/m^2 = Y = weighted for human spectral response, but physically linear (proportional to photons)

        – illuminance (lux) = lumen/m^2 = weighted for human spectral response, but physically linear (proportional to photons)

        If you want something that is weighted for human spectral sensitivity AND human perception of intensity (not physically linear), you need something like lightness, eg CIE L* from CIE LAB.

        Hope this helps,

  8. Matthew,

    Thanks for that post! That is the best and most concise explanation of this I’ve come across and I the article doe not do a good job to differentiate between the human response to spectral distribution and intensity response.

    The software library that comes with the shield does in fact account for lightness according to CIE LAB through a look up table with 12 bit resolution.

  1. Pingback: Dioda i PWM, czemu świeci nierówno? - Starter Kit

  2. Pingback: A smooth breath effect while using FastLED’s power management | Jonathan Thomson's web journal

  3. Pingback: Прокачиваем Ikea: как превратить светомузыку в Большого Брата — IT-МИР. ПОМОЩЬ В IT-МИРЕ

  4. Pingback: [Перевод] Прокачиваем Ikea: как превратить светомузыку в Большого Брата | INFOS.BY

  5. Pingback: Прокачиваем Ikea: как превратить светомузыку в Большого Брата / Хабр

  6. Pingback: how to turn light music into Big Brother / geek magazine – Developers

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: