Как рассчитать средний цвет UIImage?
Я хочу создать приложение, которое позволяет пользователю выбирать изображение и выводит "средний цвет".
Например, это изображение:
Средний цвет был бы зеленоватого/желтоватого цвета.
На данный момент у меня есть этот код:
// 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 и деля по счету.
Когда я запускаю приложение и выбираю изображение, приложение зависает. Я знаю, что это связано с тем, что изображение слишком велико, а два вложенных цикла выполняются слишком много раз.
Я хочу найти способ эффективно получить средний цвет изображения. Как мне это сделать?
Ответы
Ответ 1
Вам нужно будет использовать библиотеку ускорения, у Apple есть руководство с некоторым примером кода, оно будет работать в Swift или ObjC
Вот пример, который поможет вам понять, я использую его для расчета частоты сердечных сокращений человека и вариабельности сердечного ритма с использованием изменения цвета пальца на объективе камеры.
Полный код здесь: https://github.com/timestocome/SwiftHeartRate/blob/master/Swift%20Pulse%20Reader/ViewController.swift
Это в старой версии Swift, но я думаю, вы получите эту идею. Я делал это со скоростью 240 кадров в секунду, но с уменьшенной средой изображения.
Соответствующий код здесь:
// 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]
Ответ 2
Это не настоящий "ответ", но я чувствую, что могу дать несколько советов о распознавании цвета, для чего это стоит, поэтому отпустите.
Изменение размера
Самый большой трюк для скорости в вашем случае - изменить размер изображения на квадрат разумных размеров.
Там нет волшебного значения, потому что это зависит от того, является ли изображение шумным или нет, и т.д., Но менее 300x300 для таргетинга вашего метода выборки кажется приемлемым, например (не слишком уж экстремально).
Используйте быстрый метод изменения размера - не нужно сохранять соотношение, антиалиасы или что-либо (существует множество реализаций, доступных на SO). Мы подсчитываем цвета, нас не интересует аспект того, что показывает изображение.
Ускорение скорости, получаемое от изменения размера, стоит нескольких циклов, потерянных при изменении размера.
Шагая
Второй трюк - это выборка путем шага.
С большинством фотографий вы можете позволить себе пробовать любой другой пиксель или любую другую линию и сохранять ту же точность при распознавании цвета.
Вы также можете не отбирать (или отбрасывать один раз) границы большинства фотографий на несколько пикселей в ширину - из-за границ, фреймов, виньет и т.д. Это помогает создавать средние значения (вы хотите отбросить все, что слишком маргинально, и может привести к смещению результаты без необходимости).
Отфильтровать шум
Чтобы быть действительно точным в выборке, вы должны отказаться от шума: если вы держите все серые, все обнаружения будут слишком серыми. Отфильтруйте серости, например, не сохраняя цвета с очень низкой насыщенностью.
Подсчет количества цветов
Затем вы можете подсчитать свои цвета, и вы должны работать над уникальными цветами. Используйте, например, NSCountedSet для хранения ваших цветов и их появления, тогда вы можете работать с количеством вхождений для каждого цвета и знать самые частые и т.д.
Последний совет: отфильтруйте одинокие цвета перед вычислением средних значений - вы определяете порог (например, "если он отображается меньше, чем N раз в изображении 300x300, которое не стоит использовать"). Очень помогает точность.
Ответ 3
Если вы хотите получить точный результат, я считаю, что независимо от того, что вы делаете, либо самостоятельно, либо через API, вы будете проходить через все пиксели хотя бы один раз.
Вы в настоящее время используете уже назначенную (существующую) память, так что это означает, что по крайней мере вы не сработаете :)
Если проблема заключается в замораживании, это связано с тем, что вы выполняете в потоке пользовательского интерфейса и должны перемещать всю эту обработку в фоновом потоке, например, с помощью AsyncTask.
Вы можете попробовать изменить размер изображения, получить буфер рендеринга и работать с этим изображением, но вы будете использовать больше памяти, и я не верю, что это будет быстрее.
Ответ 4
Я получаю расширение этого класса:
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)
}
}
Ответ 5
Версия 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)
}
}