Colorize a UIImage in Swift
Asked Answered
S

2

1

I am trying to write an helper function that applies a color mask to a given image. My function has to set all opaque pixels of an image to the same color.

Here is what I have so far :

extension UIImage {

    func applyColorMask(color: UIColor, context: CIContext) -> UIImage {

        guard let cgImageInput = self.cgImage else {
            print("applyColorMask: \(self) has no cgImage attribute.")
            return self
        }

        // Throw away existing colors, and fill the non transparent pixels with the input color
        // s.r = dot(s, redVector), s.g = dot(s, greenVector), s.b = dot(s, blueVector), s.a = dot(s, alphaVector)
        // s = s + bias
        let colorFilter = CIFilter(name: "CIColorMatrix")!
        let ciColorInput = CIColor(cgColor: color.cgColor)
        colorFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputRVector")
        colorFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector")
        colorFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector")
        colorFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
        colorFilter.setValue(CIVector(x: ciColorInput.red, y: ciColorInput.green, z: ciColorInput.blue, w: 0), forKey: "inputBiasVector")
        colorFilter.setValue(CIImage(cgImage: cgImageInput), forKey: kCIInputImageKey)

        if let cgImageOutput = context.createCGImage(colorFilter.outputImage!, from: colorFilter.outputImage!.extent) {
            return UIImage(cgImage: cgImageOutput)
        } else {
            print("applyColorMask: failed to apply filter to \(self)")
            return self
        }
    }
}

The code works fine for black and white but not what I expected when applying funnier colors. See the original image and the screenshots below: the same color is used for the border and for the image. Though they're different. My function is doing wrong. Did I miss something in the filter matrix ?

