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?
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.