本文將為大家介紹常見的IOS圖像處理操作包括以下四部分:旋轉,縮放,裁剪以及像素和UIImage之間的轉化,主要使用的知識是quartz2D。Quartz2D是CoreGraphics框架中的一個重要組成部分,可以完成幾乎所有的2D圖像繪制,處理功能。跟window編程中GDI的功能一樣,而且很多概念都差不多。
一、圖像旋轉
圖像旋轉是圖像處理過程中一中常見操作,按照旋轉的角度不同,可以分為以下兩種:
1、特殊角度旋轉
特殊角度旋轉是指對圖像做90°,180°,270°等這一類旋轉,這一類旋轉操作通常是最頻繁的,如看照片時偶爾會碰到一些方向有問題,我們只需要進行簡單的左轉90°,右轉90°就可以裝好。關於特殊角度旋轉的處理我的上一篇博客《IOS:聊一聊UIImage幾點知識》有介紹過創建圖像時指定imageOrientation來完成,有興趣可以去看看。這種方法由於沒有牽扯到具體的繪制操作,因此速度很快,在IOS和Mac系統中都可以正確顯示,但是如果將圖片倒到windows系統中,方向可能依然是錯的,具體原因上一篇文章也解釋過了。
2、任意角度旋轉
任意角度旋轉顧名思義即對圖像做任意角度的旋轉,可能是30°也可能是35°等等。很顯然這一種旋轉是沒法通過imageOrientaion來完成的,因此我們得想點兒別的辦法。我們知道UIView有一個transform屬性,通過設置transform可以實現偏移,縮放,旋轉的效果。在quartz2D中我們也同樣可以通過對context設置不同的transform來完成相應的功能,下面我們要介紹的任意角度旋轉的方法就是基於對context的一系列操作來完成的。
這塊兒你可能有個疑問,問什么讓UIView旋轉只需要設置一個旋轉的transform就可以了,而context則需要通過“一系列”的transform操作才能完成相應的功能?
原因是UIView中我們通過transform進行的所有操作都是基於view的中心點的,而context中我們進行的操作是基於context的坐標原點。下面我們首先看一下UIView進行旋轉時的圖示:
由於旋轉時繞着中心點轉動,所以我們只需要一步就可以從原位置(黑色表示)轉到目標位置(藍色表示),其中黑色虛線和藍色虛線之間的夾角就是轉過的角度。我們想一下如果轉動時繞着左上角的原點轉動,完成同樣角度轉動后會是怎么一種情況呢?請看下圖
如上圖所示,由於旋轉是繞着原點進行的,雖然我們轉過了相同的角度,但是得到的結果卻相差甚遠。因此context中如果想把一幅圖片旋轉任意角度的話,至少得進行兩步:旋轉和平移。
第一步旋轉很好做,問題是第二部如何從旋轉過后圖片的中心移動到原圖中心,這個計算還不是那么直觀。於是我們想着去模擬UIView的旋轉,我們分如下三步走:
我們設圖片的寬度為width,高度為height,旋轉的三個步驟依次如上圖所示:
a、將context進行平移,將原點移動到原圖的中心位置,x,y方向的平移距離分別為width / 2,height / 2。
b、對context進行旋轉操作。
c、將旋轉后的圖像的中心點重新移回原圖的中心點,即x,y方向的平移距離分別是-width / 2,-height / 2。
進過這三步我們就可以很方便的實現圖片的任意角度旋轉了。你可能會發現步驟a中向下移動了半個圖片寬高,步驟c中又向相反方向移動了半個圖片寬高。這兩個操作不會抵消嗎?答案是NO,步驟a中我們的移動是基於原坐標系統進行移動的,到了步驟c時我們的移動是基於這個時候的坐標系移動的,兩個坐標系是不一樣的,所以才能通過一來一回完成對圖片的旋轉。
圖片旋轉的代碼如下:
// // UIImage+Rotate_Flip.m // SvImageEdit // // Created by maple on 5/14/13. // Copyright (c) 2013 smileEvday. All rights reserved. // #import "UIImage+Rotate_Flip.h" /* * @brief rotate image with radian */ - (UIImage*)rotateImageWithRadian:(CGFloat)radian cropMode:(SvCropMode)cropMode { CGSize imgSize = CGSizeMake(self.size.width * self.scale, self.size.height * self.scale); CGSize outputSize = imgSize; if (cropMode == enSvCropExpand) { CGRect rect = CGRectMake(0, 0, imgSize.width, imgSize.height); rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeRotation(radian)); outputSize = CGSizeMake(CGRectGetWidth(rect), CGRectGetHeight(rect)); } UIGraphicsBeginImageContext(outputSize); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, outputSize.width / 2, outputSize.height / 2); CGContextRotateCTM(context, radian); CGContextTranslateCTM(context, -imgSize.width / 2, -imgSize.height / 2); [self drawInRect:CGRectMake(0, 0, imgSize.width, imgSize.height)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
其中的CropMode定義如下:
enum { enSvCropClip, // the image size will be equal to orignal image, some part of image may be cliped enSvCropExpand, // the image size will expand to contain the whole image, remain area will be transparent }; typedef NSInteger SvCropMode;
clip模式下,旋轉后的圖片和原圖一樣大,部分圖片區域會被裁剪掉;expand模式下,旋轉后的圖片可能會比原圖大,所有的圖片信息都會保留,剩下的區域會是全透明的。
小結:第一部分講述了兩種圖片旋轉的方法,第一種方法處理速度塊,但是只能處理特殊角度旋轉。第二種方法處理速度比第一種要慢,因為牽扯到了實際的繪制和重新采樣生成圖片的過程。在實際操作中如果第一種方法滿足需求,應該盡量使用第一種方法完成圖片旋轉。
二、圖像縮放
圖像縮放顧名思義即對圖片的尺寸進行縮放,由於尺寸不同所以在生成新圖的過程中像素不可能是一一對應,因此會有插值操作。所謂插值即根據原圖和目標圖大小比例,結合原圖像素信息生成的新的像素的過程。常見的插值算法有線性插值,雙線性插值,立方卷積插值等。網上有很多現成的算法,感興趣的話可以去看看。
下面我們看看圖像縮放的原理圖示:
上圖中,我們假設黑色代表原圖尺寸,藍色代表縮放后的尺寸。我們將圖片放大兩倍,那么原圖中的每一個像素將會對應縮放后圖片中的四個像素。如何從一個像素生成四個像素,這個就是插值算法要解決的問題。
今天我們主要討論IOS圖像處理,使用quartz2D幫助我們完成圖像縮放,只需要通過CGContextSetInterpolationQuality函數即可完成插值質量的設置。之於底層具體使用哪種插值算法,我們無從得知,也不需要去關心。使用quartz2D解決圖像縮放的時候,所有我們需要做的事情只有生成一個目標大小的畫布,然后設置插值質量,再使用UIImage的draw方法將圖片繪制到畫布中即可。
下面看代碼:

// // UIImage+Zoom.h // SvImageEdit // // Created by maple on 5/22/13. // Copyright (c) 2013 maple. All rights reserved. // #import <UIKit/UIKit.h> enum { enSvResizeScale, // image scaled to fill enSvResizeAspectFit, // image scaled to fit with fixed aspect. remainder is transparent enSvResizeAspectFill, // image scaled to fill with fixed aspect. some portion of content may be cliped }; typedef NSInteger SvResizeMode; @interface UIImage (Zoom) /* * @brief resizeImage * @param newsize the dimensions(pixel) of the output image */ - (UIImage*)resizeImageToSize:(CGSize)newSize resizeMode:(SvResizeMode)resizeMode; @end

// // UIImage+Zoom.m // SvImageEdit // // Created by maple on 5/22/13. // Copyright (c) 2013 maple. All rights reserved. // #import "UIImage+Zoom.h" @implementation UIImage (Zoom) /* * @brief resizeImage * @param newsize the dimensions(pixel) of the output image */ - (UIImage*)resizeImageToSize:(CGSize)newSize resizeMode:(SvResizeMode)resizeMode { CGRect drawRect = [self caculateDrawRect:newSize resizeMode:resizeMode]; UIGraphicsBeginImageContext(newSize); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, CGRectMake(0, 0, newSize.width, newSize.height)); CGContextSetInterpolationQuality(context, 0.8); [self drawInRect:drawRect blendMode:kCGBlendModeNormal alpha:1]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } // caculate drawrect respect to specific resize mode - (CGRect)caculateDrawRect:(CGSize)newSize resizeMode:(SvResizeMode)resizeMode { CGRect drawRect = CGRectMake(0, 0, newSize.width, newSize.height); CGFloat imageRatio = self.size.width / self.size.height; CGFloat newSizeRatio = newSize.width / newSize.height; switch (resizeMode) { case enSvResizeScale: { // scale to fill break; } case enSvResizeAspectFit: // any remain area is white { CGFloat newHeight = 0; CGFloat newWidth = 0; if (newSizeRatio >= imageRatio) { // max height is newSize.height newHeight = newSize.height; newWidth = newHeight * imageRatio; } else { newWidth = newSize.width; newHeight = newWidth / imageRatio; } drawRect.size.width = newWidth; drawRect.size.height = newHeight; drawRect.origin.x = newSize.width / 2 - newWidth / 2; drawRect.origin.y = newSize.height / 2 - newHeight / 2; break; } case enSvResizeAspectFill: { CGFloat newHeight = 0; CGFloat newWidth = 0; if (newSizeRatio >= imageRatio) { // max height is newSize.height newWidth = newSize.width; newHeight = newWidth / imageRatio; } else { newHeight = newSize.height; newWidth = newHeight * imageRatio; } drawRect.size.width = newWidth; drawRect.size.height = newHeight; drawRect.origin.x = newSize.width / 2 - newWidth / 2; drawRect.origin.y = newSize.height / 2 - newHeight / 2; break; } default: break; } return drawRect; } @end
這個工具類里面,實現了三種縮放模式(與縮放質量無關),分別是: enSvResizeScale,enSvResizeAspectFit,enSvResizeAspectFill。
a、拉伸填充。即不管目標尺寸中寬高的比例如何,我們都將對原圖進行拉伸,使之充滿整個目標圖像。
b、保持比例顯示。即縮放后盡量使原圖最大,同事維持原圖本身的比例,剩余區域將會做全透明的填充。這個類似於UIImageView中contentMode中的UIViewContentModeScaleAspectFit模式。
c、保持比例填充。即縮放后的圖像依舊保持原圖比例的基礎上進行填充,部分圖片可能會被裁剪。這個類似於UIImageView中contentMode中的UIViewContentModeScaleAspectFill模式。
小結: 第二部分講述使用quartz2D進行圖像縮放的知識,我們可以看出quartz2D幫我們完成了圖像縮放過程中插值的處理,十分方便。
三、圖像裁剪
圖像裁剪即去除不必要的圖像區域,摳出我們希望保留的信息。按照裁剪形狀可以分為以下兩種:
1、矩形裁剪
矩形裁剪是最常見的裁剪操作,操作方法比較簡單。下面我們看一下矩形裁剪示意圖:
上圖中黑色的框代表原圖大小,藍色的虛線框代表要裁剪出來的大小。很顯然裁剪出來的圖片不會比原圖更大,如果你裁剪出來的圖片比原圖更大的話通常情況下就錯了,當然除非你刻意為之。我們設裁剪區域的左上角坐標為(x,y),裁剪的寬高分別為cropWidth,cropHeight,原圖像寬高分別為width,height。要完成裁剪功能,我們只需要三步:
a、創建目標大小(cropWidth,cropHeight)的畫布。
b、使用UIImage的drawInRect方法進行繪制的時候,指定rect為(-x,-y,width,height)。
c、從畫布中得到裁剪后的圖像。
關鍵是在第二步,指定原圖像的繪制區域,因為我們需要得到從x,y位置開始的圖像,所做一個簡單的坐標轉換,只需要從-x,-y位置開始繪制即可。
下面是裁剪部分的源碼:
// UIImage+SvImageEdit.m // SvImageEdit // // Created by maple on 5/8/13. // Copyright (c) 2013 maple. All rights reserved. // #import "UIImage+Crop.h" @implementation UIImage (SvImageEdit) /* * @brief crop image */ - (UIImage*)cropImageWithRect:(CGRect)cropRect { CGRect drawRect = CGRectMake(-cropRect.origin.x , -cropRect.origin.y, self.size.width * self.scale, self.size.height * self.scale); UIGraphicsBeginImageContext(cropRect.size); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, CGRectMake(0, 0, cropRect.size.width, cropRect.size.height)); [self drawInRect:drawRect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } @end
2、任意形狀裁剪
任意形狀裁剪一個比較典型的例子就是photo中通過磁性套索進行摳圖,通過指定一系列的關鍵點來控制要扣出的圖片區域。這種裁剪的實現比矩形裁剪要稍微復雜一點,主要用到quartz2D中的兩個知識: Path,Clipping Area。任意形狀裁剪的示意圖如下:
上圖中黑色的框代表原圖大小,虛線代表實際裁剪的形狀,藍色的框代表着時間裁剪路徑的邊框,完成任意形狀摳圖,通常需要以下六步:
a、通過給定的點集確定出整個裁剪區域的尺寸和位置cropRect,即目標畫布的大小和裁剪區域的左上角的位置。
通常有兩種方法可以完成這個需求: 第一種創建一個空的畫布,然后開始一個Path,添加所有的點到path中,CGContextGetPathBoundingBox獲取到裁剪區域的邊框。或者直接創建一個mutablePath,然后添加所有點到該path中,通過通過CGPathGetBoundingBox獲取裁剪區域的邊框。當然也可以通過自己遍歷點集重的每一個點,找到最小點的坐標和最大點的坐標計算出裁剪區域的邊框。
b、創建目標大小的畫布。
c、在目標畫布中開啟一個path,然后添加所有點到path中。
這塊需要對path進行一個移動操作,因為傳入的點集是相對於原圖的原點位置的,因此我們需要對該path做一個(-cropRect.origin.x,-cropRect.origin.y)的平移操作。
d、通過該path設置裁剪區域。
e、使用UIImage的drawInRect方法進行繪制的時候,指定rect為(-cropRect.origin.x,-cropRect.origin.x,cropRect.size.width,cropRect.size.height)。
f、從畫布中獲取目標圖像。
下面是任意形狀裁剪的源碼:

// // UIImage+SvImageEdit.m // SvImageEdit // // Created by maple on 5/8/13. // Copyright (c) 2013 maple. All rights reserved. // #import "UIImage+Crop.h" @implementation UIImage (SvImageEdit) /* * @brief crop image with path */ - (UIImage*)cropImageWithPath:(NSArray*)pointArr { if (pointArr.count == 0) { return nil; } CGPoint *points = malloc(sizeof(CGPoint) * pointArr.count); for (int i = 0; i < pointArr.count; ++i) { points[i] = [[pointArr objectAtIndex:i] CGPointValue]; } UIGraphicsBeginImageContext(CGSizeMake(self.size.width * self.scale, self.size.height * self.scale)); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextBeginPath(context); CGContextAddLines(context, points, pointArr.count); CGContextClosePath(context); CGRect boundsRect = CGContextGetPathBoundingBox(context); UIGraphicsEndImageContext(); UIGraphicsBeginImageContext(boundsRect.size); context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, CGRectMake(0, 0, boundsRect.size.width, boundsRect.size.height)); CGMutablePathRef path = CGPathCreateMutable(); CGAffineTransform transform = CGAffineTransformMakeTranslation(-boundsRect.origin.x, -boundsRect.origin.y); CGPathAddLines(path, &transform, points, pointArr.count); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextClip(context); [self drawInRect:CGRectMake(-boundsRect.origin.x, -boundsRect.origin.y, self.size.width * self.scale, self.size.height * self.scale)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); CGPathRelease(path); UIGraphicsEndImageContext(); return image; } @end
小結: 第三部分講述了兩種裁剪: 矩形裁剪,任意形狀裁剪,主要用到的知識是quartz2D中的path和clipping area。
四、獲取UIImage中圖像的像素和使用像素創建UIImage
UIImage是UIKit中一個存儲和繪制圖像的工具類,可以打開常見的jpg,png,tif等格式的圖片。IOS中通常情況下使用該類就可以滿足日常使用了,但有些時候我們也需要獲取到圖像的像素,進行更細粒度的編輯操作,例如灰度化,二值話等等。
1、從UIImage獲取像素
要獲取到UIImage所表示的圖像的像素,我們需要借助quartz2D中的CGBitmapContext,前面我們創建BitmapContext的時候都是使用UIKit中的一個便利方法UIGraphicsBeginImageContext,這個方法的好處是方便易用,但易用的同時也就導致了很多細節我們不能控制。為了得到圖片中的像素我們需要使用更低級別的CGBitmapContextCreate方法,該方法需要指定位深(RGB中每一位所占的字節),顏色空間(前面的博客中有提到)以及alpha信息等。
完成獲取像素需要以下四步:
a、申請圖像大小的內存。
b、使用CGBitmapContextCreate方法創建畫布。
c、使用UIImage的draw方法繪制圖像到畫布中。
d、使用CGBitmapContextGetData方法獲取畫布對應的像素數據。
代碼如下:
// return bmpData is rgba - (BOOL)getImageData:(void**)data width:(NSInteger*)width height:(NSInteger*)height alphaInfo:(CGImageAlphaInfo*)alphaInfo { int imgWidth = self.size.width * self.scale; int imgHegiht = self.size.height * self.scale; CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); if (colorspace == NULL) { NSLog(@"Create Colorspace Error!"); return NO; } void *imgData = NULL; imgData = malloc(imgWidth * imgHegiht * 4); if (imgData == NULL) { NSLog(@"Memory Error!"); return NO; } CGContextRef bmpContext = CGBitmapContextCreate(imgData, imgWidth, imgHegiht, 8, imgWidth * 4, colorspace, kCGImageAlphaPremultipliedLast); CGContextDrawImage(bmpContext, CGRectMake(0, 0, imgWidth, imgHegiht), self.CGImage); *data = CGBitmapContextGetData(bmpContext); *width = imgWidth; *height = imgHegiht; *alphaInfo = kCGImageAlphaLast; CGColorSpaceRelease(colorspace); CGContextRelease(bmpContext); return YES; }
2、從像素創建UIImage
上面講到了從UIImage獲取像素,在我們編輯完像素以后,大部分情況會需要重新生成UIImage並顯示出來。這一部分的邏輯跟上一部分差不多,通過傳進來的像素創建畫布,然后通過CGBitmapContextCreateImage方法從畫布中獲取到CGImage,最后再創建出UIImage。注意如果指定的alpha信息需要和實際的像素格式對應,否則會得到錯誤的效果。
下面是從像素創建UIImage的源碼:
// the data should be RGBA format + (UIImage*)createImageWithData:(Byte*)data width:(NSInteger)width height:(NSInteger)height alphaInfo:(CGImageAlphaInfo)alphaInfo { CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); if (!colorSpaceRef) { NSLog(@"Create ColorSpace Error!"); } CGContextRef bitmapContext = CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpaceRef, kCGImageAlphaPremultipliedLast); if (!bitmapContext) { NSLog(@"Create Bitmap context Error!"); CGColorSpaceRelease(colorSpaceRef); return nil; } CGImageRef imageRef = CGBitmapContextCreateImage(bitmapContext); UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; CGImageRelease(imageRef); CGColorSpaceRelease(colorSpaceRef); CGContextRelease(bitmapContext); return image; }
小結: 第四部分主要討論了一下UIImage和實際像素數據之間的相互轉換,整個流程中最關鍵的函數就是CGBitmapContextCreateImage,如果傳入參數錯誤,可能會得到錯誤的結果。
總結:本篇博客中討論了IOS中常見的圖像編輯操作的原理和實現方法,所有操作都是基於quartz2D框架。quartz2D框架在完成2D圖像的編輯和繪制方面功能還是很強大的,還包括了pattern,shadow,gradients以及pdf的加載和展示等等,文中所用到只是quartz2D中很少的一部分知識,學會了quartz2D你就可以寫一個完整的圖片編輯軟件。
注: 文中所有圖片都是我用quartz2D繪制的,轉載請注明出去,有什么不對的地方,歡迎指正。