iOS圖像處理(核心圖形,核心圖像,GPUImage)

本教程從上一節結束的地方開始。如果你沒有項目文件,你可以在這裏下載它。

如果你在第一節中表現得很好,你要好好享受這一節!既然你理解了工作原理,你將充分理解這些庫進行圖像處理是多麼的簡單。

超級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濾鏡來設置灰度,CIColorMatrixCISourceAtopCompositing來設置混合,最後把它們連接在一起。

現在,讓我們瀏覽一遍這個函數來學習它的每一個步驟。

  1. 創建CIColorControls濾鏡,設置它的inputSaturation值爲0。你可能記得,飽和度是HSV顏色空間的一個通道。這裏的0表示了灰度。
  2. 創建一個和輸入圖像一樣大小的填充的幽靈圖像。
  3. 創建CIColorMatrix濾鏡,設置它的alphaVector值爲[0 0 0.5 0]。這將給幽靈的alpha值增加0.5
  4. 創建CISourceAtopCompositing濾鏡來進行alpha混合。
  5. 合併你的濾鏡來處理圖像。
  6. 渲染輸出CIImageCGImage,創建最終的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;
}

它看來很明確。這是它的具體內容:
  1. 創建GPUImagePicture對象;再次使用-createPaddedGhostImageWithSize:爲一個工具。這時GPUImage會把圖像紋理上傳到GPU內存。
  2. 創建和鏈接你將要使用的濾鏡。這種鏈接與Core Image中的濾鏡鏈接不同,它類似於管道。在你完成後,它看起來是這樣的:

    GPUImageAlphaBlendFilter接受兩個輸入,在這種情況下爲頂部和底部的圖像,紋理的位置很重要。-addTarget:atTextureLocation: 設置紋理爲正確的輸入(位置)。

  3. 在鏈中的最後一個濾鏡調用-useNextFrameForImageCapture然後對兩個輸入調用-processImage 。這可以確保濾鏡知道你想要從中抓取圖像然後持有它。

正如你看到的,GPUImage很容易操作。你也可以在GLSL裏製作你自己的着色器並創建你自己的濾鏡。查看這裏的GPUImage文檔來更多的學習如何使用本框架。

這裏下載本節項目中的所有代碼。





當然,除本教程外還有很多其他有趣的圖像處理概念:

  • 內核和卷積。內核與圖像採樣濾鏡協同工作。例如,模糊濾鏡。
  • 圖像分析。有時候你需要對圖像進行深入的分析,例如你想進行人臉識別。Core Image爲這個過程提供了CIDetector類。











發佈了25 篇原創文章 · 獲贊 10 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章