Quartz2D 圖像處理

本文將爲大家介紹常見的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繪製的,轉載請註明出去,有什麼不對的地方,歡迎指正。

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