mjswensen
mjswensen

Reputation: 3124

Change color of certain pixels in a UIImage

For a given multi-color PNG UIImage (with transparency), what is the best/Swift-idiomatic way to:

  1. create a duplicate UIImage
  2. find all black pixels in the copy and change them to red
  3. (return the modified copy)

There are a few related questions on SO but I haven't been able to find something that works.

Upvotes: 30

Views: 16867

Answers (3)

sabiland
sabiland

Reputation: 2614

Just to give you massive speed improved version via DispatchQueue.concurrentPerform for manipulating pixels. Here is my version to make complemented version of an input image.

To give some perspective - to invert an image this way, it takes a little bit longer then native Apple's solution for inverting an image.

EDIT: I am using DynamicColor library for complementing colors. You also need converter from RGBA32 to UIColor and vice versa.

static func processPixelsPixelation1(inputCGImage: CGImage, pixelBuffer: UnsafeMutablePointer<RGBA32>)
{
    let width            = inputCGImage.width
    let height           = inputCGImage.height
    
    DispatchQueue.concurrentPerform(iterations: Int(height) - 1) { row in
        DispatchQueue.concurrentPerform(iterations: Int(width) - 1) { column in
            let offset = row * width + column
            let color = pixelBuffer[offset]
            let c = color.convertToUIColor()
            let modified = c.complemented()
            let modifiedToRGBA = modified.convertToRGBA32()
            pixelBuffer[offset] = modifiedToRGBA
        }
    }
}

Use processor as a generic pointer to the function (as an input parameter) (in this case processPixelsPixelation1):

.
.
.
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
// PROCESS IMAGE !!!
processor(inputCGImage, pixelBuffer)
.
.
.

extension RGBA32
{
    func convertToUIColor() -> UIColor
    {
        return UIColor(
            red: self.redComponent,
            green: self.greenComponent,
            blue: self.blueComponent,
            alpha: self.alphaComponent
        )
    }
}

typealias ColorComponents = (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)
extension UIColor
{
    func getColorComponents() -> ColorComponents
    {
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        self.getRed(&r, green: &g, blue: &b, alpha: &a)
        return ColorComponents(r, g, b, a)
    }

    convenience init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        self.init(
            red: CGFloat(red) / 255,
            green: CGFloat(green) / 255,
            blue: CGFloat(blue) / 255,
            alpha: CGFloat(alpha) / 255
        )
    }

    func convertToRGBA32() -> RGBA32
    {
        let tmpComponents = self.getColorComponents()
        return RGBA32(
            red: UInt8(255 * tmpComponents.r),
            green: UInt8(255 * tmpComponents.g),
            blue: UInt8(255 * tmpComponents.b),
            alpha: UInt8(255 * tmpComponents.a)
        )
    }
}

Upvotes: 0

Coder ACJHP
Coder ACJHP

Reputation: 2214

For getting better result we can search color range in image pixels, referring to @Rob answer I made update and now the result is better.

func processByPixel(in image: UIImage) -> UIImage? {

    guard let inputCGImage = image.cgImage else { print("unable to get cgImage"); return nil }
    let colorSpace       = CGColorSpaceCreateDeviceRGB()
    let width            = inputCGImage.width
    let height           = inputCGImage.height
    let bytesPerPixel    = 4
    let bitsPerComponent = 8
    let bytesPerRow      = bytesPerPixel * width
    let bitmapInfo       = RGBA32.bitmapInfo

    guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
        print("Cannot create context!"); return nil
    }
    context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))

    guard let buffer = context.data else { print("Cannot get context data!"); return nil }

    let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)

    for row in 0 ..< Int(height) {
        for column in 0 ..< Int(width) {
            let offset = row * width + column

           /*
             * Here I'm looking for color : RGBA32(red: 231, green: 239, blue: 247, alpha: 255)
             * and I will convert pixels color that in range of above color to transparent
             * so comparetion can done like this (pixelColorRedComp >= ourColorRedComp - 1 && pixelColorRedComp <= ourColorRedComp + 1 && green && blue)
             */

            if pixelBuffer[offset].redComponent >=  230 && pixelBuffer[offset].redComponent <=  232 &&
                pixelBuffer[offset].greenComponent >=  238 && pixelBuffer[offset].greenComponent <=  240 &&
                pixelBuffer[offset].blueComponent >= 246 && pixelBuffer[offset].blueComponent <= 248 &&
                pixelBuffer[offset].alphaComponent == 255 {
                pixelBuffer[offset] = .transparent
            }
        }
    }

    let outputCGImage = context.makeImage()!
    let outputImage = UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)

    return outputImage
}

I hope this helps someone 🎉

Upvotes: 2

Rob
Rob

Reputation: 437682

You have to extract the pixel buffer of the image, at which point you can loop through, changing pixels as you see fit. At the end, create a new image from the buffer.

In Swift 3, this looks like:

func processPixels(in image: UIImage) -> UIImage? {
    guard let inputCGImage = image.cgImage else {
        print("unable to get cgImage")
        return nil
    }
    let colorSpace       = CGColorSpaceCreateDeviceRGB()
    let width            = inputCGImage.width
    let height           = inputCGImage.height
    let bytesPerPixel    = 4
    let bitsPerComponent = 8
    let bytesPerRow      = bytesPerPixel * width
    let bitmapInfo       = RGBA32.bitmapInfo

    guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
        print("unable to create context")
        return nil
    }
    context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))

    guard let buffer = context.data else {
        print("unable to get context data")
        return nil
    }

    let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)

    for row in 0 ..< Int(height) {
        for column in 0 ..< Int(width) {
            let offset = row * width + column
            if pixelBuffer[offset] == .black {
                pixelBuffer[offset] = .red
            }
        }
    }

    let outputCGImage = context.makeImage()!
    let outputImage = UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)

    return outputImage
}

struct RGBA32: Equatable {
    private var color: UInt32

    var redComponent: UInt8 {
        return UInt8((color >> 24) & 255)
    }

    var greenComponent: UInt8 {
        return UInt8((color >> 16) & 255)
    }

    var blueComponent: UInt8 {
        return UInt8((color >> 8) & 255)
    }

    var alphaComponent: UInt8 {
        return UInt8((color >> 0) & 255)
    }        

    init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        let red   = UInt32(red)
        let green = UInt32(green)
        let blue  = UInt32(blue)
        let alpha = UInt32(alpha)
        color = (red << 24) | (green << 16) | (blue << 8) | (alpha << 0)
    }

    static let red     = RGBA32(red: 255, green: 0,   blue: 0,   alpha: 255)
    static let green   = RGBA32(red: 0,   green: 255, blue: 0,   alpha: 255)
    static let blue    = RGBA32(red: 0,   green: 0,   blue: 255, alpha: 255)
    static let white   = RGBA32(red: 255, green: 255, blue: 255, alpha: 255)
    static let black   = RGBA32(red: 0,   green: 0,   blue: 0,   alpha: 255)
    static let magenta = RGBA32(red: 255, green: 0,   blue: 255, alpha: 255)
    static let yellow  = RGBA32(red: 255, green: 255, blue: 0,   alpha: 255)
    static let cyan    = RGBA32(red: 0,   green: 255, blue: 255, alpha: 255)

    static let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue

    static func ==(lhs: RGBA32, rhs: RGBA32) -> Bool {
        return lhs.color == rhs.color
    }
}

For Swift 2 rendition, see previous revision of this answer.

Upvotes: 66

Related Questions