Slide 1

Slide 1 text

IMAGE PROCESSING FOR IOS Simon Gladman for ProgSCon April 2016

Slide 2

Slide 2 text

MOBILE IMAGE EDITING Image capture & manipulation a primary use case for iOS devices Apps from vendors such as Pixelmator & Adobe Desktop functionality and performance on iOS devices

Slide 3

Slide 3 text

IMAGE PROCESSING FRAMEWORKS Three main frameworks: Core Image Accelerate / vImage Metal High performance & low energy Cross platform - both iOS and OS X

Slide 4

Slide 4 text

CORE IMAGE Image processing framework Introduced to iOS in 2011 with 16 built-in filters Over 170 built-in filters Support to write custom filter kernels Feature recognition for faces, rectangles, text, QR codes Near parity with OS X

Slide 5

Slide 5 text

ACCELERATE / VIMAGE Accelerate is Apple’s vector processing framework for iOS SIMD: Single Instruction Multiple Data Includes support for digital signal processing, math and linear algebra and image processing (vImage) vImage introduced for iOS in 2011 Updated in 2013 with better Core Graphics interoperability Core Video support added in 2014

Slide 6

Slide 6 text

METAL Apple’s framework for lowest overhead GPU access Introduced in 2014 Compute kernels offer data-parallel computation C++ style language Precompiled shaders Metal Performance Shaders released in 2015 (iOS only)

Slide 7

Slide 7 text

COMPANION PROJECT https://github.com/FlexMonkey/ ProgSConCompanion

Slide 8

Slide 8 text

CORE IMAGE EXPLORED

Slide 9

Slide 9 text

CORE IMAGE KERNEL A CIKernel contains the image processing algorithm that’s executed once for every pixel in a destination image

Slide 10

Slide 10 text

CORE IMAGE FILTER A CIFilter wraps up one or more kernels into lightweight, mutable object that generates an output image Most accept a range of parameters Input image Numeric parameters

Slide 11

Slide 11 text

CORE IMAGE IMAGE A CIImage contains the “recipe” to create a final image from one or more Core Image filters It’s only when a CIImage is converted to a renderable format that the filters are actually executed Core Image filters use CIImage instances as inputs and outputs

Slide 12

Slide 12 text

CORE IMAGE CONTEXT A CIContext is the fundamental class for rendering Core Image generated content Represents the drawing destination - either CPU or GPU Expensive to instantiate

Slide 13

Slide 13 text

FILTER CATEGORIES 173 filters in 21 filter categories Distortion Effect Geometry Adjustment Composite Operation Halftone Effect Color Adjustment Color Effect Transition Tile Effect Generator Reduction Gradient Stylize Sharpen Blur Video Still Image Interlaced Non Square Pixels

Slide 14

Slide 14 text

BLUR For example Gaussian Blur Box & Tent Zoom & Motion Blur

Slide 15

Slide 15 text

COLOR ADJUSTMENT For example Hue adjustment Gamma & Exposure Tone Curves Color Temperature

Slide 16

Slide 16 text

COLOR EFFECTS For example False Color Photo Effects Posterisation Color Cube & Polynomial

Slide 17

Slide 17 text

DISTORTION For example Droste Pinch & Bump Torus Lens Twirl & Vortex

Slide 18

Slide 18 text

GENERATOR For example Starshine & Lenticular Halo Stripes & Checkerboards Solid Color Random Noise

Slide 19

Slide 19 text

STYLIZE For example Convolution Depth of Field Pixellate & Crystalize

Slide 20

Slide 20 text

