25

Im trying to get the least used color, and the most used color from MP3 file's album artwork for a music playing application. I need the colors to do an effect like the new itunes 11. Where the background color of the menu is the most used color, and the least used color is the color for song labels and artist name. I am using

`- (UIColor*) getPixelColorAtLocation:(CGPoint)point {
    UIColor* color = nil;
    CGImageRef inImage = self.image.CGImage;
    // Create off screen bitmap context to draw the image into. Format ARGB is 4 bytes for each pixel: Alpa, Red, Green, Blue
    CGContextRef cgctx = [self createARGBBitmapContextFromImage:inImage];
    if (cgctx == NULL) { return nil; /* error */ }

    size_t w = CGImageGetWidth(inImage);
    size_t h = CGImageGetHeight(inImage);
    CGRect rect = {{0,0},{w,h}}; 

    // Draw the image to the bitmap context. Once we draw, the memory
    // allocated for the context for rendering will then contain the
    // raw image data in the specified color space.
    CGContextDrawImage(cgctx, rect, inImage); 

    // Now we can get a pointer to the image data associated with the bitmap
    // context.
    unsigned char* data = CGBitmapContextGetData (cgctx);
    if (data != NULL) {
        //offset locates the pixel in the data from x,y.
        //4 for 4 bytes of data per pixel, w is width of one row of data.
        int offset = 4*((w*round(point.y))+round(point.x));
        int alpha =  data[offset];
        int red = data[offset+1];
        int green = data[offset+2];
        int blue = data[offset+3];
        NSLog(@"offset: %i colors: RGB A %i %i %i  %i",offset,red,green,blue,alpha);
        color = [UIColor colorWithRed:(red/255.0f) green:(green/255.0f) blue:(blue/255.0f) alpha:(alpha/255.0f)];
    }

    // When finished, release the context
    CGContextRelease(cgctx);
    // Free image data memory for the context
    if (data) { free(data); }

    return color;
}

- (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage {

    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;

    // Get image width, height. We'll use the entire image.
    size_t pixelsWide = CGImageGetWidth(inImage);
    size_t pixelsHigh = CGImageGetHeight(inImage);

    // Declare the number of bytes per row. Each pixel in the bitmap in this
    // example is represented by 4 bytes; 8 bits each of red, green, blue, and
    // alpha.
    bitmapBytesPerRow   = (pixelsWide * 4);
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);

    // Use the generic RGB color space.
    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    if (colorSpace == NULL)
    {
        fprintf(stderr, "Error allocating color space\n");
        return NULL;
    }

    // Allocate memory for image data. This is the destination in memory
    // where any drawing to the bitmap context will be rendered.
    bitmapData = malloc( bitmapByteCount );
    if (bitmapData == NULL)
    {
        fprintf (stderr, "Memory not allocated!");
        CGColorSpaceRelease( colorSpace );
        return NULL;
    }

    // Create the bitmap context. We want pre-multiplied ARGB, 8-bits
    // per component. Regardless of what the source image format is
    // (CMYK, Grayscale, and so on) it will be converted over to the format
    // specified here by CGBitmapContextCreate.
    context = CGBitmapContextCreate (bitmapData,
                                     pixelsWide,
                                     pixelsHigh,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedFirst);
    if (context == NULL)
    {
        free (bitmapData);
        fprintf (stderr, "Context not created!");
    }

    // Make sure and release colorspace before returning
    CGColorSpaceRelease( colorSpace );

    return context;
}`

to get the color at the bottom of the image to make it blend in my view controller which uses the color for its background, and has a shadow to make it blended.

Question: So, as it says: How do I get the least and most used color from an image?

amit
  • 175,853
  • 27
  • 231
  • 333
Maximilian Litteral
  • 3,059
  • 2
  • 31
  • 41
  • Think something along this line, http://www.markj.net/iphone-uiimage-pixel-color/ or this http://www.bobbygeorgescu.com/2011/08/finding-average-color-of-uiimage/ – iDev Dec 04 '12 at 01:30
  • i saw the first one before, thats how i get the bottom color in my screen, and now im using the same code for getting the label by getting a color at the top, but thats really bad. the average color may help though:] thanks!:] – Maximilian Litteral Dec 04 '12 at 01:40
  • Well, iterate over the pixels and add things up. But you'd better break the colors into "bands" somehow -- there are 16 million colors possible, and you can't reasonably keep a counter for each one. – Hot Licks Dec 04 '12 at 02:18
  • Are you able to provide some more information about what you've tried - a code sample? – Kirk Broadhurst Dec 04 '12 at 03:58

4 Answers4

47

The method below takes an image and analyses it for its main colours, in the following steps:

1.) scale down the image and determine the main pixel colours.

2.) add some colour flexibility to allow for the loss during scaling

3.) distinguish colours, removing similar ones

