12

Is there an efficient way of adjusting the contrast of an image in C#?

I've seen this article which advocates doing a per-pixel operation. Not quick.

I'm using colour matrices in places already and find them to be quick. Is there a way to adjust contrast using them? (Note: This guy gets it wrong.)

I'm also using EmguCV. I notice that OpenCV (which Emgu wraps) seems to have a contrast function - is there any way of accessing this through Emgu? At the moment all I can do in Emgu is normalise the histogram, which does change the contrast, but not with any degree of control on my part.

Anyone got any ideas?

Tom Wright
  • 11,278
  • 15
  • 74
  • 148
  • Why do you say that "that guy" gets it wrong? – DkAngelito Nov 11 '14 at 16:58
  • @DkAngelito Wow, this is from a long time ago. If memory serves, the ColorMatrix approach can't shift values away / towards the mid-point, which is what a contrast adjustment really is. If it helps, the accepted answer by MusicGenesis below seems to have attracted consensus as optimal. – Tom Wright Nov 17 '14 at 08:31

3 Answers3

35

If the code in that sample works for you, you can speed it up massively (by orders of magnitude) by using Bitmap.LockBits, which returns a BitmapData object that allows access to the Bitmap's pixel data via pointers. There are numerous samples on the web and on StackOverflow that show how to use LockBits.

Bitmap.SetPixel() and Bitmap.GetPixel() are the slowest methods known to mankind, and they both utilize the Color class, which is the slowest class known to mankind. They should have been named Bitmap.GetPixelAndByGodYoullBeSorryYouDid() and Bitmap.SetPixelWhileGettingCoffee as a warning to unwary developers.

Update: If you're going to modify the code in that sample, note that this chunk:

System.Drawing.Bitmap TempBitmap = Image;
System.Drawing.Bitmap NewBitmap = new System.Drawing.Bitmap(TempBitmap.Width,
    TempBitmap.Height);
System.Drawing.Graphics NewGraphics = 
    System.Drawing.Graphics.FromImage(NewBitmap);
NewGraphics.DrawImage(TempBitmap, new System.Drawing.Rectangle(0, 0, 
    TempBitmap.Width, TempBitmap.Height), 
    new System.Drawing.Rectangle(0, 0, TempBitmap.Width, TempBitmap.Height),
    System.Drawing.GraphicsUnit.Pixel);
NewGraphics.Dispose();

can be replaced with this:

Bitmap NewBitmap = (Bitmap)Image.Clone();

Update 2: Here is the LockBits version of the AdjustContrast method (with a few other speed improvements):

public static Bitmap AdjustContrast(Bitmap Image, float Value)
{
    Value = (100.0f + Value) / 100.0f;
    Value *= Value;
    Bitmap NewBitmap = (Bitmap)Image.Clone();
    BitmapData data = NewBitmap.LockBits(
        new Rectangle(0, 0, NewBitmap.Width, NewBitmap.Height), 
        ImageLockMode.ReadWrite,
        NewBitmap.PixelFormat);
    int Height = NewBitmap.Height;
    int Width = NewBitmap.Width;

    unsafe
    {
        for (int y = 0; y < Height; ++y)
        {
            byte* row = (byte*)data.Scan0 + (y * data.Stride);
            int columnOffset = 0;
            for (int x = 0; x < Width; ++x)
            {
                byte B = row[columnOffset];
                byte G = row[columnOffset + 1];
                byte R = row[columnOffset + 2];

                float Red = R / 255.0f;
                float Green = G / 255.0f;
                float Blue = B / 255.0f;
                Red = (((Red - 0.5f) * Value) + 0.5f) * 255.0f;
                Green = (((Green - 0.5f) * Value) + 0.5f) * 255.0f;
                Blue = (((Blue - 0.5f) * Value) + 0.5f) * 255.0f;

                int iR = (int)Red;
                iR = iR > 255 ? 255 : iR;
                iR = iR < 0 ? 0 : iR;
                int iG = (int)Green;
                iG = iG > 255 ? 255 : iG;
                iG = iG < 0 ? 0 : iG;
                int iB = (int)Blue;
                iB = iB > 255 ? 255 : iB;
                iB = iB < 0 ? 0 : iB;

                row[columnOffset] = (byte)iB;
                row[columnOffset + 1] = (byte)iG;
                row[columnOffset + 2] = (byte)iR;

                columnOffset += 4;
            }
        }
    }

    NewBitmap.UnlockBits(data);

    return NewBitmap;
}

