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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Core Image Tutorial for iOS: Custom Filters
25 mins
- Getting Started
- Introducing Core Image Classes
- Fetching the List of Built-In Filters
- Using Built-In Filters
- Displaying a Built-In Filter’s Output
- Meet CIKernel
- Creating Build Rules
- Adding the Metal Source
- Loading the Kernel Code
- Applying the Color Kernel Filter
- Creating a Warp Kernel
- Loading the Warp Kernel
- Applying the Warp Kernel Filter
- Challenge: Implementing a Blend Kernel
- Debugging Core Image Issues
- Using Core Image Quick Look
- Using CI_PRINT_TREE
- Where to Go From Here?
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.
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 ofCIFilters
are mutable and not thread-safe. -
CIContext: Renders the processed results from the filter. For example,
CIContext
helps create a Quartz 2D image from aCIImage
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:
- 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. - You can inspect the filter’s various attributes using
attributes
. Here, you create aScrollView
and populate the description of the attributes in aText
view if the filter exists. - 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.
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:
- Declare a private method that takes a
CIImage
as input and applies a built-in filter. - 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 fromoutputImage
. - 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.
- Here, you create a stylized effect by using
CIBlendWithMask
. You apply the filter on theinput
by setting the result ofCIPhotoEffectNoir
as the background image andsunGenerate
as the mask image. The result of this composition is aCIImage
.
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.