IOS中圖形圖像處理第一部分:位圖圖像原圖修改

   

原文地址:http://www.raywenderlich.com/69855/image-processing-in-ios-part-1-raw-bitmap-modification
泰然翻譯組:The Game。校對:lareina。

Header-250x250.png

想象一張最好的生活自拍照。它是很高大尚滴並且以後會有用武之地。轉發,票選將會使你獲得成千上萬份的關注,因爲它確實很酷很帥。現在,如果你有什麼辦法,可以讓它看起來更加的高大尚。。。

這就是圖形圖像處理要做到的!它可以讓你的照片帶上更多的特殊效果,比如修改顏色,與其它的圖片進行合成等等。

在這兩部分教程中,你需要先弄明白一些圖形圖像處理的基礎知識。接着,你可以利用如下四個流行的圖形圖像處理方法編寫一個實現“幽靈圖像過濾器”的程序:

1:位圖圖像原圖修改
2:使用Core Graphics庫
3:使用Core Image庫
4:使用GPUImage庫的第三部分

在圖形圖像處理教程的第一節,主要講解位圖圖像原圖的修改。一但你明白基本的圖形處理方法,那麼其它的相關內容你也會較容易的弄明白。在教程的第二部分,主要介紹另外的三種修改圖像方法。

本教程假設你擁有關於IOS系統和Object-C的基礎,但在開始本教程前不需要擁有任何關於圖形圖像處理的知識。

開始

在開始寫代碼之前,先理解一些關於圖形圖像處理的基本概念很是需要。所以,先別急,放輕鬆,讓我們在最短的時間裏去了解一下圖形圖像的內部工作原理。

第一件事情,看一下我們本教程中的新朋友。。。掌聲在哪裏。。。幽靈!

ghost.png

不要怕,幽靈不是真的鬼魂。實際上,它只是一張圖像。簡單來說,它就是由一堆1和0組成的。這樣說聽上去會更好一些。

什麼是圖形圖像

一張圖像就是像素點的集合,每一個像素都是一個單獨,明瞭的顏色。圖像一般情況下都存儲成數組,你可以把他們相像成2維數組。

這一張是縮放版本的幽靈,被放大後:

ghost_tiny.png

圖像中這些小的“方塊”就是像素,每一像素只表示一種顏色。當成百上千萬的像素集體到一起後,就構成了圖形圖像。

如何用字節來表示顏色

表示圖形的方式有許多種。在本教程中使用的是最簡單的:32位RGBA模式。

如同它的名字一樣,32位RGBA模式會將一個顏色值存儲在32位,或者4個字節中。每一個字節存儲一個部分或者一個顏色通道。這4個部分分別是:

~ R代表紅色

~ G代表綠色

~ B代表藍色

~ A代表透明度

正如你所知道的,紅,綠和藍是所有顏色的基本顏色集。你幾乎可以使用他們創建搭配出任何想要的顏色。

由於使用8位表示每一種顏色值,那麼使用32位RGBA模式實際上可以創建出不透明的顏色的總數是256256256種,已經接近17億種。驚歎,那是好多好多好多的顏色!

alpha通道與其它的不同。你可以把它當成透明的東西,就像UIView的alpah屬性。

透明顏色意味着沒有任何的顏色,除非在它的後面有另外一種顏色;它的主要功能就是要告訴圖像處理這個像素的透明度是多少,於是,就會有多少顏色值穿透過它而顯示出來。

你將會通過本節後面的內容更新深入的瞭解。

總結一下,一個圖形就是像素的集體,並且每一個像素只能表示一種顏色。本節,你已經瞭解了32位RGBA模式。

提示:你有沒有想過,位圖的結構組成?一張位圖就是一張2D的地圖,每一塊就是一個像素!像素就是地圖的每一塊。哈哈!

現在你已經瞭解了用字節表示顏色的基礎了。不過在你開始着手寫代碼前,還有三個以上的概念需要你瞭解。

顏色空間

使用RGB模式表示顏色是顏色空間的一個例子。它只是衆多存儲顏色方法中的一種。另外一種顏色空間是灰階空間。像它的名字一樣,所有的圖形都只有黑和白,只需要保存一個值來表示這種顏色。

下面這種使用RGB模式表示的顏色,人類的肉眼是很難識別的。

