Search code examples
iosobjective-cuiimageviewuiimage

How to replace a given color with another on an opaque UIImage


I want to change the color of an opaque UIImage. My original image is as follows:

enter image description here

and I want to convert the image to the following format

enter image description here

So, basically I want to convert red color in the image into black (Any other color) color. Above two images are added for better understanding.


Solution

  • I couldn't see any answers on the 'duplicates' (this question shouldn't have been flagged as a duplicate) that will let you replace a given color with another color and work on an opaque image, so I decided to add one that would.


    I created a UIImage category to do this, it basically works by looping through each pixel and detecting how close it is to a given colour, and blends it with your replacement colour if it is.

    This will work for images with both transparency and opaque backgrounds.

    @implementation UIImage (UIImageColorReplacement)
    
    -(UIImage*) imageByReplacingColor:(UIColor*)sourceColor withMinTolerance:(CGFloat)minTolerance withMaxTolerance:(CGFloat)maxTolerance withColor:(UIColor*)destinationColor {
    
        // components of the source color
        const CGFloat* sourceComponents = CGColorGetComponents(sourceColor.CGColor);
        UInt8* source255Components = malloc(sizeof(UInt8)*4);
        for (int i = 0; i < 4; i++) source255Components[i] = (UInt8)round(sourceComponents[i]*255.0);
    
        // components of the destination color
        const CGFloat* destinationComponents = CGColorGetComponents(destinationColor.CGColor);
        UInt8* destination255Components = malloc(sizeof(UInt8)*4);
        for (int i = 0; i < 4; i++) destination255Components[i] = (UInt8)round(destinationComponents[i]*255.0);
    
        // raw image reference
        CGImageRef rawImage = self.CGImage;
    
        // image attributes
        size_t width = CGImageGetWidth(rawImage);
        size_t height = CGImageGetHeight(rawImage);
        CGRect rect = {CGPointZero, {width, height}};
    
        // bitmap format
        size_t bitsPerComponent = 8;
        size_t bytesPerRow = width*4;
        CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big;
    
        // data pointer
        UInt8* data = calloc(bytesPerRow, height);
    
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
        // create bitmap context
        CGContextRef ctx = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo);
        CGContextDrawImage(ctx, rect, rawImage);
    
        // loop through each pixel's components
        for (int byte = 0; byte < bytesPerRow*height; byte += 4) {
    
            UInt8 r = data[byte];
            UInt8 g = data[byte+1];
            UInt8 b = data[byte+2];
    
            // delta components
            UInt8 dr = abs(r-source255Components[0]);
            UInt8 dg = abs(g-source255Components[1]);
            UInt8 db = abs(b-source255Components[2]);
    
            // ratio of 'how far away' each component is from the source color
            CGFloat ratio = (dr+dg+db)/(255.0*3.0);
            if (ratio > maxTolerance) ratio = 1; // if ratio is too far away, set it to max.
            if (ratio < minTolerance) ratio = 0; // if ratio isn't far enough away, set it to min.
    
            // blend color components
            data[byte] = (UInt8)round(ratio*r)+(UInt8)round((1.0-ratio)*destination255Components[0]);
            data[byte+1] = (UInt8)round(ratio*g)+(UInt8)round((1.0-ratio)*destination255Components[1]);
            data[byte+2] = (UInt8)round(ratio*b)+(UInt8)round((1.0-ratio)*destination255Components[2]);
    
        }
    
        // get image from context
        CGImageRef img = CGBitmapContextCreateImage(ctx);
    
        // clean up
        CGContextRelease(ctx);
        CGColorSpaceRelease(colorSpace);
        free(data);
        free(source255Components);
        free(destination255Components);
    
        UIImage* returnImage = [UIImage imageWithCGImage:img];
        CGImageRelease(img);
    
        return returnImage;
    }
    
    @end
    

    Usage:

    UIImage* colaImage = [UIImage imageNamed:@"cola1.png"];
    UIImage* blackColaImage = [colaImage imageByReplacingColor:[UIColor colorWithRed:1 green:0 blue:0 alpha:1] withMinTolerance:0.5 withMaxTolerance:0.6 withColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:1]];
    

    The minTolerance is the point at which pixels will start to blend with the replacement colour (rather than being replaced). The maxTolerance is the point at which the pixels will stop being blended.

    Before:

    enter image description here

    After:

    enter image description here

    The result is a little aliased, but bear in mind that your original image was fairly small. This will work much better with a higher resolution image. You can also play about with the tolerances to get even better results!