Count colors in image: `NSCountedSet` and `colorAtX` are very slow

305 views Asked by At

I'm making an OS X app which creates a color scheme from the main colors of an image.

As a first step, I'm using NSCountedSet and colorAtX to get all the colors from an image and count their occurrences:

func sampleImage(#width: Int, height: Int, imageRep: NSBitmapImageRep) -> (NSCountedSet, NSCountedSet) {
    // Store all colors from image
    var colors = NSCountedSet(capacity: width * height)
    // Store the colors from left edge of the image
    var leftEdgeColors = NSCountedSet(capacity: height)
    // Loop over the image pixels
    var x = 0
    var y = 0
    while x < width {
        while y < height {
            // Instruments shows that `colorAtX` is very slow
            // and using `NSCountedSet` is also very slow
            if let color = imageRep.colorAtX(x, y: y) {
                if x == 0 {
                    leftEdgeColors.addObject(color)
                }
                colors.addObject(color)
            }
            y++
        }
        // Reset y every x loop
        y = 0
        // We sample a vertical line every x pixels
        x += 1
    }
    return (colors, leftEdgeColors)
}

My problem is that this is very slow. In Instruments, I see there's two big bottlenecks: with NSCountedSet and with colorAtX.

So first I thought maybe replace NSCountedSet by a pure Swift equivalent, but the new implementation was unsurprisingly much slower than NSCountedSet.

For colorAtX, there's this interesting SO answer but I haven't been able to translate it to Swift (and I can't use a bridging header to Objective-C for this project).

My problem when trying to translate this is I don't understand the unsigned char and char parts in the answer.

What should I try to scan the colors faster than with colorAtX?

  • Continue working on adapting the Objective-C answer because it's a good answer? Despite being stuck for now, maybe I can achieve this later.

  • Use another Foundation/Cocoa method that I don't know of?

  • Anything else that I could try to improve my code?

TL;DR

colorAtX is slow, and I don't understand how to adapt this Objective-C answer to Swift because of unsigned char.

1

There are 1 answers

8
GoatInTheMachine On BEST ANSWER

The fastest alternative to colorAtX() would be iterating over the raw bytes of the image using let bitmapBytes = imageRep.bitmapData and composing the colour yourself from that information, which should be really simple if it's just RGBA data. Instead of your for x/y loop, do something like this...

let bitmapBytes = imageRep.bitmapData
var colors = Dictionary<UInt32, Int>()

var index = 0
for _ in 0..<(width * height) {
    let r = UInt32(bitmapBytes[index++])
    let g = UInt32(bitmapBytes[index++])
    let b = UInt32(bitmapBytes[index++])
    let a = UInt32(bitmapBytes[index++])
    let finalColor = (r << 24) + (g << 16) + (b << 8) + a   

    if colors[finalColor] == nil {
        colors[finalColor] = 1
    } else {
        colors[finalColor]!++
    }
}

You will have to check the order of the RGBA values though, I just guessed!

The quickest way to maintain a count might just be a [Int, Int] dictionary of pixel values to counts, doing something like colors[color]++. Later on if you need to you can convert that to a NSColor using NSColor(calibratedRed red: CGFloat, green green: CGFloat, blue blue: CGFloat, alpha alpha: CGFloat)