如何计算UIImage的平均颜色?

我想build立一个应用程序,让用户select一个图像,并输出“平均颜色”。

例如,这个图像:

在这里输入图像说明

平均颜色是绿/淡黄色。

目前,我得到了这个代码:

// In a UIColor extension public static func fromImage(image: UIImage) -> UIColor { var totalR: CGFloat = 0 var totalG: CGFloat = 0 var totalB: CGFloat = 0 var count: CGFloat = 0 for x in 0..<Int(image.size.width) { for y in 0..<Int(image.size.height) { count += 1 var rF: CGFloat = 0, gF: CGFloat = 0, bF: CGFloat = 0, aF: CGFloat = 0 image.getPixelColor(CGPoint(x: x, y: y)).getRed(&rF, green: &gF, blue: &bF, alpha: &aF) totalR += rF totalG += gF totalB += bF } } let averageR = totalR / count let averageG = totalG / count let averageB = totalB / count return UIColor(red: averageR, green: averageG, blue: averageB, alpha: 1.0) } 

getPixelColor被定义为:

 extension UIImage { func getPixelColor(pos: CGPoint) -> UIColor { let pixelData = CGDataProviderCopyData(CGImageGetDataProvider(self.CGImage)) let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData) let pixelInfo: Int = ((Int(self.size.width) * Int(pos.y)) + Int(pos.x)) * 4 let r = CGFloat(data[pixelInfo]) / CGFloat(255.0) let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0) let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0) let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0) return UIColor(red: r, green: g, blue: b, alpha: a) } } 

正如你所看到的,我在这里做的是非常天真的:我遍历图像中的所有像素,增加它们的RGB,并除以计数。

当我运行应用程序并select图像时,应用程序冻结。 我知道这是因为图像太大,两个嵌套for循环被执行太多次。

我想find一种方法来高效地获取图像的平均颜色。 我怎么做?

您需要使用Accelerate Library,Apple有一些手册,附带一些示例代码,它可以在Swift或ObjC中使用

下面是一个让你走的样本,我用它来计算一个人的心率和心率变异性,使用手指在相机镜头上的颜色变化。

完整的代码在这里: https : //github.com/timestocome/SwiftHeartRate/blob/master/Swift%20Pulse%20Reader/ViewController.swift

这是一个老版本的Swift,但是我想你会明白的。 我是以240 fps的速度进行的,但是裁剪出了一小部分图像。

相关代码在这里:

 // compute the brightness for reg, green, blue and total // pull out color values from pixels --- image is BGRA var greenVector:[Float] = Array(count: numberOfPixels, repeatedValue: 0.0) var blueVector:[Float] = Array(count: numberOfPixels, repeatedValue: 0.0) var redVector:[Float] = Array(count: numberOfPixels, repeatedValue: 0.0) vDSP_vfltu8(dataBuffer, 4, &blueVector, 1, vDSP_Length(numberOfPixels)) vDSP_vfltu8(dataBuffer+1, 4, &greenVector, 1, vDSP_Length(numberOfPixels)) vDSP_vfltu8(dataBuffer+2, 4, &redVector, 1, vDSP_Length(numberOfPixels)) // compute average per color var redAverage:Float = 0.0 var blueAverage:Float = 0.0 var greenAverage:Float = 0.0 vDSP_meamgv(&redVector, 1, &redAverage, vDSP_Length(numberOfPixels)) vDSP_meamgv(&greenVector, 1, &greenAverage, vDSP_Length(numberOfPixels)) vDSP_meamgv(&blueVector, 1, &blueAverage, vDSP_Length(numberOfPixels)) // convert to HSV ( hue, saturation, value ) // this gives faster, more accurate answer var hue: CGFloat = 0.0 var saturation: CGFloat = 0.0 var brightness: CGFloat = 0.0 var alpha: CGFloat = 1.0 var color: UIColor = UIColor(red: CGFloat(redAverage/255.0), green: CGFloat(greenAverage/255.0), blue: CGFloat(blueAverage/255.0), alpha: alpha) color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) // 5 count rolling average let currentHueAverage = hue/movingAverageCount movingAverageArray.removeAtIndex(0) movingAverageArray.append(currentHueAverage) let movingAverage = movingAverageArray[0] + movingAverageArray[1] + movingAverageArray[2] + movingAverageArray[3] + movingAverageArray[4] 

