本教程從上一節結束的地方開始。如果你沒有項目文件,你可以在這裏下載它。
如果你在第一節中表現得很好,你要好好享受這一節!既然你理解了工作原理,你將充分理解這些庫進行圖像處理是多麼的簡單。
超級SpookCam之Core Graphics版本
Core Graphics是Apple基於Quartz 2D繪圖引擎的繪圖API。它提供了底層API,如果你熟悉OpenGL可能會覺得它們很相似。
如果你曾經重寫過視圖的-drawRect:函數,你其實已經與Core Graphics交互過了,它提供了很多繪製對象、斜度和其他很酷的東西到你的視圖中的函數。
這個網站已經有大量的Core Graphics教程,比如這個和這個。所以,這本教程中,我們將關注於如何使用Core Graphics來做一些基本的圖像處理。
在開始之前,我們需要熟悉一個概念Graphics Context。
概念:Graphics Contexts是OpenGl和Core Graphics的核心概念,它是渲染中最常見的類型。它是一個持有所有關於繪製信息的全局狀態對象。
在Core Graphics中,包括了當前的填充顏色,描邊顏色,變形,蒙版,在哪裏繪製等。在iOS中,還有其他不同類型的context比如PDF context,它可以讓你繪製一個PDF文件。
在本教程中,你只會使用到Bitmap context,它可以繪製位圖。
在-drawRect:函數中,你會發現你可以直接調用UIGraphicsGetCurrentContext()來使用context。系統被設置爲你可以直接在視圖上繪製被渲染的圖像。
在-drawRect:函數外,通常沒有圖形context可用。你可以像第一個項目中一樣用CGContextCreate()創建,或者你可以使用UIGraphicsBeginImageContext()和UIGraphicsGetCurrentContext()抓取創建的context。
這叫做離屏-渲染,意思是你不是在任何地方直接繪製,而是在離屏緩衝區渲染。
在Core Graphics中,你可以獲得context中的UIImage然後把它顯示在屏幕上。使用OpenGL,你可以直接把這個緩衝區與當前渲染在屏幕中的交換,然後直接顯示它。
使用Core Graphics處理圖像利用了在緩衝區渲染圖像的離屏渲染,它從context抓取圖像,並適用任何你想要的效果。
好了,概念介紹完了,是時候變一些代碼的魔術了!添加下面的新函數到ImageProcessor.m中:
- (UIImage *)processUsingCoreGraphics:(UIImage*)input {
CGRect imageRect = {CGPointZero,input.size};
NSInteger inputWidth = CGRectGetWidth(imageRect);
NSInteger inputHeight = CGRectGetHeight(imageRect);
// 1) Calculate the location of Ghosty計算圖片的大小,位置,將他縮放25%
UIImage * ghostImage = [UIImage imageNamed:@"ghost.png"];
CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height;
NSInteger targetGhostWidth = inputWidth * 0.25;
CGSize ghostSize = CGSizeMake(targetGhostWidth, targetGhostWidth / ghostImageAspectRatio);
CGPoint ghostOrigin = CGPointMake(inputWidth * 0.5, inputHeight * 0.2);
CGRect ghostRect = {ghostOrigin, ghostSize};
// 2) Draw your image into the context.首先畫出inputImage
UIGraphicsBeginImageContext(input.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputHeight);
CGContextConcatCTM(context, flipThenShift);
CGContextDrawImage(context, imageRect, [input CGImage]);
//然後將ghostImage添加到原圖
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGContextSetAlpha(context,0.5);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
// 3) Retrieve your processed image
UIImage * imageWithGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4) Draw your image into a grayscale context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
context = CGBitmapContextCreate(nil, inputWidth, inputHeight, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone);
CGContextDrawImage(context, imageRect, [imageWithGhost CGImage]);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * finalImage = [UIImage imageWithCGImage:imageRef];
// 5) Cleanup
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
return finalImage;
}
讓我們分析一下當前代碼,
1) 計算Ghosty的位置
UIImage * ghostImage = [UIImage imageNamed:@"ghost.png"];
CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height;
NSInteger targetGhostWidth = inputWidth * 0.25;
CGSize ghostSize = CGSizeMake(targetGhostWidth, targetGhostWidth / ghostImageAspectRatio);
CGPoint ghostOrigin = CGPointMake(inputWidth * 0.5, inputHeight * 0.2);
CGRect ghostRect = {ghostOrigin, ghostSize};
創建一個新的CGContext。
像前面討論的,這裏創建了一個“離屏”(“off-screen”)的context。還記得嗎?CGContext的座標系以左下角爲原點,相反的UIImage使用左上角爲原點。
有趣的是,如果你使用UIGraphicsBeginImageContext()來創建一個context,系統會把座標翻轉,把原點設爲左上角。因此,你需要變換你的context把它翻轉回來,從而使CGImage能夠進行正確的繪製。
如果你直接在這個context中繪製UIImage,你不需要執行變換,座標系統將會自動匹配。設置這個context的變換將影響所有你後面繪製的圖像。
2) 把你的圖像繪製到context中
UIGraphicsBeginImageContext(input.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputHeight);
CGContextConcatCTM(context, flipThenShift);
CGContextDrawImage(context, imageRect, [input CGImage]);
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGContextSetAlpha(context,0.5);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
在繪製完圖像後,你context的alpha值設爲了0.5。這隻會影響後面繪製的圖像,所以本次繪製的輸入圖像使用了全alpha。
你也需要把混合模式設置爲kCGBlendModeSourceAtop。
這裏爲context設置混合模式是爲了讓它使用之前的相同的alpha混合公式。在設置完這些參數之後,翻轉幽靈的座標然後把它繪製在圖像中。
3) 取回你處理的圖像
UIImage * imageWithGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
爲了把你的圖像轉換成黑白的,你將創建一個使用灰度(grayscale)色彩的新的CGContext。它將把所有你在context中繪製的圖像轉換成灰度的。
因爲你使用CGBitmapContextCreate()來創建了這個context,座標則是以左下角爲原點,你不需要翻轉它來繪製CGImage。
4) 繪製你的圖像到一個灰度(grayscale)context中
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
context = CGBitmapContextCreate(nil, inputWidth, inputHeight, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone);
CGContextDrawImage(context, imageRect, [imageWithGhost CGImage]);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * finalImage = [UIImage imageWithCGImage:imageRef]
取回你最終的圖像。
爲什麼你不可以使用UIGraphicsGetImageFromCurrentImageContext()呢,因爲你沒有把當前的圖形context設置爲灰度context。
因此,你需要自己創建它。你需要使用CGBitmapContextCreateImage()來渲染context中的圖像。
5) 清理
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
return finalImage;
內存使用:當執行圖像處理時,密切關注內存使用情況。像在第一節中討論的一樣,一個8M像素的圖像佔用了高達32M的內存。儘量避免在內存中同一時間保持同一圖像的多個複製。
注意到爲什麼我們第二次需要釋放context而第一次不需要了嗎?這是因爲第一次時,你使用UIGraphicsGetCurrentImageContext()獲取了context。這裏的關鍵詞是‘get’。
‘Get’意味着你獲取了當前context的引用,你並不持有它。
在第二次中,你調用了CGBitmapContextCreateImage(),Create意味着你持有這個對象,並需要管理它的生命週期。這也是你爲什麼需要釋放imageRef的原因,因爲你是通過CGBitmapContextCreateImage()創建它的。
在這個簡單的例子中,使用Core Graphics看起來好像不比直接操作像素更簡單。
然而,想象一個更復雜的操作,比如旋轉圖像。在像素操作中,這需要相當複雜的數學。
但是,使用Core Graphics,你只需要在繪製圖像前給context設置一個旋轉的變換就可以了。因爲,你處理的內容越複雜,你使用Core Graphics則能節省更多的時間。
介紹完了兩種方法,下面還有兩種方法。下一個:Core Image!
超超SpookCam之Core Image版本
這個網站也已經有大量好的Core Image教程,比如IOS 6中的這個。我們也在我們的iOS教程系列中有很多關於Core Image的章節。
在本教程中,你將看到有很多關於Core Image與其他幾種方法對比的討論。
Core Image是Apple的圖像處理的解決方案。它避免了所有底層的像素操作方法,轉而使用高級別的濾鏡替代了它們。
Core Image最好的部分在於它對比操作原始像素或Core Graphics有着極好的性能。這個庫使用CPU和GPU混合處理提供接近實時的性能。
Apple還提供了巨大的預先製作的濾鏡庫。在OSX中,你甚至可以使用Core Image Kernel Language創建你自己的濾鏡,它跟OpenGL中的着色語言GLSL很相似。在寫本教程時,你還不能在iOS中製作你自己的Core Image濾鏡(只支持Mac OS X)。
它還有一些比Core Graphics更好的效果。正如你在代碼中看到的,你用Core Graphics來充分利用Core Image。
添加這個新函數到ImageProcessor.m中:
- (UIImage *)processUsingCoreImage:(UIImage*)input { CIImage * inputCIImage = [[CIImage alloc] initWithImage:input]; // 1. Create a grayscale filter CIFilter * grayFilter = [CIFilter filterWithName:@"CIColorControls"]; [grayFilter setValue:@(0) forKeyPath:@"inputSaturation"]; // 2. Create your ghost filter // Use Core Graphics for this UIImage * ghostImage = [self createPaddedGhostImageWithSize:input.size]; CIImage * ghostCIImage = [[CIImage alloc] initWithImage:ghostImage]; // 3. Apply alpha to Ghosty CIFilter * alphaFilter = [CIFilter filterWithName:@"CIColorMatrix"]; CIVector * alphaVector = [CIVector vectorWithX:0 Y:0 Z:0.5 W:0]; [alphaFilter setValue:alphaVector forKeyPath:@"inputAVector"]; // 4. Alpha blend filter CIFilter * blendFilter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; // 5. Apply your filters [alphaFilter setValue:ghostCIImage forKeyPath:@"inputImage"]; ghostCIImage = [alphaFilter outputImage]; [blendFilter setValue:ghostCIImage forKeyPath:@"inputImage"]; [blendFilter setValue:inputCIImage forKeyPath:@"inputBackgroundImage"]; CIImage * blendOutput = [blendFilter outputImage]; [grayFilter setValue:blendOutput forKeyPath:@"inputImage"]; CIImage * outputCIImage = [grayFilter outputImage]; // 6. Render your output image CIContext * context = [CIContext contextWithOptions:nil]; CGImageRef outputCGImage = [context createCGImage:outputCIImage fromRect:[outputCIImage extent]]; UIImage * outputImage = [UIImage imageWithCGImage:outputCGImage]; CGImageRelease(outputCGImage); return outputImage; }
我們看一下這個代碼跟之前的函數有多大區別。
使用Core Image,你設置了大量的濾鏡來處理你的圖像 – 你使用了CIColorControls濾鏡來設置灰度,CIColorMatrix和CISourceAtopCompositing來設置混合,最後把它們連接在一起。
現在,讓我們瀏覽一遍這個函數來學習它的每一個步驟。
- 創建CIColorControls濾鏡,設置它的inputSaturation值爲0。你可能記得,飽和度是HSV顏色空間的一個通道。這裏的0表示了灰度。
- 創建一個和輸入圖像一樣大小的填充的幽靈圖像。
- 創建CIColorMatrix濾鏡,設置它的alphaVector值爲[0 0 0.5 0]。這將給幽靈的alpha值增加0.5。
- 創建CISourceAtopCompositing濾鏡來進行alpha混合。
- 合併你的濾鏡來處理圖像。
- 渲染輸出CIImage到CGImage,創建最終的UIImage。記得在後面釋放你的內存。
這個方法使用了一個叫做-createPaddedGhostImageWithSize:的幫助函數,它使用Core Graphics創建了輸入圖像25%大小縮小版的填充的幽靈。你自己能實現這個函數嗎?
自己試一下。如果你被卡住了,請看下面的解決方案:
- (UIImage *)createPaddedGhostImageWithSize:(CGSize)inputSize {
UIImage * ghostImage = [UIImage imageNamed:@"ghost.png"];
CGFloat ghostImageAspectRatio = ghostImage.size.width / ghostImage.size.height;
NSInteger targetGhostWidth = inputSize.width * 0.25;
CGSize ghostSize = CGSizeMake(targetGhostWidth, targetGhostWidth / ghostImageAspectRatio);
CGPoint ghostOrigin = CGPointMake(inputSize.width * 0.5, inputSize.height * 0.2);
CGRect ghostRect = {ghostOrigin, ghostSize};
UIGraphicsBeginImageContext(inputSize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect inputRect = {CGPointZero, inputSize};
CGContextClearRect(context, inputRect);
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-inputSize.height);
CGContextConcatCTM(context, flipThenShift);
CGRect transformedGhostRect = CGRectApplyAffineTransform(ghostRect, flipThenShift);
CGContextDrawImage(context, transformedGhostRect, [ghostImage CGImage]);
UIImage * paddedGhost = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return paddedGhost;
}
你可以在這裏下載到本節項目的所有代碼。
Core Image提供了大量的濾鏡,你可以使用它們來創建幾乎任何你想要的效果。它是你處理圖像時的好夥伴。
現在到了最後一個解決方案,也是本教程中附帶的唯一的第三方選項:GPUImage。
大型超超SpookCam之GPUImage版本
GPUImage是一個活躍的iOS上基於GPU的圖像處理庫。它在這個網站中的十佳iOS庫中贏得了一席之地!
GPUImage隱藏了在iOS中所有需要使用OpenGL ES的複雜的代碼,並用極其簡單的接口以很快的速度處理圖像。GPUImage的性能甚至在很多時候擊敗了Core Image,但是Core Image仍然在很多函數中有優勢。
在開始學習GPUImage之前,你需要把它包含到你的項目中。這可以使用Cocoapods在項目中生成靜態庫或直接嵌入源碼來完成。
項目應用已經包含一個建立在外部的靜態框架。你可以根據下面的步驟簡單的把它複製到項目中:
你可以通過下面來自Github倉庫的說明把源代碼嵌入你的項目:
說明:
拖拽GPUImage.xcodeproj文件到你Xcode項目中來把框架嵌入到你的項目中。
然後,到應用程序的target添加GPUImage爲一個target依賴。
從GPUImage框架新產品文件夾中拖拽libGPUImage.a庫到你應用程序target中的Link Binary With Librariesbuild phase。
GPUImage需要鏈接一些其他框架到你的應用程序,所以你需要添加如下的相關庫到你的應用程序target:
CoreMedia
CoreVideo
OpenGLES
AVFoundation
QuartzCore
然後你需要找到框架的頭文件。在你項目的build設置中,設置Header Search Paths的相對路徑爲你應用程序中框架/子文件夾中的GPUImage源文件目錄。使Header Search Paths是遞歸的。
添加GPUImage到你的項目中後,一定要在ImageProcessor.m中包含頭文件。
如果你想包含靜態的框架,使用#import GPUImage/GPUImage.h。如果你想直接在項目中包含它,使用#import "GPUImage.h"。
添加新的處理函數到ImageProcessor.m中:
- (UIImage *)processUsingGPUImage:(UIImage*)input {
// 1. Create the GPUImagePictures
GPUImagePicture * inputGPUImage = [[GPUImagePicture alloc] initWithImage:input];
UIImage * ghostImage = [self createPaddedGhostImageWithSize:input.size];
GPUImagePicture * ghostGPUImage = [[GPUImagePicture alloc] initWithImage:ghostImage];
// 2. Set up the filter chain
GPUImageAlphaBlendFilter * alphaBlendFilter = [[GPUImageAlphaBlendFilter alloc] init];
alphaBlendFilter.mix = 0.5;
[inputGPUImage addTarget:alphaBlendFilter atTextureLocation:0];
[ghostGPUImage addTarget:alphaBlendFilter atTextureLocation:1];
GPUImageGrayscaleFilter * grayscaleFilter = [[GPUImageGrayscaleFilter alloc] init];
[alphaBlendFilter addTarget:grayscaleFilter];
// 3. Process & grab output image
[grayscaleFilter useNextFrameForImageCapture];
[inputGPUImage processImage];
[ghostGPUImage processImage];
UIImage * output = [grayscaleFilter imageFromCurrentFramebuffer];
return output;
}
它看來很明確。這是它的具體內容:
- 創建GPUImagePicture對象;再次使用-createPaddedGhostImageWithSize:爲一個工具。這時GPUImage會把圖像紋理上傳到GPU內存。
-
創建和鏈接你將要使用的濾鏡。這種鏈接與Core Image中的濾鏡鏈接不同,它類似於管道。在你完成後,它看起來是這樣的:
GPUImageAlphaBlendFilter接受兩個輸入,在這種情況下爲頂部和底部的圖像,紋理的位置很重要。-addTarget:atTextureLocation: 設置紋理爲正確的輸入(位置)。
-
在鏈中的最後一個濾鏡調用-useNextFrameForImageCapture然後對兩個輸入調用-processImage 。這可以確保濾鏡知道你想要從中抓取圖像然後持有它。
正如你看到的,GPUImage很容易操作。你也可以在GLSL裏製作你自己的着色器並創建你自己的濾鏡。查看這裏的GPUImage文檔來更多的學習如何使用本框架。
在這裏下載本節項目中的所有代碼。
當然,除本教程外還有很多其他有趣的圖像處理概念:
- 內核和卷積。內核與圖像採樣濾鏡協同工作。例如,模糊濾鏡。
- 圖像分析。有時候你需要對圖像進行深入的分析,例如你想進行人臉識別。Core Image爲這個過程提供了CIDetector類。