4.) return the colours as an ordered array or with their percentages

You could adapt it to return a specific number of colours, e.g. top 10 colours in image if you needed a guaranteed number of colours returned, or just use the "detail" variable if you don't.

Larger images will take a long time to analyse at high detail.

No doubt the method could be cleaned up a bit but could be a good starting point.

Use like this:

 NSDictionary * mainColours = [s mainColoursInImage:image detail:1];

Example images run through the method at detail "1"

-(NSDictionary*)mainColoursInImage:(UIImage *)image detail:(int)detail {

//1. determine detail vars (0==low,1==default,2==high)
//default detail
float dimension = 10;
float flexibility = 2;
float range = 60;

//low detail
if (detail==0){
    dimension = 4;
    flexibility = 1;
    range = 100;

//high detail (patience!)
} else if (detail==2){
    dimension = 100;
    flexibility = 10;
    range = 20;
}

//2. determine the colours in the image
NSMutableArray * colours = [NSMutableArray new];
CGImageRef imageRef = [image CGImage];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char *rawData = (unsigned char*) calloc(dimension * dimension * 4, sizeof(unsigned char));
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * dimension;
NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(rawData, dimension, dimension, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextDrawImage(context, CGRectMake(0, 0, dimension, dimension), imageRef);
CGContextRelease(context);

float x = 0;
float y = 0;
for (int n = 0; n<(dimension*dimension); n++){

    int index = (bytesPerRow * y) + x * bytesPerPixel;
    int red   = rawData[index];
    int green = rawData[index + 1];
    int blue  = rawData[index + 2];
    int alpha = rawData[index + 3];
    NSArray * a = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%i",red],[NSString stringWithFormat:@"%i",green],[NSString stringWithFormat:@"%i",blue],[NSString stringWithFormat:@"%i",alpha], nil];
    [colours addObject:a];

    y++;
    if (y==dimension){
        y=0;
        x++;
    }
}
free(rawData);

//3. add some colour flexibility (adds more colours either side of the colours in the image)
NSArray * copyColours = [NSArray arrayWithArray:colours];
NSMutableArray * flexibleColours = [NSMutableArray new];

float flexFactor = flexibility * 2 + 1;
float factor = flexFactor * flexFactor * 3; //(r,g,b) == *3
for (int n = 0; n<(dimension * dimension); n++){

    NSArray * pixelColours = copyColours[n];
    NSMutableArray * reds = [NSMutableArray new];
    NSMutableArray * greens = [NSMutableArray new];
    NSMutableArray * blues = [NSMutableArray new];

    for (int p = 0; p<3; p++){

        NSString * rgbStr = pixelColours[p];
        int rgb = [rgbStr intValue];

        for (int f = -flexibility; f<flexibility+1; f++){
            int newRGB = rgb+f;
            if (newRGB<0){
                newRGB = 0;
            }
            if (p==0){
                [reds addObject:[NSString stringWithFormat:@"%i",newRGB]];
            } else if (p==1){
                [greens addObject:[NSString stringWithFormat:@"%i",newRGB]];
            } else if (p==2){
                [blues addObject:[NSString stringWithFormat:@"%i",newRGB]];
            }
        }
    }

    int r = 0;
    int g = 0;
    int b = 0;
    for (int k = 0; k<factor; k++){

        int red = [reds[r] intValue];
        int green = [greens[g] intValue];
        int blue = [blues[b] intValue];

        NSString * rgbString = [NSString stringWithFormat:@"%i,%i,%i",red,green,blue];
        [flexibleColours addObject:rgbString];

        b++;
        if (b==flexFactor){ b=0; g++; }
        if (g==flexFactor){ g=0; r++; }
    }
}

//4. distinguish the colours
//orders the flexible colours by their occurrence
//then keeps them if they are sufficiently disimilar

NSMutableDictionary * colourCounter = [NSMutableDictionary new];

//count the occurences in the array
NSCountedSet *countedSet = [[NSCountedSet alloc] initWithArray:flexibleColours];
for (NSString *item in countedSet) {
    NSUInteger count = [countedSet countForObject:item];
    [colourCounter setValue:[NSNumber numberWithInteger:count] forKey:item];
}

//sort keys highest occurrence to lowest
NSArray *orderedKeys = [colourCounter keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2){
    return [obj2 compare:obj1];
}];

//checks if the colour is similar to another one already included
NSMutableArray * ranges = [NSMutableArray new];
for (NSString * key in orderedKeys){
    NSArray * rgb = [key componentsSeparatedByString:@","];
    int r = [rgb[0] intValue];
    int g = [rgb[1] intValue];
    int b = [rgb[2] intValue];
    bool exclude = false;
    for (NSString * ranged_key in ranges){
        NSArray * ranged_rgb = [ranged_key componentsSeparatedByString:@","];

        int ranged_r = [ranged_rgb[0] intValue];
        int ranged_g = [ranged_rgb[1] intValue];
        int ranged_b = [ranged_rgb[2] intValue];

        if (r>= ranged_r-range && r<= ranged_r+range){
            if (g>= ranged_g-range && g<= ranged_g+range){
                if (b>= ranged_b-range && b<= ranged_b+range){
                    exclude = true;
                }
            }
        }
    }

    if (!exclude){ [ranges addObject:key]; }
}

//return ranges array here if you just want the ordered colours high to low
NSMutableArray * colourArray = [NSMutableArray new];
for (NSString * key in ranges){
    NSArray * rgb = [key componentsSeparatedByString:@","];
    float r = [rgb[0] floatValue];
    float g = [rgb[1] floatValue];
    float b = [rgb[2] floatValue];
    UIColor * colour = [UIColor colorWithRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0f];
    [colourArray addObject:colour];
}

//if you just want an array of images of most common to least, return here
//return [NSDictionary dictionaryWithObject:colourArray forKey:@"colours"];


//if you want percentages to colours continue below
NSMutableDictionary * temp = [NSMutableDictionary new];
float totalCount = 0.0f;
for (NSString * rangeKey in ranges){
    NSNumber * count = colourCounter[rangeKey];
    totalCount += [count intValue];
    temp[rangeKey]=count;
}

//set percentages
NSMutableDictionary * colourDictionary = [NSMutableDictionary new];
for (NSString * key in temp){
    float count = [temp[key] floatValue];
    float percentage = count/totalCount;
    NSArray * rgb = [key componentsSeparatedByString:@","];
    float r = [rgb[0] floatValue];
    float g = [rgb[1] floatValue];
    float b = [rgb[2] floatValue];
    UIColor * colour = [UIColor colorWithRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0f];
    colourDictionary[colour]=[NSNumber numberWithFloat:percentage];
}

return colourDictionary;

}
Johnny Rockex
  • 4,136
  • 3
  • 35
  • 55
  • so how do i get the rgb color from this dictionary? –  Nov 18 '16 at 07:34
  • The returning dictionary contains UIColor Objects, if you wanted to pass back the raw RGB values, you could wrap them in an [NSNumber numberWithFloat:Red]; – Johnny Rockex Nov 18 '16 at 13:43
  • @JohnnyRockex When you use this code on iOS10 you get Extended RGB which can have weird values > 1. How do you convert to regular sRGB values (between 0 and 1) to pass to javascript code? I'm using your code in a react-native library and having [this issue here](https://github.com/bsudekum/react-native-color-grabber/issues/2) – chetstone Mar 11 '17 at 04:03
  • Haha react Native, let me check. Basically the code will be similar, you need to pull the Red Green and Blue, and then push the values through a similar process. I'll come back to you. – Johnny Rockex Mar 11 '17 at 04:06
  • To code is cool! but with this 'colourDictionary[colour]=[NSNumber numberWithFloat:percentage];' I have this ' "UIExtendedSRGBColorSpace 0.654902 0.65098 0.623529 1":"0.125" ' how get a NSString for percent and maybe an array for rgb values in objective.c? – Joannes Mar 11 '17 at 15:38
  • 1
    @JohnnyRockex Yeah Objective C is a foreign language for me, I barely know how to ask for the restroom, Haha, so thanks so much for your code. BUT NEVER MIND, I figured out another, probably better solution. Instead of converting UIColor values to Hex, react-native supports the rgb(255,255,255) format and if I use that with say, rgb(279, -58, -38), those wacky numbers get passed back to native iOS for rendering and surprise surprise, the phone knows how to deal with it. – chetstone Mar 11 '17 at 17:39
  • Hi, Can you please help me? I'm trying to use your code to get the colors from an image. I created a class in ObjC, but I need to call the method using Swift. I know nothing about Swift, so please, how do I call your method, using Swift? Thanks in advance – gbossa Jul 17 '20 at 13:29
