用于计算图像中唯一颜色数量的算法
寻找足够快的内存并保持优美状态的内存。该图像是一个24bpp的System.Drawing.Bitmap。
解决方案
如果我们需要一个确切的数字,那么我们将不得不遍历所有像素。由于颜色稀疏,因此最好将颜色和计数存储在哈希中是最好的方法。
在哈希中使用Color.ToArgb()而不是颜色对象也可能是一个好主意。
此外,如果速度是主要问题,则我们不希望使用GetPixel(x,y)之类的函数-而是尝试一次(一次)处理块。如果可以,请获取指向图像内存开头的指针,并使其不安全。
在大多数计算机以256色调色板模式运行的现代图形卡之前,这是一个相当令人感兴趣的领域。处理能力和内存的限制仅施加了某种约束,这可能对我们有用,因此,搜索用于处理调色板的算法可能会带来一些用处。
这取决于我们要分析的图像类型。对于24位图像,我们将需要最多2MB的内存(因为在最坏的情况下,我们必须处理每种颜色)。为此,最好使用位图(我们有一个2 MB的位图,其中每个位对应一种颜色)。对于可以在O(#pixels)中实现的具有高色数的图像,这将是一个很好的解决方案。对于16位图像,使用此技术,我们只需要8 kB的位图即可。
但是,如果图片色彩不多,最好使用其他颜色。但是,那么我们将需要某种检查来表明我们应该使用哪种算法...
以前从未实现过这样的事情,但是据我所知,它是一个原始的实现:
对于24位图像,图像可能具有的最大颜色数为(2 ^ 24,图像像素数)的最小值。
我们只需要记录是否已对特定颜色进行计数,而无需记录已被计数了多少次。这意味着我们需要一位来记录是否对每种颜色进行计数。那是2MB的内存。遍历像素,在2MB颜色设置图中设置相关位。最后,遍历颜色设置图,对设置的位进行计数(如果幸运的话,我们将有一条POPCNT指令来帮助实现此目的)。
对于较小的图像和较低的色深,最好保留一个色表并为图像中的每种颜色计数。
var cnt = new HashSet<System.Drawing.Color>(); foreach (Color pixel in image) cnt.Add(pixel); Console.WriteLine("The image has {0} distinct colours.", cnt.Count);
/ EDIT:正如Lou所说,使用.GetArgb()
而不是Color
值本身可能会更快一些,因为Color
实现了GetHashCode`的方式。
图像中唯一颜色的最大数量等于像素数量,因此从过程开始就可以预见。
然后,使用Konrad提出的HashSet方法似乎是一个合理的解决方案,因为哈希的大小应不大于像素数,而使用JeeBee建议的位图方法则需要512 MB的32位图像(如果有Alpha通道,并且确定为有助于颜色的唯一性)
但是,HashSet方法的性能可能会比"每种颜色的位数"方法的性能差,我们可能要尝试使用多种颜色的图像并同时进行一些基准测试
我们没有确切定义独特的颜色。如果我们实际上是指真正唯一的代码值(而不是视觉上相同),那么唯一的精确解决方案是使用其他答案中所述的一种方法来对它们进行计数。
如果我们正在寻找视觉上相似的颜色,这确实可以快速归结为一个调色板映射问题,在这里我们要寻找256种最佳独特颜色来最紧密地代表原始的全动态颜色范围图像。对于大多数的图像,令人叹为观止的图像从24位和多达有多好减少到1600万不同的颜色下手可以映射到图像时,仅256独特的颜色时,这些256种颜色选择得当。已经证明,正确选择这256种颜色的最佳选择(对于此示例)是NP完整的,但是有些实用的解决方案可能非常接近。搜索一个名叫万世杰的人的论文,以及在他的作品中建立的东西。
如果我们正在寻找图像中代码值颜色的数量的近似值,则可以使用无损压缩方案来压缩图像。压缩率将直接与图像中唯一代码值的数量有关。我们甚至不必保留压缩的输出,只需要沿途累加字节数并丢弃实际的输出数据即可。使用一组样本图像作为参考,我们可以在图像的压缩率和不同代码值的数量之间建立查找表。同样,最后一种技术虽然相当快,但肯定会是一个近似值,但是它应该具有很好的相关性。
这里的大多数人都提出了可能更快的解决方案(实际上,仅使用2 MB的解决方案在内存使用方面可能是可接受的,而且速度非常快;带有哈希的解决方案甚至可能更快,但肯定会使用2 MB以上的内存)。记忆)。编程始终是内存使用量和CPU时间之间的折衷方案。如果我们愿意"浪费"更多的内存,通常可以更快地获得结果,或者通过"浪费"更多的计算时间来获得较慢的结果,但是这通常可以为我们节省很多内存。
到目前为止,还没有人建议过这种解决方案。它可能是花费最少的内存(我们可以对其进行优化,因此它几乎不会使用比将映像保留在内存中所需的内存更多的内存,但是,映像将被更改,尽管我们可能必须先复制它)。我怀疑它能否在速度上超过哈希或者位掩码解决方案,如果我们最关心内存,这很有趣。
- 按颜色对图像中的像素进行排序。我们可以轻松地将每个像素转换为32位数字,并且可以将32位数字相互比较,一个数字小于另一个数字(大于或者等于)。如果我们使用Quicksort,则不需要额外的存储空间来进行排序,除了额外的堆栈空间。如果使用Shellsort,则根本不需要额外的内存(尽管Shellsort会比Quicksort慢得多)。 int num =(红色<< 16)+(绿色<< 8)+蓝色;
- 一旦对像素进行了排序(这意味着我们已在图像中对其进行了重新排列),所有颜色相同的像素将始终彼此相邻。因此,我们只需遍历图像一次,然后查看颜色变化的频率。例如。我们将像素的当前颜色存储在(0,0),并使用值1初始化一个计数器。下一步是转到(0,1)。如果颜色与以前相同,则无需执行任何操作,请继续下一个像素(0,2)。但是,如果不相同,则将计数器增加1并记住该像素的颜色,以进行下一次迭代。
- 一旦我们查看了最后一个像素(如果与第二个最后一个像素不同,可能又增加了计数器),计数器将包含唯一颜色的数量。
无论采用哪种解决方案,在任何情况下都必须至少对所有像素进行一次迭代,因此,与其他解决方案相比,此解决方案的速度变慢或者变快都不会产生影响。该算法的速度取决于我们可以按颜色对图像像素进行排序的速度。
就像我说的那样,当速度成为主要问题时,很容易击败该算法(这里的其他解决方案可能都更快),但是我怀疑,当存储器使用成为主要关注点时,是否可以击败这种算法,因为除了计数器之外,还有足够的存储空间可以存储一种颜色,并为图像本身存储空间,如果我们选择的排序算法需要任何存储空间,则仅需要额外的内存。
这里的大多数其他实现将很慢。为了做到这一点,我们需要直接访问扫描线和某种稀疏矩阵来存储颜色数据。
首先,我将描述32bpp的情况,这要容易得多:
- HashSet:颜色的稀疏矩阵
- ImageData:使用BitmapData对象直接访问基础内存
- PixelAccess:使用int *将内存引用为可以迭代的int
对于每次迭代,只需执行该整数的hashset.add即可。最后,只需查看HashSet中有多少个键,这就是颜色的总数。重要的是要注意,调整HashSet的大小确实很麻烦(O(n),其中n是集合中的项目数),因此我们可能想构造一个大小合理的HashSet开头,例如imageHeight * imageWidth / 4就好了。
在24bpp的情况下,PixelAccess必须是一个字节*,并且我们需要为每种颜色迭代3个字节以构造一个int。对于这组3中的每个字节,第一个左移8位(一个字节),并将其加到一个整数中。现在,我们有一个由32位int表示的24bpp Color,其余的都一样。
现代流行的颜色量化实现使用八叉树数据结构。注意维基百科页面,内容相当不错。八叉树的优点是可以根据需要限制内存,因此我们可以对整个图像进行采样,并在不增加额外内存的情况下决定调色板。一旦理解了这个概念,就可以链接到1996年Dobb博士的期刊文章的源代码。
由于这是一个问题,请参阅2003年5月MSDN上的文章"为ASP.NET图像优化颜色量化",其中包括一些源代码。