Skip to content

Instantly share code, notes, and snippets.

@andrey-str
Created April 22, 2016 00:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save andrey-str/6d836825d1aa0a96194d98fdf5aa4fb0 to your computer and use it in GitHub Desktop.
Save andrey-str/6d836825d1aa0a96194d98fdf5aa4fb0 to your computer and use it in GitHub Desktop.
Replace color in UIImage with give tolerance value
// color - source color, which is must be replaced
// withColor - target color
// tolerance - value in range from 0 to 1
func replaceColor(color:SKColor, withColor:SKColor, image:UIImage, tolerance:CGFloat) -> UIImage{
// This function expects to get source color(color which is supposed to be replaced)
// and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
assert(CGColorGetNumberOfComponents(color.CGColor) == 4 && CGColorGetNumberOfComponents(withColor.CGColor) == 4,
"Must be RGBA colorspace")
// Allocate bitmap in memory with the same width and size as source image
let imageRef = image.CGImage
let width = CGImageGetWidth(imageRef)
let height = CGImageGetHeight(imageRef)
let colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)!
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width;
let bitsPerComponent = 8
let bitmapByteCount = bytesPerRow * height
let rawData = UnsafeMutablePointer<UInt8>.alloc(bitmapByteCount)
let context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace,
CGImageAlphaInfo.PremultipliedLast.rawValue | CGBitmapInfo.ByteOrder32Big.rawValue)
let rc = CGRect(x: 0, y: 0, width: width, height: height)
// Draw source image on created context
CGContextDrawImage(context, rc, imageRef)
// Get color components from replacement color
let withColorComponents = CGColorGetComponents(withColor.CGColor)
let r2 = UInt8(withColorComponents[0] * 255)
let g2 = UInt8(withColorComponents[1] * 255)
let b2 = UInt8(withColorComponents[2] * 255)
let a2 = UInt8(withColorComponents[3] * 255)
// Prepare to iterate over image pixels
var byteIndex = 0
while byteIndex < bitmapByteCount {
// Get color of current pixel
let red:CGFloat = CGFloat(rawData[byteIndex + 0])/255
let green:CGFloat = CGFloat(rawData[byteIndex + 1])/255
let blue:CGFloat = CGFloat(rawData[byteIndex + 2])/255
let alpha:CGFloat = CGFloat(rawData[byteIndex + 3])/255
let currentColor = SKColor(red: red, green: green, blue: blue, alpha: alpha);
// Compare two colors using given tolerance value
if compareColor(color, withColor: currentColor , withTolerance: tolerance) {
// If the're 'similar', then replace pixel color with given target color
rawData[byteIndex + 0] = r2
rawData[byteIndex + 1] = g2
rawData[byteIndex + 2] = b2
rawData[byteIndex + 3] = a2
}
byteIndex = byteIndex + 4;
}
// Retrieve image from memory context
let imgref = CGBitmapContextCreateImage(context)
let result = UIImage(CGImage: imgref!)
// Clean up a bit
rawData.destroy()
return result
}
func compareColor(color:SKColor, withColor:SKColor, withTolerance:CGFloat) -> Bool {
var r1: CGFloat = 0.0, g1: CGFloat = 0.0, b1: CGFloat = 0.0, a1: CGFloat = 0.0;
var r2: CGFloat = 0.0, g2: CGFloat = 0.0, b2: CGFloat = 0.0, a2: CGFloat = 0.0;
color.getRed(&r1, green: &g1, blue: &b1, alpha: &a1);
withColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2);
return fabs(r1 - r2) <= withTolerance &&
fabs(g1 - g2) <= withTolerance &&
fabs(b1 - b2) <= withTolerance &&
fabs(a1 - a2) <= withTolerance;
}
@RomanEsin
Copy link

RomanEsin commented Jul 9, 2020

This is a great code snippet! I updated it for Swift 5 here, please consider changing it!

@Rj707
Copy link

Rj707 commented Sep 11, 2021

My code is working perfectly fine, except for one-thing. The Performance is not up-to the mark. What I am trying to achieve is I have an image with few circles(each circle has a border with different color than circle fill color), when user touches any circle, I want to change the border color of the selected circle. That's working fine, but what working isn't up-to expectation is that we have a Table in the UI from where user can select multiple circles, like 6-12 circles at max. In this 2nd scenario, The implementation is taking 3-4 seconds. Below I am sharing the code snippet I am using, if anything wrong found Please guide me.

