Implementing Image Filters in Swift
Heads up... You’re accessing parts of this content for free, with some sections shown as text.
Heads up... You’re accessing parts of this content for free, with some sections shown as text.
Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Unlock now
In this segment, you’ll implement three image processing algorithms in Swift: grayscale conversion, blur effect, and brightness adjustment. You’ll work directly with raw pixel data, learning how to manipulate individual color channels to create visual effects.
Image processing algorithms operate on pixel arrays, transforming color values according to mathematical formulas. Swift’s type safety and array handling make it well-suited for this kind of algorithmic work.
Understanding the ImageProcessor Structure
Your Starter project already has ImageProcessor.swift with TODO stubs for three filter functions. You’ll implement each filter function to process RGBA pixel data.
Implementing the Grayscale Filter
The grayscale filter converts color images to black and white by calculating a weighted average of the RGB channels.
public static func applyGrayscaleFilter(
imageData: Data,
width: Int32,
height: Int32
) -> Data? {
// 1
guard width > 0, height > 0 else { return nil }
// 2
var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
imageData.copyBytes(to: &pixels, count: pixels.count)
// 3
for i in stride(from: 0, to: pixels.count, by: 4) {
let r = Float(pixels[i])
let g = Float(pixels[i + 1])
let b = Float(pixels[i + 2])
// 4
let gray = UInt8(0.299 * r + 0.587 * g + 0.114 * b)
// 5
pixels[i] = gray
pixels[i + 1] = gray
pixels[i + 2] = gray
// Alpha (i+3) remains unchanged
}
// 6
return Data(pixels)
}
gray = 0.299 * 255 + 0.587 * 255 + 0.114 * 0
gray = 76.245 + 149.685 + 0
gray = 225.93 → 226 (rounded)
Result: [226, 226, 226, 255]
Implementing the Blur Filter
The blur filter creates a soft-focus effect by averaging each pixel with its surrounding neighbors. This is called a box blur.
public static func applyBlurFilter(
imageData: Data,
width: Int32,
height: Int32,
radius: Int32 = 5
) -> Data? {
// 1
guard width > 0, height > 0, radius > 0 else { return nil }
// 2
let safeRadius = min(radius, 10)
var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
imageData.copyBytes(to: &pixels, count: pixels.count)
var output = [UInt8](repeating: 0, count: Int(width * height * 4))
// 3
for i in stride(from: 0, to: pixels.count, by: 4) {
output[i + 3] = pixels[i + 3]
}
// 4
let step = 1
for y in stride(from: 0, to: Int(height), by: step) {
for x in stride(from: 0, to: Int(width), by: step) {
// 5
var r: Float = 0, g: Float = 0, b: Float = 0
var count: Float = 0
// 6
for ky in -Int(safeRadius)...Int(safeRadius) {
for kx in -Int(safeRadius)...Int(safeRadius) {
let px = x + kx
let py = y + ky
// 7
guard px >= 0, px < Int(width), py >= 0, py < Int(height) else { continue }
let index = (py * Int(width) + px) * 4
r += Float(pixels[index])
g += Float(pixels[index + 1])
b += Float(pixels[index + 2])
count += 1
}
}
// 8
let index = (y * Int(width) + x) * 4
output[index] = UInt8(r / count)
output[index + 1] = UInt8(g / count)
output[index + 2] = UInt8(b / count)
}
}
return Data(output)
}
Implementing Brightness Adjustment
The brightness filter adds or subtracts a value from each RGB channel, making the image lighter or darker.
public static func adjustBrightness(
imageData: Data,
width: Int32,
height: Int32,
amount: Float
) -> Data? {
// 1
guard width > 0, height > 0 else { return nil }
var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
imageData.copyBytes(to: &pixels, count: pixels.count)
// 2
for i in stride(from: 0, to: pixels.count, by: 4) {
let r = Float(pixels[i]) + amount
let g = Float(pixels[i + 1]) + amount
let b = Float(pixels[i + 2]) + amount
// 3
pixels[i] = UInt8(max(0, min(255, r)))
pixels[i + 1] = UInt8(max(0, min(255, g)))
pixels[i + 2] = UInt8(max(0, min(255, b)))
// Alpha (i+3) remains unchanged
}
return Data(pixels)
}
// Without clamping
let r = 200.0 + 100.0 // = 300.0 (invalid!)
let pixel = UInt8(r) // Wraps to 44 (incorrect)
// With clamping
let r = max(0, min(255, 200.0 + 100.0)) // = 255.0 (correct)
let pixel = UInt8(r) // = 255 (white, not wrapped)
Implementing the File Processing Wrapper
Now implement the function that ties everything together by reading files, applying filters, and writing results.
public static func processImageFile(
inputPath: String,
outputPath: String,
filterType: String,
amount: Float = 0
) -> Bool {
// 1
let inputURL = URL(fileURLWithPath: inputPath)
guard let fileData = try? Data(contentsOf: inputURL), fileData.count >= 8 else {
return false
}
// 2
let headerBytes = Array(fileData.prefix(8))
let width = Int32((UInt32(headerBytes[0]) << 24) |
(UInt32(headerBytes[1]) << 16) |
(UInt32(headerBytes[2]) << 8) |
UInt32(headerBytes[3]))
let height = Int32((UInt32(headerBytes[4]) << 24) |
(UInt32(headerBytes[5]) << 16) |
(UInt32(headerBytes[6]) << 8) |
UInt32(headerBytes[7]))
guard width > 0, height > 0 else {
return false
}
// 3
let pixelData = fileData.subdata(in: 8..<fileData.count)
// 4
let processedData: Data?
switch filterType.lowercased() {
case "grayscale":
processedData = applyGrayscaleFilter(imageData: pixelData, width: width, height: height)
case "blur":
processedData = applyBlurFilter(imageData: pixelData, width: width, height: height)
case "brighter":
processedData = adjustBrightness(imageData: pixelData, width: width, height: height, amount: 50)
case "darker":
processedData = adjustBrightness(imageData: pixelData, width: width, height: height, amount: -50)
default:
processedData = pixelData
}
guard let finalPixels = processedData else {
return false
}
// 5
let outputURL = URL(fileURLWithPath: outputPath)
do {
var outputData = Data()
// 6
outputData.append(UInt8((width >> 24) & 0xFF))
outputData.append(UInt8((width >> 16) & 0xFF))
outputData.append(UInt8((width >> 8) & 0xFF))
outputData.append(UInt8(width & 0xFF))
outputData.append(UInt8((height >> 24) & 0xFF))
outputData.append(UInt8((height >> 16) & 0xFF))
outputData.append(UInt8((height >> 8) & 0xFF))
outputData.append(UInt8(height & 0xFF))
// 7
outputData.append(finalPixels)
try outputData.write(to: outputURL)
return true
} catch {
return false
}
}
Building and Testing
Build your Swift package to verify everything compiles:
cd taskmanager-lib
swift build
Key Takeaways
In this segment, you’ve: