Search code examples
swiftxcodeuiimageviewuicolor

How to find current color in UIImageView and change repeatedly?


I have this UIImageView where I am only changing the white color of the image. When the white color changes it doesn't change again because the white color is no longer white anymore. I want to access the new color and change it to a different color every time I press a button. Im using this func I found on github.

var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)

@IBAction func changeColors(_ sender: Any) {

        
let randomRGB =  CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)


//randomly change color
var newColor = UIColor.init(red: randomRGB3, green: randomRGB2, blue: randomRGB, alpha: 1)

let changeColor = replaceColor(color: currentColor, withColor: newColor, image: mainImage.image!, tolerance: 0.5)

mainImage.image = changeColor

//change current color to new color
currentColor = newColor

}

extension ViewController {

func replaceColor(color: UIColor, withColor: UIColor, 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(color.cgColor.numberOfComponents == 4 && withColor.cgColor.numberOfComponents == 4,
       "Must be RGBA colorspace")

// Allocate bitmap in memory with the same width and size as source image
let imageRef = image.cgImage!
let width = imageRef.width
let height = imageRef.height

let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width;
let bitsPerComponent = 8
let bitmapByteCount = bytesPerRow * height

let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)

let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpace(name: CGColorSpace.genericRGBLinear)!,
                        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)


let rc = CGRect(x: 0, y: 0, width: width, height: height)

// Draw source image on created context
context!.draw(imageRef, in: rc)


// Get color components from replacement color
let withColorComponents = withColor.cgColor.components
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(rawData[byteIndex + 0]) / 255
    let green = CGFloat(rawData[byteIndex + 1]) / 255
    let blue = CGFloat(rawData[byteIndex + 2]) / 255
    let alpha = CGFloat(rawData[byteIndex + 3]) / 255

    let currentColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)

    // Compare two colors using given tolerance value
    if compareColor(color: 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 = context!.makeImage()
let result = UIImage(cgImage: imgref!)

// Clean up a bit
rawData.deallocate()

return result
}

func compareColor(color: UIColor, withColor: UIColor, 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 abs(r1 - r2) <= withTolerance &&
    abs(g1 - g2) <= withTolerance &&
    abs(b1 - b2) <= withTolerance &&
    abs(a1 - a2) <= withTolerance;
   }

}

Solution

  • Here are a few observations that I made which might be impacting the results you see:

    1. As we discussed in the comments, if you want to start from the color which was changed previously, you need to hold on the color after the image has been updated beyond the scope of your function (you did this)

    2. The next issue about ending up with one color probably has a lot to do with the fault tolerance

    • When you try to change a color in an image with 0.5 (50%) fault tolerance of a given color, you are changing a huge number of colors in an image in the first pass
    • If there were 100 colors in a color system, you are going to look for 50 of those colors in the image and change them to 1 specific color
    • In the first pass, you start with white. Lets say that 75% of the image has colors that are similar to white with a 50% fault tolerance - 75% of the image is going to change to that color
    • With such a high fault tolerance, soon enough one color will appear that will be close to most of the colors in the image with a 50% fault tolerance and you will end up with colors with 1 image

    Some ideas to improve the results

    1. Set a lower fault tolerance - you will see smaller changes and the same result could occur with 1 color but it will happen over a longer period of time
    2. If you really want to randomize and no get this 1 color results, I suggest to change how you use the currentColor and make changes to the original image, not the updated image (I have this example below)
    3. This will not impact the solution but better to handle optionals more safely as I see a lot of ! so I would recommend changing that
    4. Perform the image processing in a background thread (also in the example below)

    Here is an update with an example

    class ImageColorVC: UIViewController
    {
        // UI Related
        private var loaderController: UIAlertController!
        let mainImage = UIImageView(image: UIImage(named: "art"))
        
        // Save the current color and the original image
        var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
        var originalImage: UIImage!
        
        override func viewDidLoad()
        {
            super.viewDidLoad()
            
            // UI configuration, you can ignore
            view.backgroundColor = .white
            title = "Image Color"
            configureBarButton()
            configureImageView()
            
            // Store the original image
            originalImage = mainImage.image!
        }
        
        // MARK: AUTO LAYOUT
        private func configureBarButton()
        {
            let barButton = UIBarButtonItem(barButtonSystemItem: .refresh,
                                            target: self,
                                            action: #selector(changeColors))
            navigationItem.rightBarButtonItem = barButton
        }
        
        private func configureImageView()
        {
            mainImage.translatesAutoresizingMaskIntoConstraints = false
            mainImage.contentMode = .scaleAspectFit
            mainImage.clipsToBounds = true
            view.addSubview(mainImage)
            
            view.addConstraints([
                
                mainImage.leadingAnchor
                    .constraint(equalTo: view.leadingAnchor),
                mainImage.topAnchor
                    .constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                mainImage.trailingAnchor
                    .constraint(equalTo: view.trailingAnchor),
                mainImage.bottomAnchor
                    .constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
                
            ])
        }
        
        // Configures a loader to show while image is processing
        private func configureLoaderController()
        {
            loaderController = UIAlertController(title: nil,
                                                 message: "Processing",
                                                 preferredStyle: .alert)
            
            let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10,
                                                                         y: 5,
                                                                         width: 50,
                                                                         height: 50))
            loadingIndicator.hidesWhenStopped = true
            loadingIndicator.style = UIActivityIndicatorView.Style.medium
            loadingIndicator.startAnimating();
            loaderController.view.addSubview(loadingIndicator)
        }
        
        //MARK: FACTORY
        
        // Similar to your function, only difference is that it uses
        // the original image
        private func performChangeColors()
        {
            let randomRGB =  CGFloat.random(in: 0.0...1.0)
            let randomRGB2 = CGFloat.random(in: 0.0...1.0)
            let randomRGB3 = CGFloat.random(in: 0.0...1.0)
            
            //randomly change color
            let newColor = UIColor.init(red: randomRGB3,
                                        green: randomRGB2,
                                        blue: randomRGB,
                                        alpha: 1)
            
            // Do work in the back ground
            DispatchQueue.global(qos: .background).async
            {
                let imageWithNewColor = self.replaceColor(color: self.currentColor,
                                                          withColor: newColor,
                                                          image: self.originalImage!,
                                                          tolerance: 0.5)
                
                // Update the UI on the main thread
                DispatchQueue.main.async
                {
                    self.updateImageView(with: imageWithNewColor)
                    
                    //change current color to new color
                    self.currentColor = newColor
                }
            }
        }
        
        @objc
        private func changeColors()
        {
            // Configure a loader to show while image is processing
            configureLoaderController()
            
            present(loaderController, animated: true) { [weak self] in
                self?.performChangeColors()
            }
        }
        
        // Update the UI
        private func updateImageView(with image: UIImage)
        {
            dismiss(animated: true) { [weak self] in
                self?.mainImage.image = image
            }
        }
    }
    

    After starting with this:

    Change color of pixels in an image swift iOS

    About 50 tries later, it still seems to work well:

    Change color of UIImage pixels swift iOS

    You can watch a longer video here to see a few more color changes that happen without leading to one single color

    Hope this gives you enough to create the required workaround for your solution