The original image (there's white dot at the center):

Original pictures

From top to bottom: The image filtered with UIColor(1.0, 1.0, 1.0, 1.0) inserted into a UIImageView which has borders of the same color. Then the same with UIColor(0.6, 0.5, 0.4, 1.0). And finally with UIColor(0.2, 0.5, 1.0, 1.0)

Screenshot


EDIT

Running Filterpedia gives me the same result. My understanding of the CIColorMatrix filter may be wrong then. The documentation says:

This filter performs a matrix multiplication, as follows, to transform the color vector:

  • s.r = dot(s, redVector)
  • s.g = dot(s, greenVector)
  • s.b = dot(s, blueVector)
  • s.a = dot(s, alphaVector)
  • s = s + bias

Then, let say I throw up all RGB data with (0,0,0,0) vectros, and then pass (0.5, 0, 0, 0) mid-red as the bias vector; I would expect my image to have all its fully opaque pixels to (127, 0, 0). The screenshots below shows that it is slightly lighter (red=186):

Fliterpedia screenshot

Here is some pseudo code I want to do:

// image "im" is a vector of pixels
// pixel "p" is struct of rgba values
// color "col" is the input color or a struct of rgba values
for (p in im) {
    p.r = col.r
    p.g = col.g
    p.b = col.b
    // Nothing to do with the alpha channel
}
Snipe answered 7/5, 2018 at 14:28 Comment(8)
If you set your vectors (x:y:z:w:) to: 1,0,0,0 for red, 0,1,0,0 for green, and 0,0,1,0 for blue, you get a normal looking picture. Now, to reduce the red channel by half, set inputRVector to 0.5,0,0,0. To remove all red set it to 0,0,0,0. That's basically how I use it. I'm sure there's other combinations to get other results, but that should get you started.Davie
Here's a Swift 4 version of Filterpedia - github.com/rhoeper/Filterpedia-Swift4 I haven't tried it yet (I have my own Swift 4 port) but it's a great resource for trying out each filter. All input parameters are exposed as UISliders, displaying you the min/max/current values. If it's like the original version, there's six different test images along with a few dozen custom filters using either Metal or CIKernel (that use GLSL). One caveat: it's meant to run on iPad only (you can use the simulator but like any CoreImage projects the performance is very poor).Davie
Thank you for the link @dfd, I tried first to set the rgb vectors to (theRedlwant,0,0,0) (0,theBluelwant,0,0) and (0,0,theGreenIWant,0) but as you said the 1 is use to get a normal looking picture. My goal is to throw away all the color information and get it monochromatic. Just keeping the alpha channel. It will be mostly use to re-colorize buttons images or logos.Snipe
I'll try it the link on my iPad, or on the simulator. I also think there must be another combination.Snipe
Are you trying to end up with a greyscale image? If so, you may get what you want with another "pre-configured" filter like CIPhotoProcessingMono. Also, if you know how to write/consume a CIColorKernel, I can give you the GLSL code for that. It's pretty simple - 4 lines of code wrapped around a kernel vec4 function.Davie
I tried Filterpedia, but the result is the same, the output image is always slightly lighter than expected. I will edited my question with a screenshot.Snipe
The color is important, thus I do not want greyscale. The inputs of my function are the image itself and the color. But it could be interessant to make my image grayscaled first and then colorize it. I don't know how to a CIColorKernel yet but I can explain what I aim to do with a very simple pseudo code (pixel by pixel).Snipe
Ok, I think I understand. You want to take an image pixel-by-pixel and (at least in your example) change each RGB channel. For instance, let's say a pixel has the RGB value of (1.0, 0.4, 0.4) - I have no idea what the color is! - and you pass in a 'col' of (0.5, 0.0, 0.0). You want the output to be R=1.0*0.5, G=0.4*0.0, B=0.4*0.0, yielding (0.5, 0.0, 0.0). Keep in mind, UIKit uses 0-254 but CoreImage uses 0-1. That's a pretty simple CIColorKernel. If I'm correct in what you want, let me know an I'll write up the Swift code to do it.Davie
S
3

I finally wrote a CIColorKernel, as @dfd suggested and it works fine:

class ColorFilter: CIFilter {

    var inputImage: CIImage?
    var inputColor: CIColor?

    let kernel: CIColorKernel = {
        let kernelString = "kernel vec4 colorize(__sample pixel, vec4 color)\n"
            + "{\n"
            + "    pixel.rgb = color.rgb;\n"
            + "    return pixel;\n"
            + "}\n"

        return CIColorKernel(source: kernelString)!
    }()

    override var outputImage: CIImage? {

        guard let inputImage = inputImage else {
            print("\(self) cannot produce output because no input image provided.")
            return nil
        }
        guard let inputColor = inputColor else {
            print("\(self) cannot produce output because no input color provided.")
            return nil
        }

        let inputs = [inputImage, inputColor] as [Any]
        return kernel.apply(extent: inputImage.extent, arguments: inputs)
    }
}

To summarize, the CIColorMatrix I used first seems not to be linear (when using the bias vector). Giving a red 0,5 (float) value did not output an image with red 127 color in the [0-255] interval.

Writing a custom filter was my solution.

Snipe answered 10/5, 2018 at 9:47 Comment(0)
D
1

Glad to have helped. If I may, one thing. You can send a color into a kernel - it's a vec4, with the fourth value being the alpha channel. Just remember that CoreImage uses 0-1, not 0-254.

Here's the kernel code:

kernel vec4 colorize(__sample pixel, vec4 color) {
    pixel.rgb = color.rgb;
    return pixel;
}

It's pretty much the same as your's. But now all you need to do is create a CIColor instead of an image. If you already have a UIColor called inputColor, just do this:

let ciColor = CIColor(color: inputColor)
var inputs = [inputImage, ciColor] as [Any]

A couple of FYIs.

  • There's also a vec3 that can be used but since you already have a UIColor `vec4 looks to be the easiest way.
  • __sample is the value of the pixel being processed, so it's "base type" is really a vec4.
Davie answered 11/5, 2018 at 12:3 Comment(1)
Thank you ! indeed, making a whole image is a bit extra, so far I've only wrote OpenGL GLSL shaders bound with C++, so I was not sure. I'll update my answer with a simple vec4 !Snipe

© 2022 - 2024 — McMap. All rights reserved.