1. 程式人生 > >iOS修改圖片顏色(修改畫素色值)

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


轉自:https://www.jianshu.com/p/619ef8423895 

假設這樣一個場景:一張圖片中有一朵白花,我們想要把它變成紅花;或者一張圖片中有一段黑色的文字,我們想要把它變成紅色,應該怎麼做?

想要實現這個需求,就需要從畫素尺度上對圖片進行修改,將指定區域內的畫素的色值改為我們需要的顏色。但是,如何從這張圖上找到那段文字或者那朵花,並不在本文的討論範圍內,那是OCR和機器學期的事ㄟ( ▔, ▔ )ㄏ。

進入正題

假設我們要把一張有一段黑色文字的圖片中的文字修改為紅色:

示例圖片

要實現這個需求,我們應該怎麼做?

  1. 建立一個畫布,並將原始圖片平鋪在畫布上
  2. 遍歷圖片上的畫素,找到目標區域內的黑色文字的畫素,將它改為紅色
  3. 輸出修改後的圖片,並清理記憶體

我們需要哪些資訊才足夠實現這個功能?

  1. 一個Rect:需要修改這張圖片上哪個區域的畫素
  2. 需要被修改的色值區域:需要把哪個色值範圍內的畫素修改為目標顏色
  3. 目標顏色:需要將符合上述兩點的畫素修改為什麼顏色

具體實現

在貼程式碼之前,先講一些廢話:

  • 圖片的解析度代表著它的畫素個數,比如上圖的解析度為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)內的畫素色值。下面所講要學會自動腦補二維圖片轉換一維二進位制資料,凡是指出座標的都是二維圖片,而說指標的都是在說一維的二進位制資料中某個畫素的指標。

  1. 我們的空間複雜度為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)位置開始遍歷的。
  2. 首先需要將指標移動到初始行的起始列:pCurPtr += (long)(rect.origin.y*imageWidth);,即畫素4的所在的位置。目的是為了跳過目標區域上方的無關行。
  3. 只跳過了上面的無關行還不夠,我們還需要跳過左邊的無關列,即pCurPtr += (long)rect.origin.x;,這時候指標指到了畫素5的位置(就是步驟1中所說point(1, 1)的位置),然後我們就可以開始真正的遍歷了。
  4. 在遍歷完這一行的目標區域後,指標指到了畫素7的位置;然後還需要跳過右邊的無關列pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));,這時候指標指到了畫素8的位置。此時這一行已經完全遍歷結束,跳到了下一行的起始位置,又回到了步驟3的狀態(只是row+1了)
  5. 然後重複執行3、4步驟,直到i >= CGRectGetMaxY(rect)結束

至此,這個演算法解釋完畢~

唉~,這一塊我也是想破頭該怎麼描述,可是寫出來發現還是不太理想。。。
我只能祈禱我太低估讀者的水平,其實大家都是能直接看懂程式碼的,根本不需要我解釋ㄟ( ▔, ▔ )ㄏ。
如果大家看完之後還是有不理解的地方;還有一些我沒詳細解釋的地方,如果有不理解的,都歡迎在留言區討論。
本人作為寫文章的新手,如果有錯誤的地方,也歡迎大家在留言區指正!

最後,這裡是Demo地址