FILTERING IN PRACTICE Convert a UIImage to a CIImage let bruges = UIImage( named: “bruges.jpg")! let image = CIImage( image: bruges)!

Slide 21

Slide 21 text

FILTERING IN PRACTICE let noise = CIFilter(name: "CIRandomGenerator")?.outputImage? .imageByCroppingToRect(image.extent) let filteredImage = image .imageByApplyingFilter( "CIVignette", withInputParameters: [kCIInputIntensityKey: 4]) .imageByApplyingFilter( "CIDarkenBlendMode", withInputParameters: [kCIInputBackgroundImageKey: noise!]) .imageByApplyingFilter( "CIColorControls", withInputParameters: [kCIInputSaturationKey: 0.25, kCIInputContrastKey: 1.15]) .imageByApplyingFilter( "CISepiaTone", withInputParameters: nil)

Slide 22

Slide 22 text

FILTERING IN PRACTICE Render output to CGImage and display with UIImageView let context = CIContext() let finalImage = context.createCGImage( filteredImage, fromRect: filteredImage.extent) imageView.image = UIImage( CGImage: finalImage)

Slide 23

Slide 23 text

KERNEL CONCATENATION Noise Vignette Darken Blend Sepia Color Controls

Slide 24

Slide 24 text

KERNEL CONCATENATION Noise Vignette Darken Blend Sepia Single Kernel

Slide 25

Slide 25 text

CUSTOM CORE IMAGE KERNELS Three types of kernel General Color - can only change color information Warp - can only change where the destination pixel is sampled from Written in Core Image Kernel Language A dialect of GLSL

Slide 26

Slide 26 text

COLOR KERNEL let shadedTileKernel = CIColorKernel( string: "kernel vec4 shadedTile(__sample pixel)" + "{" + " vec2 coord = samplerCoord(pixel);" + " float brightness = mod(coord.y, 80.0) / 80.0;" + " brightness *= 1.0 - mod(coord.x, 80.0) / 80.0;" + " return vec4(sqrt(brightness) * pixel.rgb, pixel.a); " + "}")

Slide 27

Slide 27 text

COLOR KERNEL Execute the kernel to create a CIImage let final = shadedTileKernel.applyWithExtent( extent, arguments: arguments)

Slide 28

Slide 28 text

WARP KERNEL let carnivalMirrorKernel = CIWarpKernel(string: "kernel vec2 carnivalMirror(float xWavelength, float xAmount," + "float yWavelength, float yAmount)" + "{" + " float y = destCoord().y + sin(destCoord().y / yWavelength) * yAmount; " + " float x = destCoord().x + sin(destCoord().x / xWavelength) * xAmount; " + " return vec2(x, y); " + "}")

Slide 29

Slide 29 text

WARP KERNEL Execute the kernel to create a CIImage let final = kernel.applyWithExtent( extent, roiCallback: { (index, rect) in return rect }, inputImage: inputImage, arguments: arguments)

Slide 30

Slide 30 text

GENERAL KERNEL let maskedVariableBlur = CIKernel(string: "kernel vec4 lumaVariableBlur(sampler image, sampler blurImage, float blurRadius) " + "{ " + " vec2 d = destCoord(); " + " vec3 blurPixel = sample(blurImage, samplerCoord(blurImage)).rgb; " + " float blurAmount = dot(blurPixel, vec3(0.2126, 0.7152, 0.0722)); " + " float n = 0.0; " + " int radius = int(blurAmount * blurRadius); " + " vec3 accumulator = vec3(0.0, 0.0, 0.0); " + " for (int x = -radius; x <= radius; x++) " + " { " + " for (int y = -radius; y <= radius; y++) " + " { " + " vec2 workingSpaceCoordinate = d + vec2(x,y); " + " vec2 imageSpaceCoordinate = samplerTransform(image, workingSpaceCoordinate); " + " vec3 color = sample(image, imageSpaceCoordinate).rgb; " + " accumulator += color; " + " n += 1.0; " + " } " + " } " + " accumulator /= n; " + " return vec4(accumulator, 1.0); " + "} " )

Slide 31

Slide 31 text

GENERAL KERNEL • applyWithExtent() has same signature as a warp kernel

Slide 32

Slide 32 text

CUSTOM KERNELS

Slide 33

Slide 33 text

CUSTOM KERNELS

Slide 34

Slide 34 text

CUSTOM KERNELS

Slide 35

Slide 35 text

CUSTOM KERNELS

Slide 36

Slide 36 text

VIMAGE EXPLORED

Slide 37

Slide 37 text

VIMAGE BUFFERS Converting a UIImage to a vImage Buffer var format = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.Last.rawValue), version: 0, decode: nil, renderingIntent: .RenderingIntentDefault) let cgImage = UIImage(named: "landscape.jpg")!.CGImage! var inBuffer = vImage_Buffer() vImageBuffer_InitWithCGImage( &inBuffer, &format, nil, cgImage, UInt32(kvImageNoFlags))

Slide 38

Slide 38 text

VIMAGE BUFFERS Converting a vImage buffer to a UIImage extension UIImage { convenience init?(fromvImageOutBuffer outBuffer:vImage_Buffer) { var mutableBuffer = outBuffer var error = vImage_Error() let cgImage = vImageCreateCGImageFromBuffer( &mutableBuffer, &format, nil, nil, UInt32(kvImageNoFlags), &error) self.init(CGImage: cgImage.takeRetainedValue()) } }