DarkGreen.jpg

Red: 0 Green:104 Blue:55

你認爲RGB值爲[0,104,55]會產生一種什麼顏色?

認真的思考一下,你也許會說是一種藍綠色或者綠色,但那是錯的。原來,你所看到的是深綠色。

另外兩種比較常見的顏色空間是HSV和YUV。

HSV,使用色調,飽和度和亮度來直觀的存儲顏色值。你可以把這三個部分這樣來看:

·色調就是顏色
·飽和度就是這個顏色有多麼的飽滿
·值就是顏色的亮度有多亮

在這種顏色空間中,如果你發現自己並不知道HSV的值,那麼通過它的三個值,可以很容易的相像出大概是什麼顏色。

RGB和HSV顏色空間的區別是很容易理解的,請看下面的圖像:

rgbvshsv.jpg

YUV是另外一種常見的顏色空間,電視機使用的就是這種方式。

最開始的時候,電視機只有灰階空間一種顏色通道。後來,當彩色電影出現後,就有了2種通道。當然,如果你想在本教程中使用YUV,那麼你需要去研究更多關於YUV和其它顏色空間的相關知識。

NOTE:同樣的顏色空間,你也可以使用不同的方法表示顏色。比如16位RGB模式,可以使用5個字節存儲R,6個字節存儲G,5個字節存儲B。

爲什麼用6個字節存儲綠色,5個字節存儲藍色?這是一個有意思的問題,答案就是因爲眼球。人類的眼球對綠色比較敏感,所以人類的眼球更空間分辨出綠色的顏色值變化。

座標系統

既然一個圖形是由像素構成的平面地圖,那麼圖像的原點需要說明一下。通常原點在圖像的左上角,Y軸向下;或者原點在圖像的左下,Y軸向上。

沒有固定的座標系統,蘋果在不同的地方可能會使用不同的座標系。

目前,UIImage和UIView使用的是左上原點座標,Core Image和Core Graphics使用的是左下原點座標。這個概念很重要,當你遇到圖像繪製倒立問題的時候你就知道了。

圖形壓縮

這是在你開始編寫代碼前的最後一個需要了解的概念了!原圖的每一個像素都被存儲在各自的內存中。

如果你使用一張8像素的圖形做運算,它將會消耗810^6像素4比特/像素=32兆字節內存。關注一下數據!

這就是爲什麼會出現jpeg,png和其它圖形格式的原因。這些都是圖形壓縮格式。

當GPU在繪製圖像的時候,會使用大量內存把圖像的原始尺寸進行解壓縮。如果你的程序佔用了過多的內存,那麼操作系統會將進程殺死(程序崩潰)。所以請確定你的程序使用較大的圖像進行過測試。
ghost.png

我需要一些行動…

關注一下像素

現在,你已經基礎瞭解了圖形圖像的內部工作原理,已經可以開始編寫代碼嘍。今天你將會開發一款改變自己照片的程序,叫做SpookCam,該程序會把一張幽靈的圖像放到你的照片中!

下載工具包在xcode中打開該項目,編譯並運行。在你的手機上會看到如下的圖像:

screenshot2-ghosty1.png

在控制檯,你會看到如下的輸出:

Screenshot1-pixel-output-700x410.png

當前的程序可以加載這張幽靈的圖像,並得到圖像的所有像素值,打印出每個像素的亮度值到日誌中。

亮度值是神馬?它就是紅色,綠色和藍色通過的平均值。

注意輸出日誌外圍的亮度值都爲0,這意味着他們代碼的是黑色。然而,他們的透明度的值是0,所以它們是透明不可見的。爲了證明這一點,試着將imageView的背景顏色設置成紅色,然後再次編譯並運行。

Ghosty-Red.png

現在快速的瀏覽一下代碼。ViewController.m中使用UIImagePickerController來在相冊中取得圖像或者使用機機獲得圖像。

當它選定一張圖像後,調用-setupWithImage:在這行中,輸出了每一像素的亮度值到日誌中。定位到ViewController.m中的logPixelsOfImage,查看方法中的開始部分:

// 1.
CGImageRef inputCGImage = [image CGImage];
NSUInteger width =                 CGImageGetWidth(inputCGImage);
NSUInteger height = CGImageGetHeight(inputCGImage);

