如何从 CIAreaHistogram 中提取主色?

How to extract dominant color from CIAreaHistogram?

我想分析 iOS 上 UIImage 中最主要的颜色(颜色出现在大多数像素中),我偶然发现了 Core Image 基于 API 的过滤器,尤其是 CIAreaHistogram。

看起来这个过滤器可能对我有帮助,但我很难理解 API。首先,它表示过滤器的输出是一个一维图像,它是输入箱的长度和一个像素的高度。我如何读取这些数据?我基本上想找出频率最高的颜色值,所以我希望数据包含每种颜色的某种频率计数,我不清楚这个一维图像将如何表示,因为它实际上并不解释我在这个一维图像中可以预期的数据。如果它真的是直方图,为什么它不 return 像字典一样表示的数据结构

其次,在API中它要求有多少个箱子?输入应该是什么?如果我想要一个精确的分析,输入 bin 参数会是我图像的 color-space 吗?使 bin 值变小有什么作用,我想它只是通过欧几里得距离到最近的 bin 来近似附近的颜色。如果是这种情况,将不会产生精确的直方图结果,为什么有人要这样做?

从API的角度对上述两个问题的任何输入都会对我有很大帮助

CIAreaHistogram returns 一幅图像,其中每个像素的 reg、绿色、蓝色和 alpha 值表示图像中该色调的频率。您可以将该图像渲染为 UInt8 的数组以查看直方图数据。还有一个未记录的 outputData 值:

let filter = CIFilter(
    name: "CIAreaHistogram",
    withInputParameters: [kCIInputImageKey: image])!
let histogramData = filter.valueForKey("outputData")

但是,我发现 vImage 是处理直方图的更好框架。首先,您需要创建一个 vImage 图片格式:

var format = vImage_CGImageFormat(
    bitsPerComponent: 8,
    bitsPerPixel: 32,
    colorSpace: nil,
    bitmapInfo: CGBitmapInfo(
        rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue),
    version: 0,
    decode: nil,
    renderingIntent: .RenderingIntentDefault)

vImage 使用可以从 CGImage 而不是 CIImage 实例创建的图像缓冲区(您可以使用 [=25= 的 createCGImage 方法创建一个]. vImageBuffer_InitWithCGImage 将创建一个图像缓冲区:

var inBuffer: vImage_Buffer = vImage_Buffer()

vImageBuffer_InitWithCGImage(
    &inBuffer,
    &format,
    nil,
    imageRef,
    UInt32(kvImageNoFlags))

现在创建 Uint 的数组,它将保存四个通道的直方图值:

let red = [UInt](count: 256, repeatedValue: 0)
let green = [UInt](count: 256, repeatedValue: 0)
let blue = [UInt](count: 256, repeatedValue: 0)
let alpha = [UInt](count: 256, repeatedValue: 0)

let redPtr = UnsafeMutablePointer<vImagePixelCount>(red)
let greenPtr = UnsafeMutablePointer<vImagePixelCount>(green)
let bluePtr = UnsafeMutablePointer<vImagePixelCount>(blue)
let alphaPtr = UnsafeMutablePointer<vImagePixelCount>(alpha)

let rgba = [redPtr, greenPtr, bluePtr, alphaPtr]

let histogram = UnsafeMutablePointer<UnsafeMutablePointer<vImagePixelCount>>(rgba)

最后一步是执行计算,这将填充四个数组,并释放缓冲区的数据:

vImageHistogramCalculation_ARGB8888(&inBuffer, histogram, UInt32(kvImageNoFlags))

free(inBuffer.data)

快速检查不透明图像的 alpha 数组应产生 255 个零,最终值对应于图像中的像素数:

print(alpha) // [0, 0, 0, 0, 0 ... 409600]

直方图不会从视觉角度为您提供主色:半黄 {1,1,0} 半黑 {0,0,0} 的图像将给出与半黄图像相同的结果红色 {1,0,0} 并保持绿色 {0,1,0}.

希望这对您有所帮助,

西蒙

直方图方法的一个问题是您失去了颜色通道之间的相关性。也就是说,您的图像的一半可能是洋红色,一半是黄色。您会发现红色直方图全部位于 1.0 bin 中,但蓝色和绿色 bin 将在 0.0 和 1.0 之间均匀分布,中间没有任何东西。即使您可以非常确定红色是明亮的,您也无法说明 "predominant color"

的蓝色和绿色分量应该是多少

您可以使用具有 2**(8+8+8) 个 bin 的 3D 直方图,但这相当大并且您会发现信号非常稀疏。偶然情况下,三个像素可能会落在一个容器中,而在其他地方没有两个相同的像素,即使许多用户可以告诉您有一种主要颜色并且它与该像素无关。

您可以使 3D 直方图的分辨率低很多,并且(例如)每个颜色通道只有 16 个 bin。这样,箱子更有可能具有具有统计意义的人口计数。这应该为您提供一个起点,以找到该 bin 中局部像素群的平均值。如果每个 bin 都有一个计数和一个 {R,G,B} 总和,那么一旦您确定了最受欢迎的 bin,您就可以快速找到该 bin 中像素的平均颜色。这种方法仍然受到直方图网格的一些影响。与边缘相比,您更有可能识别网格单元中间的颜色。种群可能跨越多个网格单元。像 kmeans 这样的东西可能是另一种方法。

如果您只想要主要色调,那么转换为颜色 space(例如 HSV)后跟色调直方图就可以了。

我不知道 vImage、CI 或 MetalPerformanceShaders 中的任何过滤器可以为您做这些事情。您当然可以在 CPU 或 Metal 中编写代码,而不会遇到很多麻烦。

Ian Ollmann 仅针对色调计算直方图的想法非常简洁,可以使用简单的颜色内核来完成。这个内核 returns 只是图像色调的单色图像(基于 this original work

let shaderString = "kernel vec4 kernelFunc(__sample c)" +
"{" +
"    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" +
"    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));" +
"    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));" +

"    float d = q.x - min(q.w, q.y);" +
"    float e = 1.0e-10;" +
"    vec3 hsv = vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" +
"    return vec4(vec3(hsv.r), 1.0);" +
"}"

let colorKernel = CIColorKernel(string: shaderString)

如果我得到蓝天图像的色调,则生成的直方图如下所示:

...而温暖的日落会给出这样的直方图:

所以,这看起来是获得图像主色调的好方法。

西蒙