如何计算 UIImage 的平均颜色?

How to calculate the average color of a UIImage?

我想构建一个应用程序,让用户 select 图像并输出 "average color"。

例如这张图片:

平均颜色为 greenish/yellowish 颜色。

此刻,我得到了这个代码:

// 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循环执行的次数太多了。

我想找到一种有效获取图像平均颜色的方法。我该怎么做?

如果你想要一个准确的结果,我相信无论你做什么,无论你自己还是通过API,你都会至少检查一次所有像素。

您当前使用的是已分配(现有)的内存,这意味着至少您不会崩溃:)

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

您可以尝试调整 imageview 的大小,获取渲染缓冲区并处理该图像,但您将使用更多内存,而且我认为它也不会更快。

这不是真实的"answer",但我觉得我可以提供一些关于颜色检测的技巧,不管它值多少钱,所以我们开始吧。

调整大小

在您的案例中,提高速度的最大技巧是将图像调整为合理尺寸的正方形。

没有神奇的价值,因为它取决于图像是否嘈杂等,但是小于 300x300 以针对您的采样方法似乎是可以接受的,例如(不过不要太极端)。

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

我们从调整大小中获得的速度增益非常值得在调整大小时损失的几个周期。

步进

第二个技巧是步进采样。

对于大多数照片,您可以每隔一个像素或每隔一条线进行采样,并保持相同的颜色检测精度。

您也不能在几个像素宽上对大多数照片的边框进行取样(或一旦取样就丢弃)- 因为边框、框架、晕影等。它有助于进行平均(您想丢弃所有那些太边缘化了,可能会导致结果出现不必要的偏差)。

过滤掉噪音

为了在采样中真正精确,您必须丢弃噪音:如果保留所有灰色,所有检测都会太灰。例如,通过不保留饱和度非常低的颜色来滤除灰色。

计算颜色的出现次数

然后你可以数出你的颜色,你应该在独特的颜色上工作。使用例如 NSCountedSet 来存储您的颜色及其出现次数,然后您可以处理每种颜色的出现次数并了解最常见的颜色等。

最后提示:在计算平均值之前过滤掉孤独的颜色 - 您决定阈值(如 "if it appears less than N times in a 300x300 image it's not worth using")。对准确性有很大帮助。

您需要使用 Accelerate 库,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]

我最终得到这个 class 扩展名:

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

也许它可以帮助某人 - 我的扩展 return 颜色但是对于 UIView(可以找到你的图像的地方),使用旧的 QuartzCore 代码

// ObjC case
@interface UIView (ColorOfPoint)
    - (UIColor *) colorOfPoint:(CGPoint)point withSize:(CGSize)size;
@end

#import <QuartzCore/QuartzCore.h>

@implementation UIView (ColorOfPoint)

- (UIColor *) colorOfPoint:(CGPoint)point withSize:(CGSize)size {
    unsigned char pixel[4] = {0};

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = CGBitmapContextCreate(pixel, size.width, size.height, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast);

    CGContextTranslateCTM(context, -point.x-size.width/2, -point.y-size.height/2);

    [self.layer renderInContext:context];

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    //NSLog(@"pixel: %d %d %d %d", pixel[0], pixel[1], pixel[2], pixel[3]);

    UIColor *color = [UIColor colorWithRed:pixel[0]/255.0 green:pixel[1]/255.0 blue:pixel[2]/255.0 alpha:pixel[3]/255.0];

    return color;
}

@end