如何计算位图的平均RGB颜色值

20
在我的 C#(3.5)应用程序中,我需要获取位图的红色、绿色和蓝色通道的平均颜色值。最好不使用外部库。这个可以做到吗?如果可以,怎么做?谢谢! 尝试更精确地说明:位图中的每个像素具有特定的 RGB 颜色值。我想得到图像中所有像素的平均 RGB 值。

2
好的,朴素的方法是逐像素获取RGB值,我相信这不是你要求的。你能详细说明你需要什么样的平均值吗? - lc.
你说得对。希望现在好些了。 - Mats
逐像素处理可以有不同的方法 - 请参阅答案。我想知道GPU是否有帮助。 - bohdan_trotsenko
3个回答

27

使用不安全的代码是最快的方法:

BitmapData srcData = bm.LockBits(
            new Rectangle(0, 0, bm.Width, bm.Height), 
            ImageLockMode.ReadOnly, 
            PixelFormat.Format32bppArgb);

int stride = srcData.Stride;

IntPtr Scan0 = srcData.Scan0;

long[] totals = new long[] {0,0,0};

int width = bm.Width;
int height = bm.Height;

unsafe
{
  byte* p = (byte*) (void*) Scan0;

  for (int y = 0; y < height; y++)
  {
    for (int x = 0; x < width; x++)
    {
      for (int color = 0; color < 3; color++)
      {
        int idx = (y*stride) + x*4 + color;

        totals[color] += p[idx];
      }
    }
  }
}

int avgB = totals[0] / (width*height);
int avgG = totals[1] / (width*height);
int avgR = totals[2] / (width*height);

注意:我没有测试过这段代码...(可能有一些取巧的地方)

此代码还假设图像为32位。 对于24位图像,将x*4改为x*3


3
你能解释一下为什么它速度快那么多吗? - C. Ross
哇,用这种方法和使用 GetPixel 的差别惊人。使用 GetPixel 两次循环遍历一个 500x600 的图像需要约 1000 毫秒,而使用这种方法只需要约 13 毫秒。 - mpen
这段代码中有一个微妙的错误。红色和蓝色被交换了。Scan0中字节的顺序是BGRA而不是RGBA。 - Remco Ros
@RemcoRos 是的,这正是 David Hay 在早期评论中所说的。 - Philippe Leybaert
我和我的CSC似乎不理解dstData是从哪里来的。它是srcData的拼写错误还是发生了一些魔法? - prokilomer
显示剩余6条评论

14

这样的方法可以起作用,但可能速度不够快,因此并没有那么有用。

public static Color GetDominantColor(Bitmap bmp)
{

       //Used for tally
       int r = 0;
       int g = 0;
       int b = 0;

     int total = 0;

     for (int x = 0; x < bmp.Width; x++)
     {
          for (int y = 0; y < bmp.Height; y++)
          {
               Color clr = bmp.GetPixel(x, y);

               r += clr.R;
               g += clr.G;
               b += clr.B;

               total++;
          }
     }

     //Calculate average
     r /= total;
     g /= total;
     b /= total;

     return Color.FromArgb(r, g, b);
}

它会很慢。只是测试一下。GetPixel() 实际上是锁定位图并检索单个像素。 - bohdan_trotsenko

14
这里有一个更简单的方法:
Bitmap bmp = new Bitmap(1, 1);
Bitmap orig = (Bitmap)Bitmap.FromFile("path");
using (Graphics g = Graphics.FromImage(bmp))
{
    // updated: the Interpolation mode needs to be set to 
    // HighQualityBilinear or HighQualityBicubic or this method
    // doesn't work at all.  With either setting, the results are
    // slightly different from the averaging method.
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
    g.DrawImage(orig, new Rectangle(0, 0, 1, 1));
}
Color pixel = bmp.GetPixel(0, 0);
// pixel will contain average values for entire orig Bitmap
byte avgR = pixel.R; // etc.

基本上,您使用DrawImage将原始位图复制到1像素位图中。然后,该1像素的RGB值将表示整个原始值的平均值。GetPixel相对较慢,但仅当您逐像素在大位图上使用它时才如此。在这里调用一次不是什么大问题。
使用LockBits确实很快,但某些Windows用户具有防止执行“不安全”代码的安全策略。我提到这一点是因为最近这个事实让我受了点苦头。
更新:将InterpolationMode设置为HighQualityBicubic后,此方法需要的时间约为使用LockBits平均的两倍;使用HighQualityBilinear,则仅比LockBits稍长。因此,除非您的用户有禁止不安全代码的安全策略,否则绝对不要使用我的方法。
更新2:随着时间的推移,我现在意识到为什么这种方法根本行不通。即使是最高质量的插值算法也仅包含几个相邻像素,因此将图像压缩到一个像素大小的限制是存在的,无论使用什么算法都无法避免信息的丢失。
唯一的方法是逐步缩小图像(每次缩小一半),直到将其缩小到一个像素的大小。我无法用言语表达编写此类东西会是多么浪费时间,所以我很高兴当我想到它时停止了自己。:)
请不要再投票支持这个答案-这可能是我有史以来最愚蠢的想法。

哈哈,有趣。我想知道它是否比LockBits方法更快? (我会自己测试这两种方法的时间,但我懒得做!哈哈)+1 - CodeAndCats
2
尽管明确要求不要点赞,但我还是忍不住了。阅读你的答案让我受益匪浅。 - prokilomer
感谢提及安全策略阻止不安全的代码。 - Vasiliy
2
这不是一个愚蠢的想法。它是一个好主意,只是行不通而已。 :-) - Joe B
这并没有产生预期的主导颜色(与其他回复中的情况不同)。 - tmighty

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