在C#中高效地调整图像对比度

12

有没有一种有效的方法在C#中调整图像的对比度?

我看到这篇文章提倡进行逐像素操作。速度不快。

我已经在某些地方使用了颜色矩阵,并发现它们很快。有没有一种使用它们来调整对比度的方法?(注意:这个人搞错了。)

我还在使用EmguCV。我注意到OpenCV(Emgu包装的内容)似乎有一个对比度函数 - 有没有办法通过Emgu访问它?目前我在Emgu中所能做的就是归一化直方图,这确实会改变对比度,但我无法控制它。

有人有什么想法吗?


你为什么说“那个人”做错了呢? - DkAngelito
@DkAngelito 哇,这是很久以前的事了。如果我没记错,ColorMatrix方法不能将值向中间点移动,这才是对比度调整的真正含义。如果有帮助的话,下面MusicGenesis所接受的答案似乎已经得到了最优解的共识。 - Tom Wright
4个回答

35
如果您在该示例中的代码可以工作,那么您可以通过使用Bitmap.LockBits大大加快速度(几个数量级),它返回一个BitmapData对象,通过指针访问位图的像素数据。网上和StackOverflow上有很多示例展示如何使用LockBits。 Bitmap.SetPixel()Bitmap.GetPixel()是已知的最慢的方法,它们都使用Color类,这是已知的最慢的类。它们应该被命名为Bitmap.GetPixelAndByGodYoullBeSorryYouDid()Bitmap.SetPixelWhileGettingCoffee以警告不谨慎的开发人员。
更新:如果您要修改该示例中的代码,请注意以下代码块:
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();

可以用这个替代:

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

更新2:这里是使用LockBits方法的AdjustContrast方法(包括一些其他速度优化):

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;
}

注意:此代码需要在你的类的using语句中使用System.Drawing.Imaging,并且要求项目的“允许不安全代码”选项被选中(在项目的生成属性选项卡上)。

GetPixel和SetPixel方法之所以在逐像素操作时如此缓慢,是因为方法调用本身的开销开始成为一个巨大的因素。通常情况下,我的代码示例会被认为是重构的一个候选,因为你可以编写自己的SetPixel和GetPixel方法来使用现有的BitmapData对象,但每次调用方法的开销相对于函数内部数学处理时间非常小。这就是我也删除了原始方法中的Clamp调用的原因。

另一种加速的方法是将其变成一个“破坏性”函数,并修改传递的Bitmap参数,而不是创建副本并返回修改后的副本。


没错,就像有一份工作一样。 :) - MusiGenesis
2
这里有一个更快的方法,有人发布了使用安全代码的方法:https://dev59.com/Xl_Va4cB1Zd3GeqPVKPd - Trevor Elliott
谢谢!我也用JS重写了它以供我的使用! - Rutwick Gangurde
嗯...我遇到了一些奇怪的行为。首先:对比度值应该在哪个范围内?其次:为什么这个算法会在图像中创建垂直条纹和色彩偏移? - AllDayPiano
@AllDayPiano:来自原始文章:<param name="Value">用于设置对比度(-100到100)</param>。如果您的“Value”超出了该范围,可能就是为什么会看到条纹等问题的原因。 - MusiGenesis
由于某种原因,这给了我一个错误:在contrast-test.exe中发生了未处理的类型为'System.AccessViolationException'的异常。尝试读取或写入受保护的内存。 - mrid

4

@MusiGenesis,

我想指出我使用了这种方法来编写一个图像编辑器。它效果很好,但有时这种方法会在以下代码行触发AccessViolationException:

byte B = row[columnOffset];

我发现问题是由于位深度没有标准化造成的,如果一张图片是32位色彩,那么我就会遇到这个错误。所以我更改了以下这行代码:

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); 

希望这可以帮助您解决问题。感谢您的帖子。Jib

1
你可能想使用 PixelFormat.Format32bppArgb,因为这是默认的 .NET 位图格式。 - MusiGenesis
这是更好的结果 - Akbar Asghari

0

1
这是Bob在我提出的问题中链接文章中犯的同样错误:色彩矩阵不适用于对比度调整。这是因为更改是相对于中灰进行的,而不是黑色或白色。 - Tom Wright
仅仅是为了纠正这个问题, 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} 在上面的例子中完成了工作。第五行添加了一个可以调整的偏移量。这个(0.8)会产生"高键"效果。0.2是非常低的对比度。 - nick66

0
这是我基于this进行的优化。
在计算字节时,我减少了数学运算的数量,并且添加了一个RGBA循环,以减少冗余代码。希望这对你有用。
public static unsafe Bitmap SetContrast(Bitmap initial, float contrast) {

        Bitmap result = (Bitmap)initial.Clone();
        BitmapData data = result.LockBits(
            new Rectangle(0, 0, result.Width, result.Height),
            ImageLockMode.ReadWrite, 
            PixelFormat.Format32bppArgb); 

            int pixelSize = 4;    
            
            byte[] contrast_lookup = new byte[256];
            double newValue = 0;    
            
            double value1 = Math.Pow(1.0 + contrast / 100.0, 2.0);
            
            double value2 = (1.0 - value1) * 127.5;    
           
            for (int i = 0; i < 256; i++)
            {                    
                newValue = (double)i * value1 + value2;                    
                newValue = newValue < 0.0 ? 0.0 : newValue > 255.0 ? 255.0 : newValue;                   
                contrast_lookup[i] = (byte)newValue;
            }    
            
            for (int y = 0; y < result.Height; ++y)
            {                    
                byte* row = (byte*)data.Scan0 + (y * data.Stride);
                for (int x = 0; x < result.Width; ++x)                    
                    for (int i = 0; i < pixelSize; i++) 
                        row[x * pixelSize + i] = contrast_lookup[row[x * pixelSize+ i]];        
            }          

        result.UnlockBits(data);    
        return result;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接