28

Not sure about finding most color or least color, but here is a method to find out the average color.

- (UIColor *)averageColor {

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char rgba[4];
    CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

    CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), self.CGImage);
    CGColorSpaceRelease(colorSpace);
    CGContextRelease(context);  

    if(rgba[3] > 0) {
        CGFloat alpha = ((CGFloat)rgba[3])/255.0;
        CGFloat multiplier = alpha/255.0;
        return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
                               green:((CGFloat)rgba[1])*multiplier
                                blue:((CGFloat)rgba[2])*multiplier
                               alpha:alpha];
    }
    else {
        return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
                               green:((CGFloat)rgba[1])/255.0
                                blue:((CGFloat)rgba[2])/255.0
                               alpha:((CGFloat)rgba[3])/255.0];
    }
}

You can probably follow a similar approach to find out the most used color.

Also check this answer about counting red color pixels in an image.

Community
  • 1
  • 1
iDev
  • 23,310
  • 7
  • 60
  • 85
2

Thanks a lot for your code, @JohnnyRockex. It was really helpful in getting me started towards my goal (finding accent colors depending on the most predominant color in an image).

After going through it, I found the code could be simplified and made easier to read, so I'd like to give back to the community my own version; the -colors selector is in a UIImage extension.

- (NSArray *)colors {
// Original code by Johnny Rockex http://stackoverflow.com/a/29266983/825644

// Higher the dimension, the more pixels are checked against.
const float pixelDimension = 10;
// Higher the range, more similar colors are removed.
const float filterRange = 60;

unsigned char *rawData = (unsigned char*) calloc(pixelDimension * pixelDimension * kBytesPerPixel, sizeof(unsigned char));

NSUInteger bytesPerRow = kBytesPerPixel * pixelDimension;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rawData, pixelDimension, pixelDimension, kBitsInAByte, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextDrawImage(context, CGRectMake(0, 0, pixelDimension, pixelDimension), [self CGImage]);
CGContextRelease(context);

NSMutableArray * colors = [[NSMutableArray alloc] init];
float x = 0;
float y = 0;
const int pixelMatrixSize = pixelDimension * pixelDimension;
for (int i = 0; i < pixelMatrixSize; i++){
    int index = (bytesPerRow * y) + x * kBytesPerPixel;

    int red   = rawData[index];
    int green = rawData[index + 1];
    int blue  = rawData[index + 2];
    int alpha = rawData[index + 3];
    UIColor * color = [UIColor colorWithRed:(red / 255.0f) green:(green / 255.0f) blue:(blue / 255.0f) alpha:alpha];
    [colors addObject:color];

    y++;
    if (y == pixelDimension){
        y = 0;
        x++;
    }
}
free(rawData);


NSMutableDictionary * colorCounter = [[NSMutableDictionary alloc] init];
NSCountedSet *countedSet = [[NSCountedSet alloc] initWithArray:colors];
for (NSString *item in countedSet) {
    NSUInteger count = [countedSet countForObject:item];
    [colorCounter setValue:[NSNumber numberWithInteger:count] forKey:item];
}

NSArray *orderedColors = [colorCounter keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2){
    return [obj2 compare:obj1];
}];