func replaceColor(sourceColor: [UIColor], withDestColor destColor: UIColor, tolerance: CGFloat) -> UIImage
    {
        // This function expects to get source color(color which is supposed to be replaced)
        // and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
        
//        assert(sourceColor.cgColor.numberOfComponents == 4 && destColor.cgColor.numberOfComponents == 4,
//               "Must be RGBA colorspace")
        
        // *** Allocate bitmap in memory with the same width and size as destination image or back image *** //
        
        let backImageBitmap = self.backImage!.cgImage! // Back Image Bitmap
                
        let bitmapByteCountBackImage = backImageBitmap.bytesPerRow * backImageBitmap.height
        
        let rawDataBackImage = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCountBackImage) // A pointer to the memory block where the drawing is to be rendered

        /// *** A graphics context contains drawing parameters and all device-specific information needed to render the paint on a page to a bitmap image *** //
        
        let contextBackImage = CGContext(data: rawDataBackImage,
                                         width: backImageBitmap.width,
                                         height: backImageBitmap.height,
                                         bitsPerComponent: backImageBitmap.bitsPerComponent,
                                         bytesPerRow: backImageBitmap.bytesPerRow,
                                         space: backImageBitmap.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
                                         bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
        

        // Draw bitmap on created context
        
        contextBackImage!.draw(backImageBitmap, in: CGRect(x: 0, y: 0, width: backImageBitmap.width, height: backImageBitmap.height))

        // Allocate bitmap in memory with the same width and size as source image or front image
        
        let frontImageBitmap = self.frontImage!.cgImage! // Front Image Bitmap

        let bitmapByteCountFrontImage = frontImageBitmap.bytesPerRow * frontImageBitmap.height

        let rawDataFrontImage = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCountFrontImage) // A pointer to the memory block where the drawing is to be rendered

        let contextFrontImage = CGContext(data: rawDataFrontImage,
                                          width: frontImageBitmap.width,
                                          height: frontImageBitmap.height,
                                          bitsPerComponent: frontImageBitmap.bitsPerComponent,
                                          bytesPerRow: frontImageBitmap.bytesPerRow,
                                          space: frontImageBitmap.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
                                          bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)

        // Draw bitmap on created context

        contextFrontImage!.draw(frontImageBitmap, in: CGRect(x: 0, y: 0, width: frontImageBitmap.width, height: frontImageBitmap.height))

        // *** Get color components from replacement color *** \\
        
        let destinationColorComponents = destColor.cgColor.components
        let r2 = UInt8(destinationColorComponents![0] * 255)
        let g2 = UInt8(destinationColorComponents![1] * 255)
        let b2 = UInt8(destinationColorComponents![2] * 255)
        let a2 = UInt8(destinationColorComponents![3] * 255)

        // Prepare to iterate over image pixels
        
        var byteIndex = 0

        while byteIndex < bitmapByteCountBackImage
        {
            // Get color of current pixel
            
            let red = CGFloat(rawDataBackImage[byteIndex + 0]) / 255
            let green = CGFloat(rawDataBackImage[byteIndex + 1]) / 255
            let blue = CGFloat(rawDataBackImage[byteIndex + 2]) / 255
            let alpha = CGFloat(rawDataBackImage[byteIndex + 3]) / 255

            let currentColorBackImage = UIColor(red: red, green: green, blue: blue, alpha: alpha);

            // Compare two colors using given tolerance value
            
            if sourceColor.contains(currentColorBackImage)
            {
                // If the're 'similar', then replace pixel color with given target color

                rawDataFrontImage[byteIndex + 0] = r2
                rawDataFrontImage[byteIndex + 1] = g2
                rawDataFrontImage[byteIndex + 2] = b2
                rawDataFrontImage[byteIndex + 3] = a2
            }
            
            byteIndex = byteIndex + 4;
        }

        // Retrieve image from memory context
        
        let imgref = contextFrontImage!.makeImage()
        let result = UIImage(cgImage: imgref!)

        // Clean up a bit
        
        rawDataBackImage.deallocate()
        rawDataFrontImage.deallocate()
        
        return result
    } ```
 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment