iOS修改圖片顏色(修改像素色值)

假設(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)該怎么做芍秆?

  1. 創(chuàng)建一個(gè)畫布惯疙,并將原始圖片平鋪在畫布上
  2. 遍歷圖片上的像素,找到目標(biāo)區(qū)域內(nèi)的黑色文字的像素浪听,將它改為紅色
  3. 輸出修改后的圖片螟碎,并清理內(nèi)存

我們需要哪些信息才足夠?qū)崿F(xiàn)這個(gè)功能?

  1. 一個(gè)Rect:需要修改這張圖片上哪個(gè)區(qū)域的像素
  2. 需要被修改的色值區(qū)域:需要把哪個(gè)色值范圍內(nèi)的像素修改為目標(biāo)顏色
  3. 目標(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è)像素的指針。

  1. 我們的空間復(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)位置開始遍歷的刃永。
  2. 首先需要將指針移動(dòng)到初始行的起始列:pCurPtr += (long)(rect.origin.y*imageWidth);,即像素4的所在的位置羊精。目的是為了跳過目標(biāo)區(qū)域上方的無關(guān)行斯够。
  3. 只跳過了上面的無關(guān)行還不夠,我們還需要跳過左邊的無關(guān)列喧锦,即pCurPtr += (long)rect.origin.x;读规,這時(shí)候指針指到了像素5的位置(就是步驟1中所說point(1, 1)的位置),然后我們就可以開始真正的遍歷了燃少。
  4. 在遍歷完這一行的目標(biāo)區(qū)域后束亏,指針指到了像素7的位置;然后還需要跳過右邊的無關(guān)列pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));阵具,這時(shí)候指針指到了像素8的位置碍遍。此時(shí)這一行已經(jīng)完全遍歷結(jié)束,跳到了下一行的起始位置阳液,又回到了步驟3的狀態(tài)(只是row+1了)
  5. 然后重復(fù)執(zhí)行3怕敬、4步驟,直到i >= CGRectGetMaxY(rect)結(jié)束

至此帘皿,這個(gè)算法解釋完畢~

唉~赖捌,這一塊我也是想破頭該怎么描述,可是寫出來發(fā)現(xiàn)還是不太理想。越庇。。
我只能祈禱我太低估讀者的水平奉狈,其實(shí)大家都是能直接看懂代碼的卤唉,根本不需要我解釋ㄟ( ▔, ▔ )ㄏ。
如果大家看完之后還是有不理解的地方仁期;還有一些我沒詳細(xì)解釋的地方桑驱,如果有不理解的,都?xì)g迎在留言區(qū)討論跛蛋。
本人作為寫文章的新手熬的,如果有錯(cuò)誤的地方,也歡迎大家在留言區(qū)指正赊级!

最后押框,這里是Demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市理逊,隨后出現(xiàn)的幾起案子橡伞,更是在濱河造成了極大的恐慌,老刑警劉巖晋被,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兑徘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡羡洛,警方通過查閱死者的電腦和手機(jī)挂脑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欲侮,“玉大人崭闲,你說我怎么就攤上這事⌒怍铮” “怎么了镀脂?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長忘伞。 經(jīng)常有香客問我薄翅,道長,這世上最難降的妖魔是什么氓奈? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任翘魄,我火速辦了婚禮,結(jié)果婚禮上舀奶,老公的妹妹穿的比我還像新娘暑竟。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布但荤。 她就那樣靜靜地躺著罗岖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腹躁。 梳的紋絲不亂的頭發(fā)上桑包,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音纺非,去河邊找鬼哑了。 笑死,一個(gè)胖子當(dāng)著我的面吹牛烧颖,可吹牛的內(nèi)容都是我干的弱左。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼炕淮,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拆火!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鳖悠,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤榜掌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后乘综,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憎账,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年卡辰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胞皱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡九妈,死狀恐怖反砌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情萌朱,我是刑警寧澤宴树,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站晶疼,受9級(jí)特大地震影響酒贬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翠霍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一锭吨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寒匙,春花似錦零如、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祸憋。三九已至,卻和暖如春辕翰,著一層夾襖步出監(jiān)牢的瞬間夺衍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工喜命, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人河劝。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓壁榕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赎瞎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子牌里,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,100評(píng)論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,383評(píng)論 8 265
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評(píng)論 3 119
  • 走過黑色的高中生活务甥,我來到了心心念念的大學(xué)校園牡辽,對(duì)于我的大學(xué)生活,我感慨萬千敞临,所以我決定用文字把它記錄下來态辛。 我是...
    樾樾_7fdb閱讀 303評(píng)論 0 0
  • 最近無意中在閨蜜的微博中看到了“簡書”這個(gè)平臺(tái),于是立馬下載過來挺尿,不為別的奏黑,只想能夠在一個(gè)沒有人認(rèn)識(shí)我的平臺(tái)暢所欲...
    楠木也會(huì)失去方向閱讀 249評(píng)論 2 2