I want to build an app that lets the user select an image and it outputs the "average color".
For example, this image:
The average color would be a greenish/yellowish color.
At the moment, I got this code:
// 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)
}
Where getPixelColor
is defined as:
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)
}
}
As you can see, what I did here is pretty naive: I loop through all the pixels in the image, add their RGBs up, and divide by the count.
When I run the app and selects the image, the app freezes. I know that this is because the image is too large and the two nested for loops are executed too many times.
I want to find a way to efficiently get the average color of an image. How do I do that?
You'll need to use the Accelerate Library, Apple has a manual with some sample code, it'll work in Swift or ObjC
Here is a sample to get you going, I use this to calculate a person's heart rate and heart rate variability using the change in colors of a finger over the camera lens.
Full code here: https://github.com/timestocome/SwiftHeartRate/blob/master/Swift%20Pulse%20Reader/ViewController.swift
It's in an older version of Swift but I think you'll get the idea. I was doing this at 240 fps, but with a cropped smaller section of the image.
Relevant code here:
// 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]