Slide 39

Slide 39 text

VIMAGE HISTOGRAM Histograms contain the frequency of color values in an image Equalisation: increase global contrast by making an image’s histogram uniform Calculation: returns the histogram of an image. Specification: apply a histogram (e.g. from a source image) to a target image Contrast Stretch: Stretches the contrast range of a source image to the full range of supported values (i.e. normalises).

Slide 40

Slide 40 text

VIMAGE EQUALIZATION let pixelBuffer = malloc( CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef)) var outBuffer = vImage_Buffer( data: pixelBuffer, height: UInt(CGImageGetHeight(imageRef)), width: UInt(CGImageGetWidth(imageRef)), rowBytes: CGImageGetBytesPerRow(imageRef)) vImageEqualization_ARGB8888( &inBuffer, &outBuffer, UInt32(kvImageNoFlags)) let outImage = UIImage(fromvImageOutBuffer: outBuffer) free(pixelBuffer)

Slide 41

Slide 41 text

VIMAGE EQUALIZATION

Slide 42

Slide 42 text

VIMAGE SPECIFICATION Get histogram of source image func histogramCalculation(imageRef: CGImage) -> (alpha: [UInt], red: [UInt], green: [UInt], blue: [UInt]) { var inBuffer = vImage_Buffer() vImageBuffer_InitWithCGImage( &inBuffer, &format, nil, imageRef, UInt32(kvImageNoFlags)) let alpha = [UInt](count: 256, repeatedValue: 0) let red = [UInt](count: 256, repeatedValue: 0) let green = [UInt](count: 256, repeatedValue: 0) let blue = [UInt](count: 256, repeatedValue: 0) let alphaPtr = UnsafeMutablePointer(alpha) let redPtr = UnsafeMutablePointer(red) let greenPtr = UnsafeMutablePointer(green) let bluePtr = UnsafeMutablePointer(blue) let rgba = [alphaPtr, redPtr, greenPtr, bluePtr] let histogram = UnsafeMutablePointer>(rgba) vImageHistogramCalculation_ARGB8888(&inBuffer, histogram, UInt32(kvImageNoFlags)) return (alpha, red, green, blue) }

Slide 43

Slide 43 text

VIMAGE SPECIFICATION Get histogram of source image func histogramCalculation(imageRef: CGImage) -> (alpha: [UInt], red: [UInt], green: [UInt], blue: [UInt]) { var inBuffer = vImage_Buffer() vImageBuffer_InitWithCGImage( &inBuffer, &format, nil, imageRef, UInt32(kvImageNoFlags)) let alpha = [UInt](count: 256, repeatedValue: 0) let red = [UInt](count: 256, repeatedValue: 0) let green = [UInt](count: 256, repeatedValue: 0) let blue = [UInt](count: 256, repeatedValue: 0) let alphaPtr = UnsafeMutablePointer(alpha) let redPtr = UnsafeMutablePointer(red) let greenPtr = UnsafeMutablePointer(green) let bluePtr = UnsafeMutablePointer(blue) let rgba = [alphaPtr, redPtr, greenPtr, bluePtr] let histogram = UnsafeMutablePointer>(rgba) vImageHistogramCalculation_ARGB8888(&inBuffer, histogram, UInt32(kvImageNoFlags)) return (alpha, red, green, blue) }

Slide 44

Slide 44 text

VIMAGE SPECIFICATION Apply histogram to target image func histogramSpecification(imageRef: CGImage, histogram: (alpha: [UInt], red: [UInt], green: [UInt], blue: [UInt])) -> UIImage { var inBuffer = vImage_Buffer() vImageBuffer_InitWithCGImage( &inBuffer, &format, nil, imageRef, UInt32(kvImageNoFlags)) let pixelBuffer = malloc(CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef)) var outBuffer = vImage_Buffer( data: pixelBuffer, height: UInt(CGImageGetHeight(imageRef)), width: UInt(CGImageGetWidth(imageRef)), rowBytes: CGImageGetBytesPerRow(imageRef)) let alphaPtr = UnsafePointer(histogram.alpha) let redPtr = UnsafePointer(histogram.red) let greenPtr = UnsafePointer(histogram.green) let bluePtr = UnsafePointer(histogram.blue) let rgba = UnsafeMutablePointer>([alphaPtr, redPtr, greenPtr, bluePtr]) vImageHistogramSpecification_ARGB8888(&inBuffer, &outBuffer, rgba, UInt32(kvImageNoFlags)) let outImage = UIImage(fromvImageOutBuffer: outBuffer) free(pixelBuffer) return outImage! }

