From Chaining/Blending to Compositing Core Image CIFilter

dotnetmauiman

dotnetmauiman

Posted on September 6, 2022

From Chaining/Blending to Compositing Core Image CIFilter

Core Image is a powerful iOS framework that makes hardware-accelerated image manipulation easy. Oftentimes, as developers, we use it to add graphical effects to images in our app. The typical process involves choosing a right CIFilter, setting parameters, and applying the filter to an image.

Sometimes, the process may involve chaining several filters to get the desired result. For example, we can use a CIColorControls filter to first adjust the saturation of an image followed by applying a Vignette filter to add a dark fading border around the edges.

Background for a wrapper Library

Core Image CIFilter by itself is already a full fledged library that we are able to use independently. Furthermore, we often find ourselves referring to the open-source Filterpedia project by Simon Gladman for implementation details. With these, we should already have everything that we need. So, why bother with another library?

There may be times when we need to go beyond just chaining a few CIFilter. We may need to chain filters, blend the output with another image, and then apply more filters to achieve the desired result. Or in other words, we may need to apply some kind of node graph to get the effect we wanted. As developers, we often find ourselves in this programmatical chaining and blending process repeatedly.

For example, a CIEdgeWork filter produces a stylized black-and-white rendition of an image that looks similar to a woodblock cutout. The output of this filter, however, requires a background image to visualize. This requires a composite filter, CISourceAtopCompositing, to place the output of CIEdgeWork over a constant color background (CIConstantColor).

CIFilter

Beyond this, we may want to twirl the output, and then further apply an Addition composition/blend with another image. We often need to test this out in programming code.

Nodef library

The Nodef library is a very simple wrapper for applying a node graph (of CIFilter) on an image. The library is the same library used in the open-source Nodef app that that reimagines node-based compositing on a mobile device with an innovative Node Pipeline. The app enables us to perform many of the compositing behavior we desire without changing a single line of code.

Besides providing a library for node-based compositing, the library also provides default values for each of the different filters and implements the 'Codable' protocol for saving and loading a node graph in JSON. Using the JSON file, we can be creatively compositing on a mobile device, saving the composite as a file and then loading it on our desktop computer with an application or command line tool.

Happy Compositing on Mobile!

Create a CIColorMonochrome filter