这不是一个真正的“答案”,但我觉得我可以提供一些关于颜色检测的提示,为了什么是值得的,所以我们走吧。

调整

在你的情况下,速度最大的窍门是将图像调整到合理尺寸的平方。

没有什么神奇的价值,因为它取决于图像是否嘈杂等,但小于300×300,以您的采样方法为目标似乎是可以接受的,例如(尽pipe不要太过分)。

使用快速resize的方法 – 不需要保持比例,抗锯齿或任何东西(在SO上有许多实现)。 我们正在计算颜色,我们对图像显示的方面不感兴趣。

我们从resize获得的速度增益非常值得在resize时丢失的几个周期。

步进

第二招是通过步进进行采样。

对于大多数照片,您可以负担得起对每个其他像素或每一行进行采样,并保持相同的颜色检测精度。

你也可以不抽样(或丢弃一次抽样)的几个像素宽的大多数照片的边界 – 因为边界,框架,小插曲等。它有助于做出平均值(你想放弃所有的边缘,可以偏差结果不必要)。

滤除噪音

为了确保采样的准确性,您必须丢弃噪声:如果保留所有灰度,则所有检测结果都将变得过于灰暗。 例如,通过不保持非常低的饱和度来滤除灰色。

计算颜色的出现次数

那么你可以计算你的颜色,你应该工作在独特的颜色。 使用例如NSCountedSet来存储你的颜色和它们的出现次数,然后你可以处理每种颜色的出现次数,并知道最常见的出现次数等。

最后提示:在计算平均值之前滤除孤独的颜色 – 决定阈值(如“在300×300的图像中出现less于N次是不值得使用的”)。 帮助准确性很多。

如果你想得到一个确切的结果,我相信无论你自己做什么,或者通过一个API,你都会至less遍历所有的像素。

您目前使用已经分配(现有)的内存,这意味着至less你不会崩溃:)

如果冻结是问题,那是因为你正在UI线程中执行,并且应该在后台线程中移动所有这些处理,比如使用AsyncTask。

你可以尝试调整imageview的大小,获取渲染缓冲区,并在该图像上工作,但是你会使用更多的内存,我不相信它会更快。

我最终得到这个类扩展:

 extension UIImage { func averageColor(alpha : CGFloat) -> UIColor { let rawImageRef : CGImageRef = self.CGImage! let data : CFDataRef = CGDataProviderCopyData(CGImageGetDataProvider(rawImageRef))! let rawPixelData = CFDataGetBytePtr(data); let imageHeight = CGImageGetHeight(rawImageRef) let imageWidth = CGImageGetWidth(rawImageRef) let bytesPerRow = CGImageGetBytesPerRow(rawImageRef) let stride = CGImageGetBitsPerPixel(rawImageRef) / 6 var red = 0 var green = 0 var blue = 0 for row in 0...imageHeight { var rowPtr = rawPixelData + bytesPerRow * row for _ in 0...imageWidth { red += Int(rowPtr[0]) green += Int(rowPtr[1]) blue += Int(rowPtr[2]) rowPtr += Int(stride) } } let f : CGFloat = 1.0 / (255.0 * CGFloat(imageWidth) * CGFloat(imageHeight)) return UIColor(red: f * CGFloat(red), green: f * CGFloat(green), blue: f * CGFloat(blue) , alpha: alpha) } 

}

Swift 3版本

 extension UIImage { func averageColor(alpha : CGFloat) -> UIColor { let rawImageRef : CGImage = self.cgImage! let data : CFData = rawImageRef.dataProvider!.data! let rawPixelData = CFDataGetBytePtr(data); let imageHeight = rawImageRef.height let imageWidth = rawImageRef.width let bytesPerRow = rawImageRef.bytesPerRow let stride = rawImageRef.bitsPerPixel / 6 var red = 0 var green = 0 var blue = 0 for row in 0...imageHeight { var rowPtr = rawPixelData! + bytesPerRow * row for _ in 0...imageWidth { red += Int(rowPtr[0]) green += Int(rowPtr[1]) blue += Int(rowPtr[2]) rowPtr += Int(stride) } } let f : CGFloat = 1.0 / (255.0 * CGFloat(imageWidth) * CGFloat(imageHeight)) return UIColor(red: f * CGFloat(red), green: f * CGFloat(green), blue: f * CGFloat(blue) , alpha: alpha) } }