iOS修改圖片顏色(修改畫素色值)
轉自:https://www.jianshu.com/p/619ef8423895
假設這樣一個場景:一張圖片中有一朵白花,我們想要把它變成紅花;或者一張圖片中有一段黑色的文字,我們想要把它變成紅色,應該怎麼做?
想要實現這個需求,就需要從畫素尺度上對圖片進行修改,將指定區域內的畫素的色值改為我們需要的顏色。但是,如何從這張圖上找到那段文字或者那朵花,並不在本文的討論範圍內,那是OCR和機器學期的事ㄟ( ▔, ▔ )ㄏ。
進入正題
假設我們要把一張有一段黑色文字的圖片中的文字修改為紅色:
示例圖片
要實現這個需求,我們應該怎麼做?
- 建立一個畫布,並將原始圖片平鋪在畫布上
- 遍歷圖片上的畫素,找到目標區域內的黑色文字的畫素,將它改為紅色
- 輸出修改後的圖片,並清理記憶體
我們需要哪些資訊才足夠實現這個功能?
- 一個Rect:需要修改這張圖片上哪個區域的畫素
- 需要被修改的色值區域:需要把哪個色值範圍內的畫素修改為目標顏色
- 目標顏色:需要將符合上述兩點的畫素修改為什麼顏色
具體實現
在貼程式碼之前,先講一些廢話:
- 圖片的解析度代表著它的畫素個數,比如上圖的解析度為1054 * 316,那麼它的畫素個數就是 1054 * 316 = 333064;
- 圖片的寬度代表這張圖一共有多少列畫素,高度代表一共有多少行畫素;即寬度代表列數,高度代表行數
- 在畫素尺度上,圖片中元素邊緣的顏色並不如我們肉眼看到的那樣。比如上圖中的文字是純黑色的,但是如果你放大放大再放大,會發現文字邊緣的顏色其實是灰色的(這也是上面為什麼說需要一個色值區域的原因);
- 圖片轉為2進位制的資料時,每個畫素為最小單元,從左上角開始,到右下角結束,從左到右從上到下排列畫素,但它並不是二維,而是一維的。
- alpha通道:一個畫素的色值是由RGBA四個值確定的。如果不含alpha通道的話,則是由RGB三個值確定,而A則一直是0xFF,即RGBX(X代表不含alpha通道,X一直為0xFF)
下面就是實現這個功能的核心程式碼了,這裡是作為UIImage的一個category方法實現的:
/** 解釋一下前兩個引數的含義: 想象一個數軸,最左邊是黑色(RGBX:0x000000FF),最右邊是白色(0xFFFFFFFF), nearBlackColor是靠近左邊邊界的色值,nearWhiteColor是靠近右邊邊界的色值, 它們中間則是需要被修改的色值範圍 */ - (UIImage *)translatePixelColorByTargetNearBlackColorRGBA:(UInt32)nearBlackRGBA nearWhiteColorRGBA:(UInt32)nearWhiteRGBA transColorRGBA:(UInt32)transRGBA inRect:(CGRect)rect { // 第一步:判斷傳入的rect是否在圖片的bounds內 CGRect canvas = CGRectMake(0, 0, self.size.width, self.size.height); if (!CGRectContainsRect(canvas, rect)) { if (CGRectIntersectsRect(canvas, rect)) { rect = CGRectIntersection(canvas, rect); // 取交集 } else { return self; } } UIImage *transImage = nil; int imageWidth = self.size.width; int imageHeight = self.size.height; // 第二步:建立色彩空間、畫布上下文,並將圖片以bitmap(不含alpha通道)的方式畫在畫布上。 size_t bytesPerRow = imageWidth * 4; uint32_t *rgbImageBuf = (uint32_t *)malloc(bytesPerRow * imageHeight); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast); CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), self.CGImage); // 第三步:遍歷並修改畫素 uint32_t *pCurPtr = rgbImageBuf; pCurPtr += (long)(rect.origin.y*imageWidth); // 將指標移動到初始行的起始位置 // 空間複雜度:O(rect.size.width * rect.size.height) for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) { // row pCurPtr += (long)rect.origin.x; // 將指標移動到當前行的起始列 for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) { // column if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; } // 將圖片轉成想要的顏色 uint8_t *ptr = (uint8_t *)pCurPtr; ptr[3] = (transRGBA >> 24) & 0xFF; // R ptr[2] = (transRGBA >> 16) & 0xFF; // G ptr[1] = (transRGBA >> 8) & 0xFF; // B } pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect)); // 將指標移動到下一行的起始列 } // 第四步:輸出圖片 CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight, providerReleaseDataCallback); CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace, kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider, NULL, true, kCGRenderingIntentDefault); CGDataProviderRelease(dataProvider); transImage = [UIImage imageWithCGImage:imageRef]; // end:清理空間 CGImageRelease(imageRef); CGContextRelease(context); CGColorSpaceRelease(colorSpace); return transImage ? : self; } void providerReleaseDataCallback (void *info, const void *data, size_t size) { free((void*)data); }
怎麼呼叫呢?
[image translatePixelColorByTargetNearBlackColorRGBA:0x000000FF nearWhiteColorRGBA:0x323232FF transColorRGBA:0xFF0000FF inRect:rect];
看起來有些麻煩是嗎?色值要寫那麼長,而且既然是以不含alpha通道的方式實現的,那麼alpha值便沒有意義,所以我們還可以再封裝幾個方法以便使用起來更方便:
- (UIImage *)translatePixelColorByTargetNearBlackColor:(UIColor *)nearBlackColor
nearWhiteColor:(UIColor *)nearWhiteColor
transColor:(UIColor *)transColor {
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
return [self translatePixelColorByTargetNearBlackColor:nearBlackColor nearWhiteColor:nearWhiteColor transColor:transColor inRect:rect];
}
- (UIImage *)translatePixelColorByTargetNearBlackColor:(UIColor *)nearBlackColor
nearWhiteColor:(UIColor *)nearWhiteColor
transColor:(UIColor *)transColor
inRect:(CGRect)rect {
// UIColor 轉 RGBA
UInt32 nearBlackRGBA = nearBlackColor.RGBA;
UInt32 nearWhiteRGBA = nearWhiteColor.RGBA;
UInt32 transRGBA = transColor.RGBA;
return [self translatePixelColorByTargetNearBlackColorRGBA:nearBlackRGBA nearWhiteColorRGBA:nearWhiteRGBA transColorRGBA:transRGBA inRect:rect];
}
- (UIImage *)translatePixelColorByTargetNearBlackColorHex:(UInt32)nearBlackRGB
nearWhiteColorHex:(UInt32)nearWhiteRGB
transColorHex:(UInt32)transRGB {
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
return [self translatePixelColorByTargetNearBlackColorHex:nearBlackRGB nearWhiteColorHex:nearWhiteRGB transColorHex:transRGB inRect:rect];
}
- (UIImage *)translatePixelColorByTargetNearBlackColorHex:(UInt32)nearBlackRGB
nearWhiteColorHex:(UInt32)nearWhiteRGB
transColorHex:(UInt32)transRGB
inRect:(CGRect)rect {
// RGB 轉 RGBA
UInt32 nearBlackRGBA = (nearBlackRGB << 8) + 0xFF;
UInt32 nearWhiteRGBA = (nearWhiteRGB << 8) + 0xFF;
UInt32 transRGBA = (transRGB << 8) + 0xFF;
return [self translatePixelColorByTargetNearBlackColorRGBA:nearBlackRGBA nearWhiteColorRGBA:nearWhiteRGBA transColorRGBA:transRGBA inRect:rect];
}
另外,這是上面使用到的UIColor轉RGBA的方法,它是作為UIColor的category方法實現的:
- (UInt32)RGBA {
CGFloat red = 0;
CGFloat green = 0;
CGFloat blue = 0;
CGFloat alpha = 0;
BOOL succ = [self getRed:&red green:&green blue:&blue alpha:&alpha];
UInt32 r = round(red*255);
UInt32 g = round(green*255);
UInt32 b = round(blue*255);
UInt32 a = round(alpha*255);
r = (r << 24);
g = (g << 16);
b = (b << 8);
UInt32 rgba = r + g + b + a;
return succ ? rgba : 0x00000000;
}
如果上述正好能符合你目前遇到的問題,而你又急於驗證能否解決問題的話,把上面的程式碼copy一下就可以了。如果你既想知其然,又想知其所以然,那麼我們繼續。
上述核心程式碼中分四步實現了修改圖片畫素色值,其中第一、二、四沒有什麼可說的,都是固定程式碼。
但第三步的演算法我認為有必要解釋一下,所以有了下面這些內容。
當然,如果你已經從程式碼中看明白了,那麼我可以負責任的告訴你,本文已經結束啦~!
如果你覺得有些懵嗶,那太好了!我又可以繼續講(zhuang)解(bi)了!那麼,來嘛客官,咱們繼續~
首先先來看下面一張圖:
畫素矩陣示例
前面已經說過了,我們採用不含alpha通道的方式實現。那麼一個畫素就是由RGBX四個值確定,其中X是無效的。這是上圖中“Pixel”想要表示的含義。
“Image Raw Data”想要表示的是,圖片在轉為2進位制後,畫素在其中是怎樣排列的。其中的數字表示的是畫素在整張圖片中的索引。前面也說過,是由一個二維的圖片畫素矩陣(就是上圖最後那個4*4的“Image Pixel Matrix”)從左到右從上到下轉換成的一維佇列。
可以看出,在二維的圖片上,我們需要修改的區域是連續的一塊,但是在轉化為二進位制的資料中,它們則是斷續的。
我把上面的那段程式碼再貼一下,以便對照解釋:
// 第三步:遍歷並修改畫素
uint32_t *pCurPtr = rgbImageBuf;
pCurPtr += (long)(rect.origin.y*imageWidth); // 將指標移動到初始行的起始位置
// 空間複雜度:O(rect.size.width * rect.size.height)
for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) { // row
pCurPtr += (long)rect.origin.x; // 將指標移動到當前行的起始列
for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) { // column
if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; }
// 將圖片轉成想要的顏色
uint8_t *ptr = (uint8_t *)pCurPtr;
ptr[3] = (transRGBA >> 24) & 0xFF; // R
ptr[2] = (transRGBA >> 16) & 0xFF; // G
ptr[1] = (transRGBA >> 8) & 0xFF; // B
}
pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect)); // 將指標移動到下一行的起始列
}
所以按上圖所示,整張圖片的bounds為(0, 0, 4, 4),我們需要修改rect(1, 1, 2, 2)內的畫素色值。下面所講要學會自動腦補二維圖片轉換一維二進位制資料,凡是指出座標的都是二維圖片,而說指標的都是在說一維的二進位制資料中某個畫素的指標。
- 我們的空間複雜度為O(rect.size.width * rect.size.height),所以遍歷時第一層的for迴圈遍歷次數為rect.size.width(即2),而i是從rect.origin.y(即1)開始的;第二層for迴圈的遍歷次數為rect.size.height(也是2),而j是從rect.origin.x(即1)開始的。總之,我們是從point(1, 1)位置開始遍歷的。
- 首先需要將指標移動到初始行的起始列:
pCurPtr += (long)(rect.origin.y*imageWidth);
,即畫素4的所在的位置。目的是為了跳過目標區域上方的無關行。 - 只跳過了上面的無關行還不夠,我們還需要跳過左邊的無關列,即
pCurPtr += (long)rect.origin.x;
,這時候指標指到了畫素5的位置(就是步驟1中所說point(1, 1)的位置),然後我們就可以開始真正的遍歷了。 - 在遍歷完這一行的目標區域後,指標指到了畫素7的位置;然後還需要跳過右邊的無關列:
pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));
,這時候指標指到了畫素8的位置。此時這一行已經完全遍歷結束,跳到了下一行的起始位置,又回到了步驟3的狀態(只是row+1了) - 然後重複執行3、4步驟,直到
i >= CGRectGetMaxY(rect)
結束
至此,這個演算法解釋完畢~
唉~,這一塊我也是想破頭該怎麼描述,可是寫出來發現還是不太理想。。。
我只能祈禱我太低估讀者的水平,其實大家都是能直接看懂程式碼的,根本不需要我解釋ㄟ( ▔, ▔ )ㄏ。
如果大家看完之後還是有不理解的地方;還有一些我沒詳細解釋的地方,如果有不理解的,都歡迎在留言區討論。
本人作為寫文章的新手,如果有錯誤的地方,也歡迎大家在留言區指正!
最後,這裡是Demo地址