【问题标题】:How to calculate the average color of a UIImage?如何计算 UIImage 的平均颜色?
【发布时间】:2016-08-16 18:21:28
【问题描述】:

我想构建一个让用户选择图像并输出“平均颜色”的应用。

例如这张图片:

平均颜色为绿色/黄色。

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

// 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 相加,然后除以计数。

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

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

【问题讨论】:

  • @EricD 哦!为什么我没有想过调整图像大小?但我可以将其调整为 1x1 图像吗?这样我就不需要丢弃噪音。我说的对吗?

标签: ios swift colors average pixel


【解决方案1】:

也许它可以帮助某人 - 我的扩展返回颜色,但对于 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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 让 stride = rawImageRef.bitsPerPixel / 8 不是 /6 对吧?
    【解决方案3】:

    我最终得到了这个类扩展:

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

    }

    【讨论】:

      【解决方案4】:

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

      调整大小

      在您的情况下,最大的速度技巧是将图像大小调整为合理尺寸的正方形。

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

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

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

      步进

      第二个技巧是步进采样。

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

      您也不能采样(或在采样后丢弃)大多数照片的边框在几个像素宽 - 因为边框、框架、晕影等。它有助于制作平均值(您想丢弃所有太边缘的并且可能会不必要地偏向结果)。

      过滤掉噪音

      要真正精确地进行采样,您必须丢弃噪声:如果保留所有灰色,则所有检测都将过于灰色。例如,通过不保持饱和度非常低的颜色来过滤掉灰色。

      计算颜色的出现次数

      然后你就可以数数你的颜色了,你应该研究独特的颜色。使用例如 NSCountedSet 来存储您的颜色及其出现次数,然后您可以处理每种颜色的出现次数并知道最常见的次数等。

      最后提示:在计算平均值之前过滤掉孤立的颜色 - 您决定阈值(例如“如果它在 300x300 图像中出现少于 N 次,则不值得使用”)。对准确性有很大帮助。

      【讨论】:

        【解决方案5】:

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

        【讨论】:

        • 为什么你的代码在 github 上的第 164 行和第 171 行,在你提供的链接上,使用相同的 API?你在干嘛?抓取同一张图片两次?
        • vDSP_vfltu8(dataBuffer, 4, &blueVector, 1, vDSP_Length(numberOfPixels)) 这个项目卡在这一行。为什么?如何解决?
        • Swift 在每个版本中都有很大的变化,从一个版本到下一个版本没有任何变化。这是一个 ObjC 版本:github.com/timestocome/HardwareSensors/blob/master/TestHardware/…
        • 是的,文件被打开了两次,只是代码中的一个错字
        【解决方案6】:

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

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

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

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

        【讨论】: