How do you chain CIFilter

In Apple's documentation it says this:


"This method, though convenient, is inefficient if used multiple times in succession. Achieve better performance by chaining filters without asking for the outputs of individual filters."


That confuses me though because I don't know how to link them together without getting the output. For example, this is my method to apply a TiltShift filter, based on the instructions from Apple's docs. When I perform the gradient filter, I have to take the outputImage of that to pass into the next filter. What's the right way to be doing this?


    override public var outputImage: CIImage? {
        guard let inputImage = inputImage else {
            return nil
        }

        let clamped = inputImage.clampedToExtent()
        let blurredImage = clamped.applyingGaussianBlur(sigma: inputRadius)

        var gradientParameters = [
            "inputPoint0": CIVector(x: 0, y: 0.75 * inputImage.extent.height),
            "inputColor0": CIColor(red: 0, green: 1, blue: 0, alpha: 1),
            "inputPoint1": CIVector(x: 0, y: 0.5 * inputImage.extent.height),
            "inputColor1": CIColor(red: 0, green: 1, blue: 0, alpha: 0)
        ];

        guard let gradientImage = ciImage(from: "CILinearGradient", parameters: gradientParameters) else {
            return nil
        }

        gradientParameters["inputPoint0"] = CIVector(x: 0, y: 0.25 * inputImage.extent.height)

        guard let backgroundGradientImage = ciImage(from: "CILinearGradient", parameters: gradientParameters) else {
            return nil
        }

        let maskParameters = [
            kCIInputImageKey: gradientImage,
            kCIInputBackgroundImageKey: backgroundGradientImage
        ]


        guard let maskImage = ciImage(from: "CIAdditionCompositing", parameters: maskParameters) else {
            return nil
        }


        let combinedParameters = [
            kCIInputImageKey: blurredImage,
            kCIInputBackgroundImageKey: clamped,
            kCIInputMaskImageKey: maskImage
        ]


        return ciImage(from: "CIBlendWithMask", parameters: combinedParameters)
    }

    private func ciImage(from filterName: String, parameters: [String: Any]) -> CIImage? {
        guard let filtered = CIFilter(name: filterName, parameters: parameters) else {
            return nil
        }

        return filtered.outputImage
    }
Post not yet marked as solved Up vote post of Gargoyle Down vote post of Gargoyle
2.7k views

Replies

I had to search a bit before understanding what method you were referring to - seems to imageByApplyingFilter? I also find that note quite weird as it does not sound like this convenience method does anything but setting the inputImage and returning outputImage and I would not expect this to trigger any kind of rendering at all

I had the same question and find this confusing as well. I have submitted a "bug report" for clarification in the documentation; I recommend anyone with the same question do the same.


The documentaion in question is: https://developer.apple.com/documentation/coreimage/ciimage/2915368-imagebyapplyingfilter?language=objc

On the Apple documentation archive there is a section under the Core Image Programming Guide about 'Chaining Filters for Complex Effects'. It explains why chaining multiple filters should be done as follows to increase efficiency.[https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_tasks/ci_tasks.html]

Filter chain example in Swift:

    // Create a filter (Photo effect)
    let colorFilter: CIFilter = CIFilter(name: "CIPhotoEffectProcess", parameters: [kCIInputImageKey: image])!
     
    // Apply second filter (Bloom) onto resulting image of first filter
    let bloomImage: CIImage = colorFilter.outputImage!.applyingFilter("CIBloom", parameters: [kCIInputRadiusKey: 10.0, kCIInputIntensityKey: 1.0])
     
    // Apply third filter (Exposure) from second's filtered CIImage result
    let exposeImage: CIImage = bloomImage.applyingFilter("CIExposureAdjust", parameters: [kCIInputEVKey: 1.0])
     
    return exposeImage
  }

Please do note that the arrangement of the order of the chain affects the final image output. So putting 'filter A' before 'filter B' will result in something different opposed to 'filter A' after 'filter B'.

Messed upped the formatting in my previous answer. Here is the full code again:

func applyFilterChain(_ image: CIImage) -> CIImage {
    // Create a filter (Photo effect)
    let colorFilter: CIFilter = CIFilter(name: "CIPhotoEffectProcess", parameters: [kCIInputImageKey: image])!
     
    // Apply second filter (Bloom) onto resulting image of first filter
    let bloomImage: CIImage = colorFilter.outputImage!.applyingFilter("CIBloom", parameters: [kCIInputRadiusKey: 10.0, kCIInputIntensityKey: 1.0])
     
    // Apply third filter (Exposure) from seconds filtered CIImage result
    let exposeImage: CIImage = bloomImage.applyingFilter("CIExposureAdjust", parameters: [kCIInputEVKey: 1.0])
     
    return exposeImage
  }

@Stijn_Klomp I'm not sure if this example clarifies anything, as the filter chain is still "asking for the outputs of individual filters" by calling outputImage