NOTE: this code requires using System.Drawing.Imaging; in your class' using statements, and it requires that the project's allow unsafe code option be checked (on the Build Properties tab for the project).

One of the reasons GetPixel and SetPixel are so slow for pixel-by-pixel operations is that the overhead of the method call itself starts to become a huge factor. Normally, my code sample here would be considered a candidate for refactoring, since you could write your own SetPixel and GetPixel methods that use an existing BitmapData object, but the processing time for the math inside the functions would be very small relative to the method overhead of each call. This is why I removed the Clamp calls in the original method as well.

One other way to speed this up would be to simply make it a "destructive" function, and modify the passed Bitmap parameter instead of making a copy and returning the modified copy.

MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • Right, like *I* have a job. :) – MusiGenesis Jun 25 '10 at 03:02
  • 2
    Here's a faster method someone posted which uses safe code: http://stackoverflow.com/questions/8990926/faster-contrast-algorithm-for-a-bitmap – Trevor Elliott Jan 24 '12 at 17:47
  • Thanks! I too rewrote it in JS for my use! – Rutwick Gangurde Dec 31 '12 at 10:00
  • Hmm... I got some strange behavior. first of all: What range should the contrast value be within? And second: Why is this algorithm creating vertical stripes and color displacements in the image? – AllDayPiano Mar 30 '16 at 09:42
  • @AllDayPiano: from the original article: `Used to set the contrast (-100 to 100)`. If your `Value` is outside that range, perhaps that's why you're seeing stripes etc. – MusiGenesis Mar 30 '16 at 15:15
  • For some reason, this gives me the error: An unhandled exception of type 'System.AccessViolationException' occurred in contrast-test.exe Attempted to read or write protected memory. – mrid Dec 02 '19 at 11:18
4

@MusiGenesis,

Just wanted to note that I used this method for an image editor I've been writing. It works well, but sometimes this method triggers an AccessViolationException on this line:

byte B = row[columnOffset];

I realised it was because there was no standardisation of BitDepth, so if an image was 32 bit colour I was getting this error. So I changed this line:

BitmapData data = NewBitmap.LockBits(new Rectangle(0, 0, NewBitmap.Width, NewBitmap.Height),  ImageLockMode.ReadWrite, NewBitmap.PixelFormat);

to:

BitmapData data = NewBitmap.LockBits(new Rectangle(0, 0, NewBitmap.Width, NewBitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb); 

Hope this helps as it seems to have eradicated my problem.

Thanks for the post.

Jib

CoolBeans
  • 20,654
  • 10
  • 86
  • 101
Mike
  • 61
  • 5
0

I'm a bit late, but use a color matrix implementation as these will be optimised for such transformations and is much easier than manipulating the pixels yourself: http://www.geekpedia.com/tutorial202_Using-the-ColorMatrix-in-Csharp.html

rbcc
  • 2,452
  • 3
  • 26
  • 28
  • 1
    This is the same mistake Bob makes in the article I link to in my question: Colour Matrices are not suitable for contrast alterations. This is because the changes are made relative to the middle grey, not black or white. – Tom Wright May 18 '12 at 11:25
  • Just to correct this, ` new float[]{0.1f, 0, 0, 0, 0}, new float[]{0, 0.1f, 0, 0, 0}, new float[]{0, 0, 0.1f, 0, 0}, new float[]{0, 0, 0, 1, 0}, new float[]{0.8f, 0.8f, 0.8f, 0, 1}` Does the job in the above example. The fifth row adds an offset which can be adjusted. This (0.8) give a "high-Key" effect. 0.2 is very low contrast. – nick66 Feb 06 '14 at 21:24