假設(shè)這樣一個(gè)場(chǎng)景:一張圖片中有一朵白花,我們想要把它變成紅花逞带;或者一張圖片中有一段黑色的文字,我們想要把它變成紅色,應(yīng)該怎么做较锡?
想要實(shí)現(xiàn)這個(gè)需求畴嘶,就需要從像素尺度上對(duì)圖片進(jìn)行修改熏挎,將指定區(qū)域內(nèi)的像素的色值改為我們需要的顏色管宵。但是,如何從這張圖上找到那段文字或者那朵花锚扎,并不在本文的討論范圍內(nèi)吞瞪,那是OCR和機(jī)器學(xué)期的事ㄟ( ▔, ▔ )ㄏ。
進(jìn)入正題
假設(shè)我們要把一張有一段黑色文字的圖片中的文字修改為紅色:
要實(shí)現(xiàn)這個(gè)需求驾孔,我們應(yīng)該怎么做芍秆?
- 創(chuàng)建一個(gè)畫布惯疙,并將原始圖片平鋪在畫布上
- 遍歷圖片上的像素,找到目標(biāo)區(qū)域內(nèi)的黑色文字的像素浪听,將它改為紅色
- 輸出修改后的圖片螟碎,并清理內(nèi)存
我們需要哪些信息才足夠?qū)崿F(xiàn)這個(gè)功能?
- 一個(gè)Rect:需要修改這張圖片上哪個(gè)區(qū)域的像素
- 需要被修改的色值區(qū)域:需要把哪個(gè)色值范圍內(nèi)的像素修改為目標(biāo)顏色
- 目標(biāo)顏色:需要將符合上述兩點(diǎn)的像素修改為什么顏色
具體實(shí)現(xiàn)
在貼代碼之前迹栓,先講一些廢話:
- 圖片的分辨率代表著它的像素個(gè)數(shù)掉分,比如上圖的分辨率為1054 * 316,那么它的像素個(gè)數(shù)就是 1054 * 316 = 333064克伊;
- 圖片的寬度代表這張圖一共有多少列像素酥郭,高度代表一共有多少行像素;即寬度代表列數(shù)愿吹,高度代表行數(shù)
- 在像素尺度上不从,圖片中元素邊緣的顏色并不如我們?nèi)庋劭吹降哪菢印1热缟蠄D中的文字是純黑色的犁跪,但是如果你放大放大再放大椿息,會(huì)發(fā)現(xiàn)文字邊緣的顏色其實(shí)是灰色的(這也是上面為什么說需要一個(gè)色值區(qū)域的原因);
- 圖片轉(zhuǎn)為2進(jìn)制的數(shù)據(jù)時(shí)坷衍,每個(gè)像素為最小單元寝优,從左上角開始,到右下角結(jié)束枫耳,從左到右從上到下排列像素乏矾,但它并不是二維,而是一維的迁杨。
- alpha通道:一個(gè)像素的色值是由RGBA四個(gè)值確定的钻心。如果不含alpha通道的話,則是由RGB三個(gè)值確定铅协,而A則一直是0xFF捷沸,即RGBX(X代表不含alpha通道,X一直為0xFF)
下面就是實(shí)現(xiàn)這個(gè)功能的核心代碼了狐史,這里是作為UIImage的一個(gè)category方法實(shí)現(xiàn)的:
/**
解釋一下前兩個(gè)參數(shù)的含義:
想象一個(gè)數(shù)軸亿胸,最左邊是黑色(RGBX:0x000000FF),最右邊是白色(0xFFFFFFFF)预皇,
nearBlackColor是靠近左邊邊界的色值,nearWhiteColor是靠近右邊邊界的色值婉刀,
它們中間則是需要被修改的色值范圍
*/
- (UIImage *)translatePixelColorByTargetNearBlackColorRGBA:(UInt32)nearBlackRGBA
nearWhiteColorRGBA:(UInt32)nearWhiteRGBA
transColorRGBA:(UInt32)transRGBA
inRect:(CGRect)rect {
// 第一步:判斷傳入的rect是否在圖片的bounds內(nèi)
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;
// 第二步:創(chuàng)建色彩空間吟温、畫布上下文,并將圖片以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); // 將指針移動(dòng)到初始行的起始位置
// 空間復(fù)雜度:O(rect.size.width * rect.size.height)
for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) { // row
pCurPtr += (long)rect.origin.x; // 將指針移動(dòng)到當(dāng)前行的起始列
for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) { // column
if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; }
// 將圖片轉(zhuǎn)成想要的顏色
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)); // 將指針移動(dòng)到下一行的起始列
}
// 第四步:輸出圖片
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);
}
怎么調(diào)用呢鲁豪?
[image translatePixelColorByTargetNearBlackColorRGBA:0x000000FF nearWhiteColorRGBA:0x323232FF transColorRGBA:0xFF0000FF inRect:rect];
看起來有些麻煩是嗎潘悼?色值要寫那么長,而且既然是以不含alpha通道的方式實(shí)現(xiàn)的爬橡,那么alpha值便沒有意義治唤,所以我們還可以再封裝幾個(gè)方法以便使用起來更方便:
- (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 轉(zhuǎn) 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 轉(zhuǎn) 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轉(zhuǎn)RGBA的方法糙申,它是作為UIColor的category方法實(shí)現(xiàn)的:
- (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;
}
如果上述正好能符合你目前遇到的問題宾添,而你又急于驗(yàn)證能否解決問題的話,把上面的代碼copy一下就可以了柜裸。如果你既想知其然缕陕,又想知其所以然,那么我們繼續(xù)疙挺。
上述核心代碼中分四步實(shí)現(xiàn)了修改圖片像素色值扛邑,其中第一、二铐然、四沒有什么可說的蔬崩,都是固定代碼。
但第三步的算法我認(rèn)為有必要解釋一下搀暑,所以有了下面這些內(nèi)容沥阳。
當(dāng)然,如果你已經(jīng)從代碼中看明白了险掀,那么我可以負(fù)責(zé)任的告訴你沪袭,本文已經(jīng)結(jié)束啦~!
如果你覺得有些懵嗶樟氢,那太好了冈绊!我又可以繼續(xù)講(zhuang)解(bi)了!那么埠啃,來嘛客官死宣,咱們繼續(xù)~
首先先來看下面一張圖:
前面已經(jīng)說過了,我們采用不含alpha通道的方式實(shí)現(xiàn)碴开。那么一個(gè)像素就是由RGBX四個(gè)值確定毅该,其中X是無效的。這是上圖中“Pixel”想要表示的含義潦牛。
“Image Raw Data”想要表示的是眶掌,圖片在轉(zhuǎn)為2進(jìn)制后,像素在其中是怎樣排列的巴碗。其中的數(shù)字表示的是像素在整張圖片中的索引朴爬。前面也說過,是由一個(gè)二維的圖片像素矩陣(就是上圖最后那個(gè)4*4的“Image Pixel Matrix”)從左到右從上到下轉(zhuǎn)換成的一維隊(duì)列橡淆。
可以看出召噩,在二維的圖片上母赵,我們需要修改的區(qū)域是連續(xù)的一塊,但是在轉(zhuǎn)化為二進(jìn)制的數(shù)據(jù)中具滴,它們則是斷續(xù)的凹嘲。
我把上面的那段代碼再貼一下,以便對(duì)照解釋:
// 第三步:遍歷并修改像素
uint32_t *pCurPtr = rgbImageBuf;
pCurPtr += (long)(rect.origin.y*imageWidth); // 將指針移動(dòng)到初始行的起始位置
// 空間復(fù)雜度:O(rect.size.width * rect.size.height)
for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) { // row
pCurPtr += (long)rect.origin.x; // 將指針移動(dòng)到當(dāng)前行的起始列
for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) { // column
if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; }
// 將圖片轉(zhuǎn)成想要的顏色
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)); // 將指針移動(dòng)到下一行的起始列
}
所以按上圖所示构韵,整張圖片的bounds為(0, 0, 4, 4)周蹭,我們需要修改rect(1, 1, 2, 2)內(nèi)的像素色值。下面所講要學(xué)會(huì)自動(dòng)腦補(bǔ)二維圖片轉(zhuǎn)換一維二進(jìn)制數(shù)據(jù)贞绳,凡是指出坐標(biāo)的都是二維圖片谷醉,而說指針的都是在說一維的二進(jìn)制數(shù)據(jù)中某個(gè)像素的指針。
- 我們的空間復(fù)雜度為O(rect.size.width * rect.size.height)冈闭,所以遍歷時(shí)第一層的for循環(huán)遍歷次數(shù)為rect.size.width(即2)俱尼,而i是從rect.origin.y(即1)開始的;第二層for循環(huán)的遍歷次數(shù)為rect.size.height(也是2)萎攒,而j是從rect.origin.x(即1)開始的遇八。總之耍休,我們是從point(1, 1)位置開始遍歷的刃永。
- 首先需要將指針移動(dòng)到初始行的起始列:
pCurPtr += (long)(rect.origin.y*imageWidth);
,即像素4的所在的位置羊精。目的是為了跳過目標(biāo)區(qū)域上方的無關(guān)行斯够。 - 只跳過了上面的無關(guān)行還不夠,我們還需要跳過左邊的無關(guān)列喧锦,即
pCurPtr += (long)rect.origin.x;
读规,這時(shí)候指針指到了像素5的位置(就是步驟1中所說point(1, 1)的位置),然后我們就可以開始真正的遍歷了燃少。 - 在遍歷完這一行的目標(biāo)區(qū)域后束亏,指針指到了像素7的位置;然后還需要跳過右邊的無關(guān)列:
pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));
阵具,這時(shí)候指針指到了像素8的位置碍遍。此時(shí)這一行已經(jīng)完全遍歷結(jié)束,跳到了下一行的起始位置阳液,又回到了步驟3的狀態(tài)(只是row+1了) - 然后重復(fù)執(zhí)行3怕敬、4步驟,直到
i >= CGRectGetMaxY(rect)
結(jié)束
至此帘皿,這個(gè)算法解釋完畢~
唉~赖捌,這一塊我也是想破頭該怎么描述,可是寫出來發(fā)現(xiàn)還是不太理想。越庇。。
我只能祈禱我太低估讀者的水平奉狈,其實(shí)大家都是能直接看懂代碼的卤唉,根本不需要我解釋ㄟ( ▔, ▔ )ㄏ。
如果大家看完之后還是有不理解的地方仁期;還有一些我沒詳細(xì)解釋的地方桑驱,如果有不理解的,都?xì)g迎在留言區(qū)討論跛蛋。
本人作為寫文章的新手熬的,如果有錯(cuò)誤的地方,也歡迎大家在留言區(qū)指正赊级!
最后押框,這里是Demo地址