Ble21
Ble21

Reputation: 43

Control black and white coloring using CIImage filter in swift

My code converts a colored NSImage into grayscale. It then converts the grayscale into pure black and white image. Is there a way to create a filter which can specify how to convert the gray pixels into black / white? For example - 1. If pixel value > 100, turn it into black. 2. Else turn the pixel into white.

What filter settings can I use to achieve this?

Updated with CIKernel custom filter:

func applyCustomCIFilter( path: String )->NSImage{
    let url = URL(fileURLWithPath: path)
    let origImage = CIImage(contentsOf: url)!

    // Convert image into grayscale
    let grayImage = CIFilter(name: "CIPhotoEffectNoir", parameters: [kCIInputImageKey: origImage])?.outputImage
    print("convert into black and white")

    // custom filter to convert light gray to white and dark gray to black
    let replaceGrayKernel = CIColorKernel( source:
        "kernel vec4 replaceGrayWithBlackOrWhite(sampler grayImage) {" +
            "if( sample(grayImage, samplerCoord(grayImage)).rgb > 0.7  ){" +
            "return vec4(0.0,0.0,0.0,1.0);" +
            "}" +
            "else" +
            "{" +
            "return vec4(1.0,1.0,1.0,1.0);" +
            "}" +
        "}"
    )

    //Apply custom filter to grayscale image. 
    //ERROR: blackAndWhiteImage is nil causing runtime failure
    let blackAndWhiteImage = replaceGrayKernel?.apply(extent: (grayImage!.extent), arguments: [grayImage as Any])

    //Convert CIImage to NSImage
    let rep = NSCIImageRep(ciImage: blackAndWhiteImage!)
    let nsImage = NSImage(size: rep.size)
    nsImage.addRepresentation(rep)

    return nsImage;
}

Upvotes: 1

Views: 1727

Answers (1)

user7014451
user7014451

Reputation:

You had 2 mistakes in your kernel code. Here's the correct code:

let kernel = CIColorKernel( source:
    "kernel vec4 replaceGrayWithBlackOrWhite(__sample s) {" +
        "if (s.r > 0.25 && s.g > 0.25 && s.b > 0.25) {" +
        "    return vec4(0.0,0.0,0.0,1.0);" +
        "} else {" +
        "    return vec4(1.0,1.0,1.0,1.0);" +
        "}" +
    "}"
)
  • The change from sampler to __ sample is because a CIColorKernel, which is optimized for working with one pixel at a time, just passes into the kernel that pixel. Thus, there's also no need for calling sample(samplerCoord()). CIWarpKernel, and CIKernel use sampler, as you send into the kernel a RIO (region of interest) and those kernels can access surrounding pixels... think blur effects.

  • The second change was to the if statement. if (s.rgb > 0.7) is comparing a vec3 (or three floats) to a single float. I had to play around with values once I corrected these and think that (a) using AND (&&) instead or OR (||) along with (b) lowering the threshold to 0.25 makes for a closer black/white image. Play around yourself with this to see what you want.

I've created a small Swift 5 project (using a hard-coded image) that uses this kernel.

There's no comments in it and it contains various extensions (and a subclass of GLKView) because I gleaned things from a production project. Besides focusing on your kernel code issue, one word of warning about the code - it contains several force-unwraps that should be removed for "production-ready" code.

Upvotes: 2

Related Questions