func createOneFilter(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()
    filters.add(filterHolder: filters.getFilterWithHolder("Color Monochrome"))
    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

chainfilters

Chain a CISepiaTone filter and CIZoomBlur filter

CISepiaTone takes the original image as the input image and CIZoomBlur takes the output of CISepiaTone as the input image.

func chainFilters(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()
    filters.add(filterHolder: filters.getFilterWithHolder("Sepia Tone"))
    filters.add(filterHolder: filters.getFilterWithHolder("Zoom Blur"))
    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

Apply a CIDotScreen filter and then CISubtractBlendMode it with the original image

func blendFilters(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()

    filters.add(filterHolder: filters.getFilterWithHolder("Dot Screen"))
    filters.add(filterHolder: filters.getFilterWithHolder("Subtract Blend Mode"))

    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

CISubtractBlendMode takes the output of CIDotScreen as the inputImage and the original image as the backgroundImage.

Blending

Create a CICheckboardGenerator filter

CICheckboardGenerator requires no input Image.

func generatorFilters(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()

    filters.size=CGSize(width:inputImage.size.width, height:inputImage.size.height)
    filters.add(filterHolder: filters.getFilterWithHolder("Checkerboard Generator"))

    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

Changing CIFilter properties through the wrapper class

Changing the width of CICheckboardGenerator.

func filterProperties(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()
    filters.size=CGSize(width:inputImage.size.width, height:inputImage.size.height)

    let fxHolder=filters.getFilterWithHolder("Checkerboard Generator")
    (fxHolder.filter as! CheckerboardGeneratorFX).width = 500
    filters.add(filterHolder: fxHolder)

    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

Checkerboard

Compositing CIFilter

Apply a CIMultiplyBlendMode on the CIColorMonochrome version of the original image with a CICheckboardGenerator.

func compositingFilters(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()

    filters.size=CGSize(width:inputImage.size.width, height:inputImage.size.height)

    filters.add(filterHolder: filters.getFilterWithHolder("Color Monochrome")) //Node 1
    filters.add(filterHolder: filters.getFilterWithHolder("Checkerboard Generator")) //Node 2

    let fxHolder=filters.getFilterWithHolder("Multiply Blend Mode")
    (fxHolder.filter as! MultiplyBlendModeFX).inputImageAlias = "2"
    (fxHolder.filter as! MultiplyBlendModeFX).backgroundImageAlias = "1"
    filters.add(filterHolder: fxHolder)

    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

Compositing

Node Graph Compositing CIFilters

First, chain the original image with the following filters.

Original Image -> CILineScreen -> CIColorMonochrome
Enter fullscreen mode Exit fullscreen mode

Next, generate a checkboard and then apply a triangle tile.

CheckboardGenerator -> CITriangleTile
Enter fullscreen mode Exit fullscreen mode

Finally, multiply blend the output of the color monochrome with the output of the triangle tile.

CIMultiplyBlendMode on CIColorMonochrome and CITriangleTile
Enter fullscreen mode Exit fullscreen mode

Image description

The Swift code for the node graph above.

func nodeGraphFilters(_ inputImage: UIImage) -> UIImage {

    let filters = FiltersX()

    filters.size=CGSize(width:inputImage.size.width, height:inputImage.size.height)

    filters.add(filterHolder: filters.getFilterWithHolder("Line Screen")) //Node 1
    filters.add(filterHolder: filters.getFilterWithHolder("Color Monochrome")) //Node 2
    filters.add(filterHolder: filters.getFilterWithHolder("Checkerboard Generator")) //Node 3
    filters.add(filterHolder: filters.getFilterWithHolder("Triangle Tile")) //Node 4

    let fxHolder=filters.getFilterWithHolder("Multiply Blend Mode")
    (fxHolder.filter as! MultiplyBlendModeFX).inputImageAlias = "4"
    (fxHolder.filter as! MultiplyBlendModeFX).backgroundImageAlias = "2"
    filters.add(filterHolder: fxHolder)

    return filters.applyFilters(image: inputImage)

}
Enter fullscreen mode Exit fullscreen mode

Saving the Node Graph as a JSON string

    let filters = FiltersX()
    filters.add(filterHolder: filters.getFilterWithHolder("Color Controls"))
    filters.add(filterHolder: filters.getFilterWithHolder("Sepia Tone"))
    filters.add(filterHolder: filters.getFilterWithHolder("Zoom Blur"))
    pageSettings.filters = filters

    let encoder=JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    let pageSettingsData = (try? encoder.encode(pageSettings))!
    let pageSettingsDataStr = String(data: pageSettingsData, encoding: .utf8)!

    var jsonObject: [String: String] = [String: String]()
    var savedJSONStr = ""
    jsonObject["page_settings"]=pageSettingsDataStr


    if let jsonData = try? encoder.encode(jsonObject) {
        if let jsonString = String(data: jsonData, encoding: .utf8) {

            var jsonLabel: [String: String] = [String: String]()
            jsonLabel["nodef"]=jsonString

            if let jsonLabelData = try? encoder.encode(jsonLabel) {
                if let jsonLabelString = String(data: jsonLabelData, encoding: .utf8) {
                    savedJSONStr=jsonLabelString
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Loading the JSON String for initializing the node graph

    if let data = savedJSONStr.data(using: .utf8) {
                let labelDictionary : [String: Any] = (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any])!
                let labelStr = labelDictionary["nodef"] as? String

                if let attributesData = labelStr!.data(using: .utf8) {
                    let attributesDictionary : [String: Any] = (try? JSONSerialization.jsonObject(with: attributesData, options: []) as? [String: Any])!
                    let loadedPageSettingsStr = attributesDictionary["page_settings"] as? String

                    print(loadedPageSettingsStr as Any)
                }
    }    
Enter fullscreen mode Exit fullscreen mode

will give you

{  
   filters : {    
    filterList : [      
    {
      type : CIColorControls,      
      alias : "",        
      inputImageAlias : "",        
      backgroundImageAlias : "",        
      brightness : 0,        
      saturation : 1,        
      contrast : 1        
    },      
    {
      type : CISepiaTone,        
      alias : "",        
      inputImageAlias : "",        
      backgroundImageAlias : "",        
      intensity : 1      
    },      
    {        
     type : CIZoomBlur,      
     alias : "",        
     inputImageAlias : "",        
     backgroundImageAlias : "",        
     centerX : 0,        
     centerY : 0,        
     amount : 20        
    }    
    ],    
    size : [0,0]  
    }
   }
Enter fullscreen mode Exit fullscreen mode

Compiling the Source

Prerequisites

  • XCode 13
  • iOS 15
  • Build

Download the Source Code
Launch XCode and load Nodef.xcodeproj
Build and run on iPhone Simulator or Device

or

Download Nodef on the App Store

💖 💪 🙅 🚩
dotnetmauiman
dotnetmauiman

Posted on September 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

New open-source swift macros
swift New open-source swift macros

September 6, 2024

Input stepper
swift Input stepper

November 28, 2021