Slide 45

Slide 45 text

VIMAGE SPECIFICATION Apply histogram to target image func histogramSpecification(imageRef: CGImage, histogram: (alpha: [UInt], red: [UInt], green: [UInt], blue: [UInt])) -> UIImage { var inBuffer = vImage_Buffer() vImageBuffer_InitWithCGImage( &inBuffer, &format, nil, imageRef, UInt32(kvImageNoFlags)) let pixelBuffer = malloc(CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef)) var outBuffer = vImage_Buffer( data: pixelBuffer, height: UInt(CGImageGetHeight(imageRef)), width: UInt(CGImageGetWidth(imageRef)), rowBytes: CGImageGetBytesPerRow(imageRef)) let alphaPtr = UnsafePointer(histogram.alpha) let redPtr = UnsafePointer(histogram.red) let greenPtr = UnsafePointer(histogram.green) let bluePtr = UnsafePointer(histogram.blue) let rgba = UnsafeMutablePointer>([alphaPtr, redPtr, greenPtr, bluePtr]) vImageHistogramSpecification_ARGB8888(&inBuffer, &outBuffer, rgba, UInt32(kvImageNoFlags)) let outImage = UIImage(fromvImageOutBuffer: outBuffer) free(pixelBuffer) return outImage! }

Slide 46

Slide 46 text

let monalisa = UIImage(named: "monalisa.jpg")! let bluesky = UIImage(named: "bluesky.jpg")! let histogram = histogramCalculation(bluesky.CGImage!) let colored = histogramSpecification(monalisa.CGImage!, histogram: histogram) VIMAGE SPECIFICATION

Slide 47

Slide 47 text

VIMAGE CONTRAST STRETCH vImageContrastStretch_ARGB8888( &inBuffer, &outBuffer, UInt32(kvImageNoFlags))

Slide 48

Slide 48 text

VIMAGE CONTRAST STRETCH

Slide 49

Slide 49 text

VIMAGE CONVOLUTION Convolution Different kernel for each color channel Deconvolution (Richardson-Lucy) More edging options than Core Image Supports larger kernels than Core Image

Slide 50

Slide 50 text

DECONVOLUTION vImageRichardsonLucyDeConvolve_ARGB8888( &inBuffer, &outBuffer, nil, 0, 0, kernel, nil, kernelSide, kernelSide, 0, 0, divisor, 0, [0,0,0,0], iterationCount, UInt32(kvImageNoFlags))

Slide 51

Slide 51 text

DECONVOLUTION Source image blurred

Slide 52

Slide 52 text

DECONVOLUTION Destination image deconvolved using Richardson-Lucy

Slide 53

Slide 53 text

VIMAGE MORPHOLOGY Dilate & Erode With custom kernel Max & Min Fast versions of dilate & erode with rectangular kernels

Slide 54

Slide 54 text

VIMAGE DILATION let kernel: [UInt8] = [ 255, 255, 255, 255, 255, 255, 000, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 000, 255, 255, 255, 255, 255, 255, 255, 255, 000, 255, 255, 255, 000, 255, 255, 255, 000, 255, 255, 255, 255, 255, 000, 255, 255, 000, 255, 255, 000, 255, 255, 255, 255, 255, 255, 255, 000, 255, 000, 255, 000, 255, 255, 255, 255, 255, 255, 255, 255, 255, 000, 000, 000, 255, 255, 255, 255, 255, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 255, 255, 255, 255, 255, 000, 000, 000, 255, 255, 255, 255, 255, 255, 255, 255, 255, 000, 255, 000, 255, 000, 255, 255, 255, 255, 255, 255, 255, 000, 255, 255, 000, 255, 255, 000, 255, 255, 255, 255, 255, 000, 255, 255, 255, 000, 255, 255, 255, 000, 255, 255, 255, 255, 255, 255, 255, 255, 000, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 000, 255, 255, 255, 255, 255, 255] vImageDilate_ARGB8888( &inBuffer, &outBuffer, 0, 0, kernel, UInt(kernelSide), UInt(kernelSide), UInt32(kvImageNoFlags))

Slide 55

Slide 55 text

DILATION Original Image

Slide 56

Slide 56 text

DILATION Dilation applied to thresholded version and composited over original

Slide 57

Slide 57 text