// 2.
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel *     width;
NSUInteger bitsPerComponent = 8;

UInt32 * pixels;
pixels = (UInt32 *) calloc(height * width,     sizeof(UInt32));

// 3.
CGColorSpaceRef colorSpace =     CGColorSpaceCreateDeviceRGB();
CGContextRef context =     CGBitmapContextCreate(pixels, width, height,     bitsPerComponent, bytesPerRow, colorSpace,     kCGImageAlphaPremultipliedLast |     kCGBitmapByteOrder32Big);

// 4.
CGContextDrawImage(context, CGRectMake(0,     0, width, height), inputCGImage);

// 5. Cleanup
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);

現在,讓我們分段的來看一下:

1:第一部分:把UIImage對象轉換爲需要被核心圖形庫調用的CGImage對象。同時,得到圖形的寬度和高度。

2:第二部分:由於你使用的是32位RGB顏色空間模式,你需要定義一些參數bytesPerPixel(每像素大小)和bitsPerComponent(每個顏色通道大小),然後計算圖像bytesPerRow(每行有大)。最後,使用一個數組來存儲像素的值。

3:第三部分:創建一個RGB模式的顏色空間CGColorSpace和一個容器CGBitmapContext,將像素指針參數傳遞到容器中緩存進行存儲。在後面的章節中將會進一步研究核圖形庫。

4:第四部分:把緩存中的圖形繪製到顯示器上。像素的填充格式是由你在創建context的時候進行指定的。

5:第五部分:清除colorSpace和context.

NOTE:當你繪製圖像的時候,設備的GPU會進行解碼並將它顯示在屏幕。爲了訪問本地數據,你需要一份像素的複製,就像剛纔做的那樣。

此時此刻,pixels存儲着圖像的所有像素信息。下面的幾行代碼會對pixels進行遍歷,並打印:

// 1.
#define Mask8(x) ( (x) & 0xFF )
#define R(x) ( Mask8(x) )
#define G(x) ( Mask8(x >> 8 ) )
#define B(x) ( Mask8(x >> 16) )

NSLog(@"Brightness of image:");
// 2.
UInt32 * currentPixel = pixels;
for (NSUInteger j = 0; j < height; j++) {
  for (NSUInteger i = 0; i < width; i++) {
    // 3.
    UInt32 color = *currentPixel;
    printf("%3.0f ",     (R(color)+G(color)+B(color))/3.0);
    // 4.
    currentPixel++;
  }
  printf("\n");
}

代碼解釋:

1:定義了一些簡單處理32位像素的宏。爲了得到紅色通道的值,你需要得到前8位。爲了得到其它的顏色通道值,你需要進行位移並取截取。

2:定義一個指向第一個像素的指針,並使用2個for循環來遍歷像素。其實也可以使用一個for循環從0遍歷到width*height,但是這樣寫更容易理解圖形是二維的。

3:得到當前像素的值賦值給currentPixel並把它的亮度值打印出來。

4:增加currentPixel的值,使它指向下一個像素。如果你對指針的運算比較生疏,記住這個:currentPixel是一個指向UInt32的變量,當你把它加1後,它就會向前移動4字節(32位),然後指向了下一個像素的值。

提示:還有一種非正統的方法就是把currentPiexl聲明爲一個指向8字節的類型的指針,比如char。這種方法,你每增加1,你將會移動圖形的下一個顏色通道。與它進行位移運算,你會得到顏色通道的8位數值。

此時此刻,這個程序只是打印出了原圖的像素信息,但並沒有進行任何修改!下面將會教你如何進行修改。

SpookCame-原圖修改

四種研究方法都會在本小節進行,你將會花費更多的時間在本節,因爲它包括了圖形圖像處理的第一原則。掌握了這個方法你會明白其它庫所做的。

在本方法中,你會遍歷每一個像素,就像之前做的那個,但這次,將會對每個像素進行新的賦值。

這種方法的優點是容易實現和理解;缺點就是掃描大的圖形和效果的時候會更復雜,不精簡。

正如你在程序開始看到的,ImageProcessor類已經存在。將它應用到ViewController中,替換-setupWithImage,代碼如下:

