原文地址:http://www.raywenderlich.com/69855/image-processing-in-ios-part-1-raw-bitmap-modification
泰然翻譯組:The Game。校對:lareina。
想象一張最好的生活自拍照。它是很高大尚滴並且以後會有用武之地。轉發,票選將會使你獲得成千上萬份的關注,因爲它確實很酷很帥。現在,如果你有什麼辦法,可以讓它看起來更加的高大尚。。。
這就是圖形圖像處理要做到的!它可以讓你的照片帶上更多的特殊效果,比如修改顏色,與其它的圖片進行合成等等。
在這兩部分教程中,你需要先弄明白一些圖形圖像處理的基礎知識。接着,你可以利用如下四個流行的圖形圖像處理方法編寫一個實現“幽靈圖像過濾器”的程序:
1:位圖圖像原圖修改
2:使用Core Graphics庫
3:使用Core Image庫
4:使用GPUImage庫的第三部分
在圖形圖像處理教程的第一節,主要講解位圖圖像原圖的修改。一但你明白基本的圖形處理方法,那麼其它的相關內容你也會較容易的弄明白。在教程的第二部分,主要介紹另外的三種修改圖像方法。
本教程假設你擁有關於IOS系統和Object-C的基礎,但在開始本教程前不需要擁有任何關於圖形圖像處理的知識。
開始
在開始寫代碼之前,先理解一些關於圖形圖像處理的基本概念很是需要。所以,先別急,放輕鬆,讓我們在最短的時間裏去了解一下圖形圖像的內部工作原理。
第一件事情,看一下我們本教程中的新朋友。。。掌聲在哪裏。。。幽靈!
不要怕,幽靈不是真的鬼魂。實際上,它只是一張圖像。簡單來說,它就是由一堆1和0組成的。這樣說聽上去會更好一些。
什麼是圖形圖像
一張圖像就是像素點的集合,每一個像素都是一個單獨,明瞭的顏色。圖像一般情況下都存儲成數組,你可以把他們相像成2維數組。
這一張是縮放版本的幽靈,被放大後:
圖像中這些小的“方塊”就是像素,每一像素只表示一種顏色。當成百上千萬的像素集體到一起後,就構成了圖形圖像。
如何用字節來表示顏色
表示圖形的方式有許多種。在本教程中使用的是最簡單的:32位RGBA模式。
如同它的名字一樣,32位RGBA模式會將一個顏色值存儲在32位,或者4個字節中。每一個字節存儲一個部分或者一個顏色通道。這4個部分分別是:
~ R代表紅色
~ G代表綠色
~ B代表藍色
~ A代表透明度
正如你所知道的,紅,綠和藍是所有顏色的基本顏色集。你幾乎可以使用他們創建搭配出任何想要的顏色。
由於使用8位表示每一種顏色值,那麼使用32位RGBA模式實際上可以創建出不透明的顏色的總數是256256256種,已經接近17億種。驚歎,那是好多好多好多的顏色!
alpha通道與其它的不同。你可以把它當成透明的東西,就像UIView的alpah屬性。
透明顏色意味着沒有任何的顏色,除非在它的後面有另外一種顏色;它的主要功能就是要告訴圖像處理這個像素的透明度是多少,於是,就會有多少顏色值穿透過它而顯示出來。
你將會通過本節後面的內容更新深入的瞭解。
總結一下,一個圖形就是像素的集體,並且每一個像素只能表示一種顏色。本節,你已經瞭解了32位RGBA模式。
提示:你有沒有想過,位圖的結構組成?一張位圖就是一張2D的地圖,每一塊就是一個像素!像素就是地圖的每一塊。哈哈!
現在你已經瞭解了用字節表示顏色的基礎了。不過在你開始着手寫代碼前,還有三個以上的概念需要你瞭解。
顏色空間
使用RGB模式表示顏色是顏色空間的一個例子。它只是衆多存儲顏色方法中的一種。另外一種顏色空間是灰階空間。像它的名字一樣,所有的圖形都只有黑和白,只需要保存一個值來表示這種顏色。
下面這種使用RGB模式表示的顏色,人類的肉眼是很難識別的。
Red: 0 Green:104 Blue:55
你認爲RGB值爲[0,104,55]會產生一種什麼顏色?
認真的思考一下,你也許會說是一種藍綠色或者綠色,但那是錯的。原來,你所看到的是深綠色。
另外兩種比較常見的顏色空間是HSV和YUV。
HSV,使用色調,飽和度和亮度來直觀的存儲顏色值。你可以把這三個部分這樣來看:
·色調就是顏色
·飽和度就是這個顏色有多麼的飽滿
·值就是顏色的亮度有多亮
在這種顏色空間中,如果你發現自己並不知道HSV的值,那麼通過它的三個值,可以很容易的相像出大概是什麼顏色。
RGB和HSV顏色空間的區別是很容易理解的,請看下面的圖像:
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在繪製圖像的時候,會使用大量內存把圖像的原始尺寸進行解壓縮。如果你的程序佔用了過多的內存,那麼操作系統會將進程殺死(程序崩潰)。所以請確定你的程序使用較大的圖像進行過測試。
我需要一些行動…
關注一下像素
現在,你已經基礎瞭解了圖形圖像的內部工作原理,已經可以開始編寫代碼嘍。今天你將會開發一款改變自己照片的程序,叫做SpookCam,該程序會把一張幽靈的圖像放到你的照片中!
下載工具包在xcode中打開該項目,編譯並運行。在你的手機上會看到如下的圖像:
在控制檯,你會看到如下的輸出:
當前的程序可以加載這張幽靈的圖像,並得到圖像的所有像素值,打印出每個像素的亮度值到日誌中。
亮度值是神馬?它就是紅色,綠色和藍色通過的平均值。
注意輸出日誌外圍的亮度值都爲0,這意味着他們代碼的是黑色。然而,他們的透明度的值是0,所以它們是透明不可見的。爲了證明這一點,試着將imageView的背景顏色設置成紅色,然後再次編譯並運行。
現在快速的瀏覽一下代碼。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)的定義,用來方便處理。
編譯,並運行。從相冊(拍照)選擇一張圖片,它將會出現在屏幕上:
照片中的人看上去在放鬆,是時候把幽靈放進去了!
在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並返回它。暫時忽視掉內存泄露問題。編譯並運行,你將會看到漂浮的幽靈圖像:
好了,完成了,這個程序簡直就像個病毒!
黑白顏色
最後一種效果。嘗試自己實現黑白顏色效果。爲了做到這點,你需要把每一個像素的紅色,綠色,藍色通道的值設定成三個通道原始顏色值的平均值,就像開始的時候輸出幽靈圖像所有像素亮度值那樣。
在註釋語句// 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);
編譯並運行,不要被結果嚇到:
下面需要做的:
恭喜!你已經完成了自己的第一個圖像處理程序。你可以在這裏下載該工程的源代碼。
還不錯吧?你可以嘗試修改一下循環中的代碼創建自己想要的效果,嘗試下實現下面的效果:
·嘗試調換圖像的紅色和藍色通道值
·提高圖像的亮度10%
·作爲進一步的挑戰,嘗試只使用基於像素的方法縮放幽靈的圖像,下面是步驟:
1:使用幽靈圖像的尺寸大小創建一個新的CGContext。
2:在原圖像中得到你想要的並賦值到新的緩存圖像中。
3:附加,嘗試在像素之前進行計算並插入相似值像素點。如果你可以在四個像素間進行插入,你自己就已經實現Bilinear scaling(雙線性插值法)了!太牛了!
如果你已經完成了第一個項目,想必你對圖形圖像的處理已經有了基本的概念。現在你可以嘗試使用更快更好的方法來實現相同的效果。
在下一章節中,你將會使用另外三個新的方法替換-processUsingPixels:完成相同的任務。一定要看丫!
同時,如果你對該章節有任何疑問和不解,請留言給我!