Core Image Tutorial for iOS: Custom Filters

Learn to create your own Core Image filters using the Metal Shading Language to build kernels that provide pixel-level image processing. By Vidhur Voora.

Leave a rating/review
Download materials
Save for later
Share

Core Image is a powerful and efficient image processing framework. You can create beautiful effects using the built-in filters the framework provides, as well as create custom filters and image processors. You can adjust the color, geometry and perform complex convolutions.

Creating beautiful filters is an art, and one of the greatest artists was Leonardo da Vinci. In this tutorial, you’ll add some interesting touches to da Vinci’s famous paintings.

In the process, you’ll:

  • Get an overview of Core Image’s classes and built-in filters.
  • Create a filter using built-in filters.
  • Transform an image’s color using a custom color kernel.
  • Transform the geometry of an image using a custom warp kernel.
  • Learn to debug Core Image issues.

Get your paintbrushes, oops, I mean your Xcode ready. It’s time to dive into the amazing world of Core Image!

Getting Started

Download the project by clicking Download Materials at the top or bottom of this page. Open the RayVinci project in starter. Build and run.

RayVinci Getting Started

You’ll see four of Leonardo da Vinci’s most famous works. Tapping a painting opens a sheet, but the image’s output is empty.

In this tutorial, you’ll create filters for these images and then see the result of applying a filter in the output.

Swipe down to dismiss the sheet. Next, tap Filter List on the top right.

That button should show a list of available built-in filters. But wait, it’s currently empty. You’ll fix that next. :]

Introducing Core Image Classes

Before you populate the list of filters, you need to understand the Core Image framework’s basic classes.

You’ll see how to render the image to display later in this tutorial.

  • CIImage: Represents an image that is either ready for processing or produced by the Core Image filters. A CIImage object has all the image’s data within it but isn’t actually an image. It’s like a recipe that contains all the ingredients to make a dish but isn’t the dish itself.

    You’ll see how to render the image to display later in this tutorial.

  • CIFilter: Takes one or more images, processes each image by applying transformations and produces a CIImage as its output. You can chain multiple filters and create interesting effects. The objects of CIFilters are mutable and not thread-safe.
  • CIContext: Renders the processed results from the filter. For example, CIContext helps create a Quartz 2D image from a CIImage object.

To learn more about these classes, refer to the Core Image Tutorial: Getting Started.

Now that you’re familiar with the Core Image classes, it’s time to populate the list of filters.

Fetching the List of Built-In Filters

Open RayVinci and select FilterListView.swift. Replace filterList in FilterListView with:

let filterList = CIFilter.filterNames(inCategory: nil)

Here, you fetch the list of all available built-in filters provided by Core Image by using filterNames(inCategory:) and passing nil as the category. You can view the list of available categories in CIFilter‘s developer documentation.

Open FilterDetailView.swift. Replace Text("Filter Details") in body with:

// 1
if let ciFilter = CIFilter(name: filter) {
  // 2
  ScrollView {
    Text(ciFilter.attributes.description)
  }
} else {
  // 3
  Text("Unknown filter!")
}

Here, you:

  1. Initialize a filter, ciFilter, using the filter name. Since the name is a string and can be misspelled, the initializer returns an optional. For this reason, you’ll need to check for the existence of a filter.
  2. You can inspect the filter’s various attributes using attributes. Here, you create a ScrollView and populate the description of the attributes in a Text view if the filter exists.
  3. If the filter doesn’t exist or isn’t known, you show a Text view explaining the situation.

Build and run. Tap Filter List. Whoa, that’s a lot of filters!

Tap any filter to see its attributes.

Fetch List of available filters

Remarkable, isn’t it? You’re just getting started! In the next section, you’ll use one of these built-in filters to make the sun shine on the “Mona Lisa”. :]

Using Built-In Filters

Now that you’ve seen the list of available filters, you’ll use one of these to create an interesting effect.

Open ImageProcessor.swift. At the top, before the class declaration, add:

enum ProcessEffect {
  case builtIn
  case colorKernel
  case warpKernel
  case blendKernel
}

Here, you declare ProcessEffect as an enum. It has all the filter cases you’ll work on in this tutorial.

Add the following to ImageProcessor:

// 1
private func applyBuiltInEffect(input: CIImage) {
  // 2
  let noir = CIFilter(
    name: "CIPhotoEffectNoir",
    parameters: ["inputImage": input]
  )?.outputImage
  // 3
  let sunGenerate = CIFilter(
    name: "CISunbeamsGenerator",
    parameters: [
      "inputStriationStrength": 1,
      "inputSunRadius": 300,
      "inputCenter": CIVector(
        x: input.extent.width - input.extent.width / 5,
        y: input.extent.height - input.extent.height / 10)
    ])?
    .outputImage
  // 4
  let compositeImage = input.applyingFilter(
    "CIBlendWithMask",
    parameters: [
      kCIInputBackgroundImageKey: noir as Any,
      kCIInputMaskImageKey: sunGenerate as Any
    ])
}

Here, you:

  1. Declare a private method that takes a CIImage as input and applies a built-in filter.
  2. You start by creating a darkened, moody noir effect using CIPhotoEffectNoir. CIFilter takes a string as the name and parameters in the form of a dictionary. You fetch the resulting filtered image from outputImage.
  3. Next, you create a generator filter using CISunbeamsGenerator. This creates a sunbeams mask. In the parameters, you set:
    • inputStriationStrength: Represents the intensity of the sunbeams.
    • inputSunRadius: Represents the radius of the sun.
    • inputCenter: The x and y position of the center of the sunbeam. In this case, you set the position to the top right of the image.
  4. Here, you create a stylized effect by using CIBlendWithMask. You apply the filter on the input by setting the result of CIPhotoEffectNoir as the background image and sunGenerate as the mask image. The result of this composition is a CIImage.

ImageProcessor has output, a published property which is a UIImage. You’ll need to convert the result of the composition to a UIImage to display it.

In ImageProcessor, add the following below @Published var output = UIImage():

let context = CIContext()

Here, you create an instance of CIContext that all the filters will use.

Add the following to ImageProcessor:

private func renderAsUIImage(_ image: CIImage) -> UIImage? {
  if let cgImage = context.createCGImage(image, from: image.extent) {
    return UIImage(cgImage: cgImage)
  }
  return nil
}

Here, you use context to create an instance of CGImage from CIImage.

Using cgImage, you then create a UIImage. The user will see this image.