NSMutableArray *filteredColors = [NSMutableArray new];
for (UIColor *color in orderedColors){

    bool filtered = false;
    for (UIColor *rangedColor in filteredColors){
        if (abs(color.redRGBComponent - rangedColor.redRGBComponent) <= filterRange &&
            abs(color.greenRGBComponent - rangedColor.greenRGBComponent) <= filterRange &&
            abs(color.blueRGBComponent - rangedColor.blueRGBComponent) <= filterRange) {

            filtered = true;
            break;
        }
    }

    if (!filtered) {
        [filteredColors addObject:color];
    }
}

return [filteredColors copy];

The code for UIColor's extension adding the -rgbComponent function can be found underneath, but I wrote it in Swift (trying to write all new classes in Swift, but this wasn't the case for the -colors selector):

extension UIColor {

    open func redRGBComponent() -> UInt8 {
        let colorComponents = cgColor.components!
        return UInt8(colorComponents[0] * 255)
    }

    open func greenRGBComponent() -> UInt8 {
        let colorComponents = cgColor.components!
        return UInt8(colorComponents[1] * 255)
    }

    open func blueRGBComponent() -> UInt8 {
        let colorComponents = cgColor.components!
         return UInt8(colorComponents[2] * 255)
    }

}

Enjoy!

dinesharjani
  • 545
  • 6
  • 14
1

I wrote this tool to do that.

https://github.com/623637646/UIImageColorRatio

ezgif com-gif-maker

// replace the UIImage() to yourself's UIImage.
let theMostUsedColor = UIImage().calculateColorRatio(deviation: 0)?.colorRatioArray.first?.color
let theLeastUsedColor = UIImage().calculateColorRatio(deviation: 0)?.colorRatioArray.last?.color
Yanni
  • 580
  • 2
  • 6
  • 21
  • 3
    Yanni, please don't just post some tool or library as an answer. At least demonstrate [how it solves the problem](//meta.stackoverflow.com/a/251605) in the answer itself. – 4b0 May 11 '21 at 03:36
  • Thanks, @Shree. I update my comment to show the code. – Yanni May 11 '21 at 04:34
  • Yanni, we're talking about showing the code that does answer the question, not just the code to call your methods. :) For example you could post here the content of https://github.com/623637646/UIImageColorRatio/blob/main/UIImageColorRatio/UIImageColorRatio.swift or at least demonstrate a summary of how it works. Thanks! – Eric Aya May 11 '21 at 06:53