I have the following source image:
I am trying to convert it to:
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:
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?
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.