iosswiftfiltercore-imagecifilter

Why is iOS CILightenBlendMode changing the light color?


I have the following source image:

enter image description here

I am trying to convert it to:

enter image description here

I have built this code:

import CoreImage

if let image = UIImage(named: "demo9"), let editted = applyDuotoneEffect(to: image) {
    imageView.image = editted
}

func applyDuotoneEffect(to image: UIImage) -> UIImage? {
    guard let ciImage = CIImage(image: image) else { return nil }
    
    let context = CIContext(options: nil)
    
    let grayscaleFilter = CIFilter(name: "CIPhotoEffectMono", parameters: [kCIInputImageKey: ciImage])
    
    let solidLightColorImage = CIImage(color: CIColor(cgColor: ("efa403".toUIColor() ?? .red).cgColor)).cropped(to: ciImage.extent)
    let multiplyFilter = CIFilter(name: "CIMultiplyBlendMode", parameters: [kCIInputImageKey: grayscaleFilter?.outputImage as Any, kCIInputBackgroundImageKey: solidLightColorImage as Any])
       
    
    let solidDarkColorImage = CIImage(color: "290a59".toCIColor() ?? .black).cropped(to: ciImage.extent)
    let lightenFilter = CIFilter(name: "CILightenBlendMode", parameters: [kCIInputImageKey: multiplyFilter?.outputImage as Any, kCIInputBackgroundImageKey: solidDarkColorImage as Any])
    
    guard let outputImage = lightenFilter?.outputImage,
          let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
    
    return UIImage(cgImage: cgImage)
}

extension String {
    func toUIColor() -> UIColor? {
        var cString = trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        
        if (cString.hasPrefix("#")) {
            cString.remove(at: cString.startIndex)
        }
        
        if ((cString.count) != 6) {
            return nil
        }
        
        var rgbValue: UInt64 = 0
        Scanner(string: cString).scanHexInt64(&rgbValue)
        
        return UIColor(
            red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
            green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
            blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
            alpha: CGFloat(1.0)
        )
    }
}

However, the yellow color background doesn't look accurate in the output:

enter image description here

It appears that the CILightenBlendMode filter changes the yellow color too even though I expected it to only change the blacks to purple. What am I doing wrong?


Solution

  • Your duotone effect isn't working because CILightenBlendMode changes more than just the blacks - it's affecting your yellow too.

    func applyDuotoneEffect(to image: UIImage) -> UIImage? {
        guard let ciImage = CIImage(image: image) else { return nil }
        
        let context = CIContext(options: nil)
        
        // Get grayscale
        let grayscaleFilter = CIFilter(name: "CIColorControls", parameters: [
            kCIInputImageKey: ciImage,
            kCIInputSaturationKey: 0.0
        ])
        
        // Use FalseColor filter - maps shadows to purple, highlights to yellow
        let falseColorFilter = CIFilter(name: "CIFalseColor", parameters: [
            kCIInputImageKey: grayscaleFilter?.outputImage as Any,
            "inputColor0": CIColor(hex: "290a59"), // Purple for shadows
            "inputColor1": CIColor(hex: "efa403")  // Yellow for highlights
        ])
        
        guard let outputImage = falseColorFilter?.outputImage,
              let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
        
        return UIImage(cgImage: cgImage)
    }
    
    // Helper for hex colors
    extension CIColor {
        convenience init(hex: String) {
            var cString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
            if cString.hasPrefix("#") { cString.remove(at: cString.startIndex) }
            var rgbValue: UInt64 = 0
            Scanner(string: cString).scanHexInt64(&rgbValue)
            self.init(
                red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
                green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
                blue: CGFloat(rgbValue & 0x0000FF) / 255.0
            )
        }
    }
    

    Fix: Replace the blend modes with CIFalseColor filter - it's specifically made for duotone effects. It maps dark areas to purple and light areas to yellow in one step.