- (void)setupWithImage:(UIImage*)image {
  UIImage * fixedImage = [image     imageWithFixedOrientation];
  self.workingImage = fixedImage;

  // Commence with processing!
  [ImageProcessor     sharedProcessor].delegate = self;
  [[ImageProcessor sharedProcessor]     processImage:fixedImage];
}

註釋掉-viewDidLoad中下面的代碼:

// [self setupWithImage:[UIImage     imageNamed:@"ghost_tiny.png"]];

現在,打開ImageProcessor.m。如你所見,ImageProcessor是單例模式,調用-processUsingPixels來加載圖像,然後通過ImageProcessorDelegate返回輸出。

-processsUsingPixels:是之前你所看到獲得圖形像素代碼的一種複製品,如同inputImage。注意兩個額外的宏A(x)和RGBAMake(r,g,b,a)的定義,用來方便處理。

編譯,並運行。從相冊(拍照)選擇一張圖片,它將會出現在屏幕上:

BuildnRun-1-308x500.png

照片中的人看上去在放鬆,是時候把幽靈放進去了!

在processUsingPixels的返回語句前,添加如下代碼,創建一個幽靈的CGImageRef對象。
UIImage * ghostImage = [UIImage imageNamed:@”ghost”];
CGImageRef ghostCGImage = [ghostImage CGImage];

現在,做一些數學運算來確定幽靈圖像放在原圖的什麼位置。

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);

以上代碼會把幽靈的圖像寬度縮小25%,並把它的原點設定在點ghostOrigin。

下一步是創建一張幽靈圖像的緩存圖,

NSUInteger ghostBytesPerRow = bytesPerPixel * ghostSize.width;
UInt32 * ghostPixels = (UInt32     *)calloc(ghostSize.width * ghostSize.height,     sizeof(UInt32));

CGContextRef ghostContext =     CGBitmapContextCreate(ghostPixels,     ghostSize.width, ghostSize.height,
                                       bit    sPerComponent, ghostBytesPerRow, colorSpace,
                                       kCG    ImageAlphaPremultipliedLast |     kCGBitmapByteOrder32Big);
CGContextDrawImage(ghostContext,     CGRectMake(0, 0, ghostSize.width,     ghostSize.height),ghostCGImage);

上面的代碼和你從inputImage中獲得像素信息一樣。不同的地方是,圖像會被縮小尺寸,變得更小了。

現在已經到了把幽靈圖像合併到你的照片中的最佳時間了。

合併:像前面提到的,每一個顏色都有一個透明通道來標識透明度。並且,你每創建一張圖像,每一個像素都會有一個顏色值。

所以,如果遇到有透明度和半透明的顏色值該如何處理呢?

答案是,對透明度進行混合。在最頂層的顏色會使用一個公式與它後面的顏色進行混合。公式如下:

NewColor = TopColor * TopColor.Alpha + BottomColor * (1 - TopColor.Alpha)

這是一個標準的線性差值方程。

·當頂層透明度爲1時,新的顏色值等於頂層顏色值。
·當頂層透明度爲0時,新的顏色值於底層顏色值。
·最後,當頂層的透明度值是0到1之前的時候,新的顏色值會混合借於頂層和底層顏色值之間。

還可以用 premultiplied alpha的方法。

當處理成千上萬像素的時候,他的性能會得以發揮。

好,回到幽靈圖。

如同其它位圖運算一樣,你需要一些循環來遍歷每一個像素。但是,你只需要遍歷那些你需要修改的像素。

把下面的代碼添加到processUsingPixels的下面,還是放在返回語句的前面:
NSUInteger offsetPixelCountForInput = ghostOrigin.y * inputWidth + ghostOrigin.x;
for (NSUInteger j = 0; j < ghostSize.height; j++) {
for (NSUInteger i = 0; i < ghostSize.width; i++) {
UInt32 * inputPixel = inputPixels + j * inputWidth + i + offsetPixelCountForInput;
UInt32 inputColor = *inputPixel;

    UInt32 * ghostPixel = ghostPixels + j     * (int)ghostSize.width + i;
    UInt32 ghostColor = *ghostPixel;

    // Do some processing here      
  }
}

通過對幽靈圖像像素數的循環和offsetPixelCountForInput獲得輸入的圖像。記住,雖然你使用的是2維數據存儲圖像,但在內存他它實際上是一維的。

