Как рассчитать средний цвет UIImage?

Я хочу создать приложение, которое позволяет пользователю выбирать изображение и выводит "средний цвет".

Например, это изображение:

enter image description here

Средний цвет был бы зеленоватого/желтоватого цвета.

На данный момент у меня есть этот код:

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