Image Processing Algorithms Part 5: Contrast Adjustment

This article was originally published in issue 56 of The Crypt Mag

Last time we looked at adjusting the brightness of an image. This time we are going to look at adjusting the contrast of an image which is a little bit more complex.

The first step is to calculate a contrast correction factor which is given by the following formula:

In order for the algorithm to function correctly the value for the contrast correction factor (F) needs to be stored as a floating point number and not as an integer. The value C in the formula denotes the desired level of contrast.

The next step is to perform the actual contrast adjustment itself. The following formula shows the adjustment in contrast being made to the red component of a colour:

Translating the above formulas into pseudo-code would give something like this:

   factor = (259 * (contrast + 255)) / (255 * (259 - contrast))
   colour = GetPixelColour(x, y)
   newRed   = Truncate(factor * (Red(colour)   - 128) + 128)
   newGreen = Truncate(factor * (Green(colour) - 128) + 128)
   newBlue  = Truncate(factor * (Blue(colour)  - 128) + 128)
   PutPixelColour(x, y) = RGB(newRed, newGreen, newBlue)

The procedure Truncate() was previously mentioned in part 2 of this series and just ensures that the new values of red, green and blue are within the valid range of 0 to 255.

The value of contrast will be in the range of -255 to +255. Negative values will decrease the amount of contrast and, conversely, positive values will increase the amount of contrast.

Here we have the ‘Lena’ and ‘Mandrill’ images which have had the contrast adjusted by -128 (decreased) and +128 (increased):


‘Lena’ image with contrast adjusted by -128 (left) and +128 (right)
(click images to enlarge)


‘Mandrill’ image with contrast adjusted by -128 (left) and +128 (right)
(click images to enlarge)

Article copyright © 2008, 2010, 2015 Francis G. Loch

