1

I have a matrix of greyscale image pixels, for example:

[ [0, 0, 125],
  [10, 50, 255],
  [90, 0, 255] ]

My goal is to apply a tint to it (UIColor) and export a CGImage/UIImage from the structure that holds it.

public typealias Pixel = UInt8

extension UIColor {
    var red: Float { return Float(CIColor(color: self).red * 255) }
    var green: Float { return Float(CIColor(color: self).green * 255) }
    var blue: Float { return Float(CIColor(color: self).blue * 255) }
    var alpha: Float { return Float(CIColor(color: self).alpha * 255) }
}

public struct PixelData {
    let r: UInt8
    let g: UInt8
    let b: UInt8
    let a: UInt8
}

public struct Map {
    let pixelCount: UInt
    let pixels: [Pixel] //all pixels of an image, linear
    let dimension: UInt //square root of pixel count
    let tintColor: UIColor = UIColor(red: 9/255, green: 133/255, blue: 61/255, alpha: 1)

    public var image: UIImage? {
        var pixelsData = [PixelData]()
        pixelsData.reserveCapacity(Int(pixelCount) * 3)
        let alpha = UInt8(tintColor.alpha)
        let redValue = tintColor.red
        let greenValue = tintColor.green
        let blueValue = tintColor.blue
        let red: [PixelData] = pixels.map {
            let redInt: UInt8 = UInt8((Float($0) / 255.0) * redValue)
            return PixelData(r: redInt, g: 0, b: 0, a: alpha)
        }
        let green: [PixelData] = pixels.map {
            let greenInt: UInt8 = UInt8((Float($0) / 255.0) * greenValue)
            return PixelData(r: 0, g: greenInt, b: 0, a: alpha) }
        let blue: [PixelData] = pixels.map {
            let blueInt: UInt8 = UInt8((Float($0) / 255.0) * blueValue)
            return PixelData(r: 0, g: 0, b: blueInt, a: alpha) }
        pixelsData.append(contentsOf: red)
        pixelsData.append(contentsOf: green)
        pixelsData.append(contentsOf: blue)
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
        let bitsPerComponent = 8
        let bitsPerPixel = 32
        let dimension: Int = Int(self.dimension)
        var data = pixelsData
        guard let providerRef = CGDataProvider(
            data: NSData(bytes: &data, length: data.count * MemoryLayout<PixelData>.size)
            ) else { return nil }
        if let cgim = CGImage(
            width: dimension,
            height: dimension,
            bitsPerComponent: bitsPerComponent,
            bitsPerPixel: bitsPerPixel,
            bytesPerRow: dimension * MemoryLayout<PixelData>.size,
            space: rgbColorSpace,
            bitmapInfo: bitmapInfo,
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
            ) {
            return UIImage(cgImage: cgim)
        }
        return nil
    }
}

The problem is the output looks gibberish. I have used this tutorial and this SO thread but with no success. The result in the playground is:

enter image description here

(the output is there, just barely visible)

Any help appreciated!

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Kacper Cz
  • 504
  • 3
  • 17

1 Answers1

1

There are two key issues.

  1. The code is calculating all the red values for every grayscale pixel and creating the four byte PixelData for each (even though only the red channel is populated) and adding that to the pixelsData array. It then repeats that for the green values, and then again for the blue values. That results in three times as much data as one needs for the image, and only the red channel data is being used.

    Instead, we should calculate the RGBA values once, create a PixelData for each, and repeat this pixel by pixel.

  2. The premultipliedFirst means ARGB. But your structure is using RGBA, so you want premultipliedLast.

Thus:

func generateTintedImage(completion: @escaping (UIImage?) -> Void) {
    DispatchQueue.global(qos: .userInitiated).async {
        let image = self.tintedImage()
        DispatchQueue.main.async {
            completion(image)
        }
    }
}

private func tintedImage() -> UIImage? {
    let tintRed = tintColor.red
    let tintGreen = tintColor.green
    let tintBlue = tintColor.blue
    let tintAlpha = tintColor.alpha

    let data = pixels.map { pixel -> PixelData in
        let red = UInt8((Float(pixel) / 255) * tintRed)
        let green = UInt8((Float(pixel) / 255) * tintGreen)
        let blue = UInt8((Float(pixel) / 255) * tintBlue)
        let alpha = UInt8(tintAlpha)
        return PixelData(r: red, g: green, b: blue, a: alpha)
    }.withUnsafeBytes { Data($0) }

    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    let bitsPerComponent = 8
    let bitsPerPixel = 32

    guard
        let providerRef = CGDataProvider(data: data as CFData),
        let cgImage = CGImage(width: width,
                              height: height,
                              bitsPerComponent: bitsPerComponent,
                              bitsPerPixel: bitsPerPixel,
                              bytesPerRow: width * MemoryLayout<PixelData>.stride,
                              space: rgbColorSpace,
                              bitmapInfo: bitmapInfo,
                              provider: providerRef,
                              decode: nil,
                              shouldInterpolate: true,
                              intent: .defaultIntent)
    else {
        return nil
    }

    return UIImage(cgImage: cgImage)
}

I’ve also renamed a few variables, used stride instead of size, replaced dimension with width and height so I could process non-square images, etc.

I also would advise against using a computed property for anything this computationally intense, so I gave this an asynchronous method, which you might use as follows:

let map = Map(with: image)
map.generateTintedImage { image in
    self.tintedImageView.image = image
}

Anyway, the above yields the following, where the rightmost image is your tinted image:

enter image description here


Needless to say, to convert your matrix into your pixels array, you can just flatten the array of arrays:

let matrix: [[Pixel]] = [
    [0, 0, 125],
    [10, 50, 255],
    [90, 0, 255]
]
pixels = matrix.flatMap { $0 }

Here is a parallelized rendition which is also slightly more efficient with respect to the memory buffer:

private func tintedImage() -> UIImage? {
    let tintAlpha = tintColor.alpha
    let tintRed = tintColor.red / 255
    let tintGreen = tintColor.green / 255
    let tintBlue = tintColor.blue / 255

    let alpha = UInt8(tintAlpha)

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
    let bitsPerComponent = 8
    let bytesPerRow = width * MemoryLayout<PixelData>.stride

    guard
        let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
        let data = context.data
    else {
        return nil
    }

    let buffer = data.bindMemory(to: PixelData.self, capacity: width * height)

    DispatchQueue.concurrentPerform(iterations: height) { row in
        let start = width * row
        let end = start + width
        for i in start ..< end {
            let pixel = pixels[i]
            let red = UInt8(Float(pixel) * tintRed)
            let green = UInt8(Float(pixel) * tintGreen)
            let blue = UInt8(Float(pixel) * tintBlue)
            buffer[i] = PixelData(r: red, g: green, b: blue, a: alpha)
        }
    }

    return context.makeImage()
        .flatMap { UIImage(cgImage: $0) }
}
Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Yes thank you! I noticed the `premultipliedLast` option shortly after posting the question. Also, by design I will be processing 256x256px images so that's why I decided to go for a computed property. I will of course test how long it takes for a picture this size. – Kacper Cz Jun 04 '19 at 06:06
  • 1
    No worries. I just wanted to explain why I changed what I did. By the way, when you think about how big your images will be, remember to think about how they’ll be rendered on a 3x retina device (because you presumably want them to be sharp as the device supports). Personally, I’d just assume I’d want to do it asynchronously (for slow devices and for 3x devices). That having been said, you’d eventually want to consider optimized algorithms (e.g. at least parallelize the algorithm, maybe even use Accelerate framework e.g. blas or vimage)... – Rob Jun 04 '19 at 06:34