下一步,添加下面的代碼到註釋語句 Do some processing here的下面來進行混合:

// Blend the ghost with 50% alpha
CGFloat ghostAlpha = 0.5f * (A(ghostColor)     / 255.0);
UInt32 newR = R(inputColor) * (1 -     ghostAlpha) + R(ghostColor) * ghostAlpha;
UInt32 newG = G(inputColor) * (1 -     ghostAlpha) + G(ghostColor) * ghostAlpha;
UInt32 newB = B(inputColor) * (1 -     ghostAlpha) + B(ghostColor) * ghostAlpha;

// Clamp, not really useful here :p
newR = MAX(0,MIN(255, newR));
newG = MAX(0,MIN(255, newG));
newB = MAX(0,MIN(255, newB));

*inputPixel = RGBAMake(newR, newG, newB,     A(inputColor));

這部分有2點需要說明:

1:你將幽靈圖像的每一個像素的透明通道都乘以了0.5,使它成爲半透明狀態。然後將它混合到圖像中像之前討論的那樣。

2:clamping部分將每個顏色的值範圍進行限定到0到255之間,雖然一般情況下值不會越界。但是,大多數情況下需要進行這種限定防止發生意外的錯誤輸出。

最後一步,添加下面的代碼到processUsingPixels的下面,替換之前的返回語句:

// Create a new UIImage
CGImageRef newCGImage =     CGBitmapContextCreateImage(context);
UIImage * processedImage = [UIImage     imageWithCGImage:newCGImage];

return processedImage;

上面的代碼創建了一張新的UIImage並返回它。暫時忽視掉內存泄露問題。編譯並運行,你將會看到漂浮的幽靈圖像:

BuildnRun-2.png

好了,完成了,這個程序簡直就像個病毒!

黑白顏色

最後一種效果。嘗試自己實現黑白顏色效果。爲了做到這點,你需要把每一個像素的紅色,綠色,藍色通道的值設定成三個通道原始顏色值的平均值,就像開始的時候輸出幽靈圖像所有像素亮度值那樣。

在註釋語句// create a new UIImage前添加上一步的代碼 。

找到了嗎?
// Convert the image to black and white
for (NSUInteger j = 0; j < inputHeight; j++) {
for (NSUInteger i = 0; i < inputWidth; i++) {
UInt32 * currentPixel = inputPixels + (j * inputWidth) + i;
UInt32 color = *currentPixel;

    // Average of RGB = greyscale
    UInt32 averageColor = (R(color) +     G(color) + B(color)) / 3.0;

    *currentPixel = RGBAMake(averageColor,     averageColor, averageColor, A(color));
  }
}

最後的一步就是清除內存。ARC不能代替你對CGImageRefs和CGContexts進行管理。添加如下代碼到返回語句之前。

 CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CGContextRelease(ghostContext);
free(inputPixels);
free(ghostPixels);

編譯並運行,不要被結果嚇到:

BuildnRun-3.png

下面需要做的:

恭喜!你已經完成了自己的第一個圖像處理程序。你可以在這裏下載該工程的源代碼。

還不錯吧?你可以嘗試修改一下循環中的代碼創建自己想要的效果,嘗試下實現下面的效果:

·嘗試調換圖像的紅色和藍色通道值
·提高圖像的亮度10%
·作爲進一步的挑戰,嘗試只使用基於像素的方法縮放幽靈的圖像,下面是步驟:
1:使用幽靈圖像的尺寸大小創建一個新的CGContext。

2:在原圖像中得到你想要的並賦值到新的緩存圖像中。

3:附加,嘗試在像素之前進行計算並插入相似值像素點。如果你可以在四個像素間進行插入,你自己就已經實現Bilinear scaling(雙線性插值法)了!太牛了!

如果你已經完成了第一個項目,想必你對圖形圖像的處理已經有了基本的概念。現在你可以嘗試使用更快更好的方法來實現相同的效果。

在下一章節中,你將會使用另外三個新的方法替換-processUsingPixels:完成相同的任務。一定要看丫!

同時,如果你對該章節有任何疑問和不解,請留言給我!

IOS中圖形圖像處理第一部分:位圖圖像原圖修改》若爲泰然網原創(翻譯),禁止用於一切商業行爲,轉載請註明出處並通知泰然網!


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