VIMAGE CONTINUED Conversions - lots! Floating point to integer color with dithering YUV to RGB Interleaved (RGB, RGB, RGB) to planar (RRR, GGG, BBB)

Slide 58

Slide 58 text

VIMAGE CONTINUED Geometry - high quality transforms Scale Rotate Shear

Slide 59

Slide 59 text

VIMAGE CONTINUED Transform Gamma Hue, saturation, brightness Color matrix Color polynomial

Slide 60

Slide 60 text

METAL EXPLORED

Slide 61

Slide 61 text

METAL PERFORMANCE SHADERS Framework of data-parallel image processing algorithms for GPU Similar set of functionality to vImage Histogram functions including equalisation & specification Convolution, Gaussian blur Morphology: min, max, erode and dilate Resampling: scale and transform Thresholding & Integral

Slide 62

Slide 62 text

MPS GAUSSIAN BLUR Different implementations of Gaussian blur optimised for Image size Blur radius Blur sigma

Slide 63

Slide 63 text

MPS GAUSSIAN BLUR Create a Metal texture from a UIImage let sourceImage = UIImage(named: “telescope.jpg")! let imageTexture: MTLTexture = { let textureLoader = MTKTextureLoader(device: MTLCreateSystemDefaultDevice()!) let imageTexture:MTLTexture do { imageTexture = try textureLoader.newTextureWithCGImage( sourceImage.CGImage!, options: nil) } catch { fatalError("unable to create texture from image") } return imageTexture }()

Slide 64

Slide 64 text

MPS GAUSSIAN BLUR Textures based on UIImage are upside-down let intermediateTextureDesciptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat( MTLPixelFormat.RGBA8Unorm, width: imageTexture.width, height: imageTexture.height, mipmapped: false) let intermediateTexture = device.newTextureWithDescriptor(intermediateTextureDesciptor) let blur = MPSImageGaussianBlur( device: device, sigma: abs(sin(value)) * 200) let scale = MPSImageLanczosScale(device: device) var tx = MPSScaleTransform( scaleX: 1, scaleY: -1, translateX: 0, translateY: Double(-imageTexture.height)) withUnsafePointer(&tx) { scale.scaleTransform = $0 }

Slide 65

Slide 65 text

MPS GAUSSIAN BLUR Encode both shaders to command buffer let imageView = MTKView() let commandQueue = device.newCommandQueue() let commandBuffer = commandQueue.commandBuffer() scale.encodeToCommandBuffer( commandBuffer, sourceTexture: imageTexture, destinationTexture: intermediateTexture) blur.encodeToCommandBuffer( commandBuffer, sourceTexture: intermediateTexture, destinationTexture: currentDrawable.texture) commandBuffer.presentDrawable(imageView.currentDrawable!) commandBuffer.commit();

Slide 66

Slide 66 text

MPS GAUSSIAN BLUR

Slide 67

Slide 67 text

METAL COMPUTE SHADERS Lowest level - developer is responsible for setting up Metal classes: Device - interface to GPU Library - repository of functions Command Queue - queues & submits commands Function - a Metal shader Pipeline State - compiles function Command Buffer - stores encoded commands Command Encoder - encodes resources to byte code

Slide 68

Slide 68 text

METAL COMPUTE SHADERS Advantages over a Core Image kernel Improved tooling Support for arrays Write to multiple pixels Write to multiple targets Disadvantages A lot more code! CIKL is automatically translated on the fly to Metal shader language

Slide 69

Slide 69 text

METAL COMPUTE SHADERS kernel void pixellate( texture2d inTexture [[texture(0)]], texture2d outTexture [[texture(1)]], constant float &pixelWidth [[ buffer(0) ]], constant float &pixelHeight [[ buffer(1) ]], uint2 gid [[thread_position_in_grid]]) { uint width = uint(pixelWidth); uint height = uint(pixelHeight); const uint2 pixellatedGid = uint2( (gid.x / width) * width, (gid.y / height) * height); const float4 colorAtPixel = inTexture.read(pixellatedGid); outTexture.write(colorAtPixel, gid); }

Slide 70

Slide 70 text

METAL PIXELLATE FILTER

Slide 71

Slide 71 text

RESOURCES Twitter: @FlexMonkey Core Image for Swift on Apple’s iBooks Store & Gumroad https://github.com/FlexMonkey/ Filterpedia https://github.com/FlexMonkey/ ProgSConCompanion http://flexmonkey.blogspot.co.uk