42 thoughts on “Image Processing Algorithms Part 5: Contrast Adjustment”

  1. Hi, I would like to ask, where did you get the formula for contrast correction factor?any reference?

    Thanks

  2. Hi Fatima,

    Because I’m using a range of -255 to 255 here to adjust the contrast I have had to use this correction factor so that it will work with the second formula for the contrast adjustment. All the correction factor does is convert the range I’m using to a range of 0 to 129.5. If you are using a different range, e.g. -100 to 100, then that would obviously require a different correction factor to work properly.

    I’ve had a look back through my notes, but I couldn’t find where I originally got this from. I can’t remember if I had calculated this myself or if I’ve read it in a book or online.

    Kind regards,

    Francis

  3. Hi, could you please explain, why this works (especially why the mapping to 129.5 is needed)? I see it works, but I don’t understand the logic behind it. Thank you 🙂

  4. Thanks for your question Tiffany. Yes, I probably didn’t explain that part very well as to why the range is 0 to 129.5. This range is only for the contrast correction factor and does not represent a colour value per se. I’ll explain more about this later.

    To recap the formulas are:

    F = 259(C + 255) / 255(259 – C) for the contrast correction factor. C is the contrast (range = -255 to 255).

    and

    R’ = F(R-128) + 128 for the actual contrast adjustment. R is the red colour component (range = 0 to 255).

    Let’s assume that C is zero (i.e. no change in contrast). Using the first formula we work out that F is 1. Using the second equation we see that R’ is the same as R. I’ll illustrate this with some values:

    R / R’
    0 = 0
    64 = 64
    128 = 128
    196 = 196
    255 = 255

    Let’s now assume that C is -255. F now becomes zero. The R’ values will then become:

    R / R’
    0 = 128
    64 = 128
    128 = 128
    196 = 128
    255 = 128

    As you can see with C turned all the way down to -255 the result is a mid-level grey no matter what the R value is.

    Let’s now assume that C is 255. F becomes 129.5. The R’ values will now become:

    R / R’
    0 = -16448
    64 = -8160
    128 = 128
    196 = 8934
    255 = 16575

    And because the R’ values are outside of the acceptable range these will be truncated so in reality the values are:

    R / R’
    0 = 0
    64 = 0
    128 = 128
    196 = 255
    255 = 255

    Now to explain the reason why the range of the contrast correction factor is 0 to 129.5. Yes it’s true that the range should be 0 to 128, but the value of 259 used in the contrast correction factor formula has been rounded for simplicity. To get a range closer to 0 to 128 you would need to use a value of 259.047619047619 instead. However, as you can see from the R’ values you get at the higher contrast levels they end up being quite extreme and end up being truncated anyway so that fact that the range is 0 to 129.5 instead of 0 to 128 is not very significant.

    I hope that explains things more clearly for you. If you still have any questions then please do not hesitate to get back in touch with me.

    1. Hi Jay,

      That’s correct. Decreasing the contrast of an image (i.e. using negative values) causes the colours to move further away from the maximum and minimum intensity values (e.g. 0 and 255). Reducing the contrast all the way down will cause the image to become a pure grey colour with an intensity at the midpoint (e.g. 128).

      Conversely, increasing the contrast (i.e. using positive values) causes the colour intensities to move closer to the maximum and minimum intensity values. At maximum contrast, all the colours will most likely be either 0 or maximum intensity.

      If you have a read further up, I did touch on some of the maths of this. If you have any further questions then please feel free to ask.

      Kind regards,

      Francis

      1. Hi Felix,

        Make sure that you are using a float and not an integer for the ‘factor’ variable. If you still cannot get it working, please feel free to post an example of your code so that I can have a look at it and maybe point you in the right direction.

        Kind regards,

        Francis

  5. I was wondering whether there is a way to soften the contrast. I tried adjusting the constants but to no avail.

    I’ll really appreciate a reply even though your post is so old. It’ll be of great help!

    Thank you very much 🙂

    -Evan

    1. No problem Evan, I’m always happy to help. (^_^)

      By “soften the contrast” do you mean reduce the amount of contrast? If so, it should just be a case of using negative numbers (e.g. -128) for your contrast to do that. For example:

      contrast = -128
      factor = (259 * (contrast + 255)) / (255 * (259 – contrast))
      colour = GetPixelColour(x, y)
      newRed = Truncate(factor * (Red(colour) – 128) + 128)
      newGreen = Truncate(factor * (Green(colour) – 128) + 128)
      newBlue = Truncate(factor * (Blue(colour) – 128) + 128)
      PutPixelColour(x, y) = RGB(newRed, newGreen, newBlue)

      Remember that the Truncate() function truncates the number to a value in the range of 0 to 255:

      Procedure Truncate(value)
      If value < 0 Then value = 0 If value > 255 Then value = 255
      Return value
      EndProcedure

      If I have misunderstood, could you give me an example of what you are trying to achieve?

      Kind regards,

      Francis

      1. Hi Francis,
        Thanks so much for replying so quickly! I think what I was getting at, was say instead of using 128 as the contrast, maybe a lesser number? The change in contrast was a little bit too much for me. I tried using lesser numbers like 30, 40, or 90 but the contrast was either too strong, or none at all.
        Does this help?
        Thank you very nuch!
        – Evan

        1. Hi Evan,

          Yes, I understand now.

          You should be able to see a subtle change in contrast going from, say, 128 to 127. The only thing I can think of is that you have the ‘factor’ variable set up for integer numbers instead of floating point numbers. If the variable is an integer it will only store numbers like 0 and 1 so you will get no change or quite a dramatic change. If the variable is a floating point number you will get numbers like 0.1 and 0.5 which should give you subtler changes.

          Kind regards,

          Francis

          1. Hi Evan,

            Glad you got it sorted.

            I’ll update the article to clarify that the factor variable should be a float and not an integer to avoid further confusion.

            Kind regards,

            Francis

  6. Why does my image turns yellow when the contrast is increased especially the black section of the image

    thanks in advance!

  7. Awesome tutorial Francis! Very useful! I was wondering if I need to make an image 2x more contrast, what should be the level C I’m setting? Since this is not a percentage… I’m not sure what C increases the contrast 1x and 2x.

    1. I’m glad you found it useful! (^_^)

      As for giving an image 2x more contrast, I’m not sure if there is an easy way of doing this. If I understand correctly what you are trying to achieve, it would involve you determining how much contrast is already present in the image and then working out what value of C would give you double that amount of contrast (which will vary between different images).

      As I understand the method for measuring the amount of contrast in an image you need to do something like the following:

      1. Work out the average pixel brightness of the image (i.e. add up the brightness of all the pixels in the image and then divide by the number of pixels).
      2. Work out the difference between the brightness of each pixel and the average brightness.
      3. Square the results of each pixel and add them together.
      4. Divide by the total number of pixels.

      If you have an image that is just made up of the one colour (e.g. pure black or white) then your contrast value should be 0.

      I have never tried doing something like this myself, but it should hopefully give you somewhere to start.

      Kind regards,

      Francis

    1. I’m not sure. Double check the maths, and check that the variable types you have used make sense (e.g. that you are not using a variable defined as a byte to store a number in the -255 to 255 range).

      If you still can’t get it working, you could post an example of the code here so that I can have a look.

      Kind regards,

      Francis

      1. Alright , It turns out , that using float as the factor value fixes the problem. I had used an integer , as the factor value which might have caused the problem.
        Thanks Francis.

  8. good afternoon
    please tell me, can you have information about the article 56 issue kriptmag? the network does not have it anymore, I would like to read more
    thanks for the answer

    1. Hi there,

      Sorry for the late reply, but I have been on holiday.

      The algorithm was never designed to go outside of the range of -255 to +255, but I gave it a try to see what would happen.

      If you keep reducing the contrast from -255 the image starts off as being grey and gradually you can start to see a version of the image where the colours are inverted.

      If you increase the contrast from 256 up to 258 there is no major difference from what you see at 255. At 259 the image just becomes black (caused by a division by zero error). At 260 and above you have a version of the image where the colours are inverted.

      Kind regards,

      Francis

  9. Hi,
    the formula is working perfeckt for me really thanks man! but I have one wierd thing to ask about, when I set the value to lets say 100 (it will work fine) then back to 0 again so the value now is -100 (which should theoritically set the pixel colors back) some pixels are losing data (because of the truncate each time editing the pixels color) and after several times making the contrast hight and low (max and min) when i set it back to 0 it would be more grey (each value of each color of each pixel is reduced)

    1. Hi Karro,

      I’m glad that the formula is working for you.

      The image degradation is an unfortunate side-effect of the truncating of the pixel information. There are two ways that I can think of off the top of my head to get around this:

      1. Keep a copy of the original image in memory and perform all operations from that rather each subsequently processed image.

      2. Use a custom high accuracy format for the pixel data which allows you to get around the normal limits (e.g. instead of the 0-255 limit for the red, green and blue channels use 0-65535) and convert and truncate when you are actually displaying or saving the image.

      Kind regards,

      Francis

  10. I want to create just 1 function to adjust both brightness and contrast.
    e.g. AdjustAll (IMAGE img, int brightness, int contrast)

    does the order of operation (brightness then contrast or contrast then brightness) makes a difference?

    1. Hi Lawrence,

      The order of the operations will make a difference to the final result. Typically you would adjust the contrast first and then the brightness.

      There is already a function that allows you to adjust brightness, contrast and also saturation at the same time, however it is a bit more complex. Have a look at https://docs.rainmeter.net/tips/colormatrix-guide/ which documents it quite well.

      Kind regards,

      Francis

      1. Thank you for the reply. Contrast first the Brightness second is what I wanted to double check.

        I also took a quick look at the link and yes it was more than I bargain for. However, it looks like a great article and I will study it slowly.

        1. Hi Lawrence,

          If I get a chance I may write an article which explains the brightness, contrast and saturation function that is easier to follow.

          Kind regards,

          Francis

      2. One more quick question on implementation. Should I truncate as I go or truncate only at the final value?

        For example (using Red Pixel value as an example):

        // apply contrast 1st
        Red = (int)(factor * (Red – 128) + 128);
        Red = Truncate(Red);
        // then brightness 2nd (over the newly adjusted contrast values)
        Red = Red + brightness;
        return Truncate(Red);

        or

        Red = (int)(factor * (Red – 128) + 128) + brightness;
        return Truncate(Red);

        1. Hi Lawrence,

          The reason we truncate the value is so that it is in a format suitable for the pixel colour data (i.e. 0 to 255), so we only need to truncate the value at the end of the function.

          Kind regards,

          Francis

          1. Sorry, I was a bit too hasty with my previous answer and I didn’t actually think it through. You will need to truncate at each stage since the contrast function, prior to truncating, can produce very large values. If this is not truncated then the brightness part of the function is not going to have any effect.

            The code below is similar to what I use for my own stuff that allows you to change to the brightness, contrast and saturation for a given pixel. The values next to the procedure’s input parameters are the defaults that will cause no change. Increase or decrease these values to affect the pixel.

            Procedure Transform(pixel, Brightness.f=0, Contrast.f=1, Saturation.f=1)

            t.f=(1-Contrast)/2
            sR.f=(1-Saturation)*0.3086
            sG.f=(1-Saturation)*0.6094
            sB.f=(1-Saturation)*0.0820

            rR.f=Contrast*(sR+Saturation)
            rG.f=Contrast*sR
            rB.f=Contrast*sR
            rA.f=0
            rW.f=0

            gR.f=Contrast*sG
            gG.f=Contrast*(sG+Saturation)
            gB.f=Contrast*sG
            gA.f=0
            gW.f=0

            bR.f=Contrast*sB
            bG.f=Contrast*sB
            bB.f=Contrast*(sB+Saturation)
            bA.f=0
            bW.f=0

            aR.f=0
            aG.f=0
            aB.f=0
            aA.f=1
            aW.f=0

            wR.f=(t+Brightness)*255
            wG.f=(t+Brightness)*255
            wB.f=(t+Brightness)*255
            wA.f=0
            wW.f=1

            pR=Red(pixel)
            pG=Green(pixel)
            pB=Blue(pixel)
            pA=Alpha(pixel)

            r=(rR*pR)+(gR*pG)+(bR*pB)+(aR*pA)+wR
            g=(rG*pR)+(gG*pG)+(bG*pB)+(aG*pA)+wG
            b=(rB*pR)+(gB*pG)+(bB*pB)+(aB*pA)+wB
            a=(rA*pR)+(gA*pG)+(bA*pB)+(aA*pA)+wA

            TruncateRGBA(r,g,b,a)

            ProcedureReturn RGBA(r,g,b,a)

            EndProcedure

            Kind regards,

            Francis

  11. Hi Francis Loch,
    Thank your supporting !
    I have a issue. I want to adjust Contrast in YUV (YCrCb) color space. Really, I don’t want to convert YUV -> RGB -> YUV . Can you give me any solution for this problem, pls ?
    Thanks one more !

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Homepage of Francis G. Loch's various projects