What is a CIImage extent, why is a generator infinite, and why do blurred edges look wrong or the image grow?
Predict first
What are the extents and pixels?
import Foundation
import CoreImage
let context = CIContext()
func pixel(_ image: CIImage, x: Int, y: Int) -> [Int] {
var rgba = [UInt8](repeating: 0, count: 4)
context.render(image, toBitmap: &rgba, rowBytes: 4, bounds: CGRect(x: x, y: y, width: 1, height: 1),
format: .RGBA8, colorSpace: CGColorSpace(name: CGColorSpace.sRGB))
return rgba.map(Int.init)
}
let red = CIImage(color: CIColor(red: 1, green: 0, blue: 0))
print("a generator's extent is infinite:", red.extent.isInfinite)
let card = red.cropped(to: CGRect(x: 0, y: 0, width: 100, height: 50))
print("cropped:", card.extent)
print("blurred (sigma 10):", card.applyingGaussianBlur(sigma: 10).extent)
let clamped = card.clampedToExtent().applyingGaussianBlur(sigma: 10)
print("clamped then blurred infinite:", clamped.extent.isInfinite, "| cropped back:", clamped.cropped(to: card.extent).extent)
// Core Image's origin is the bottom-left: y grows upward.
let white = CIImage(color: .white).cropped(to: CGRect(x: 0, y: 5, width: 10, height: 5))
let canvas = CIImage(color: .black).cropped(to: CGRect(x: 0, y: 0, width: 10, height: 10))
let composed = white.composited(over: canvas)
print("white band at y 5..<10: pixel at y=0 is", pixel(composed, x: 0, y: 0), "and at y=9 is", pixel(composed, x: 0, y: 9))
- generator infinite, blur grows to (-30, -30, 160, 110), clamped is infinite until cropped, white at y=9
- the blur keeps (0, 0, 100, 50)
- white band appears at y=0
Mental model. A sheet of tracing paper with a drawn region: a blur smudges ink beyond the lines, so the smudged sheet is larger, and clamping is first extending the border pattern outward.
A CIImage is a recipe with an extent, the rectangle where it can be non-transparent, in Core Image’s coordinate space whose origin is the bottom-left (the check’s white band at y 5…9 is at the top of the output). Generators such as CIImage(color:) have an infinite extent, so you must cropped(to:) before rendering or sizing anything. Filters change extents: a Gaussian blur of sigma 10 turned a 100×50 image at the origin into a rectangle from (−30, −30) of size 160×110, because blurring spreads colour into transparent surroundings, so the result is bigger than the source and its edges fade to transparent.
The standard fix is clampedToExtent() before the blur (edge pixels repeat to infinity, so the blur has data to average), then cropped(to:) back to the original rectangle; the check confirms the clamped image is infinite until cropped. Render only a finite rectangle (CIContext.render(_:toBitmap:…bounds:…) takes explicit bounds), convert to CGImage or a Metal texture via a context you reuse (Q397), and flip y when mixing with UIKit’s top-left origin. Not run: GPU timing, since nothing here measures speed.
Extents grow with filters, generators are infinite, and y runs upward.
Common traps
- Rendering an infinite extent. Crop first.
- Assuming a blur keeps the size. The extent grows by about three sigma each side.
They might follow up with
- Why is y flipped? Core Image uses a mathematical origin at bottom-left, like Core Graphics.
Related questions
- 18.8.2 Which colour space does Core Image blend in by default, and how does that change a simple alpha blend?
- 18.5.2 How do Core Image and Metal relate, why is Core Image lazy, and how do you build a smooth image-processing…
- 18.3.2 What are CVPixelBuffer, CMSampleBuffer and IOSurface, and how do you process frames without copying them?
Which colour space does Core Image blend in by default, and how does… →