iOS-摳圖:去除圖片中指定範圍顏色的三種方式

Tr2e發表於2017-12-25

實際專案場景:去除圖片的純白色背景圖,獲得一張透明底圖片用於拼圖功能

下面介紹兩種途徑的三種處理方式(不知道為啥想起了孔乙己),具體效能鶸並未對比,如果有大佬能告知,不勝感激。

  • Core Image
  • Core Graphics/Quarz 2D

Core Image

Core Image是一個很強大的框架。它可以讓你簡單地應用各種濾鏡來處理影象,比如修改鮮豔程度,色澤,或者曝光。 它利用GPU(或者CPU)來非常快速、甚至實時地處理影象資料和視訊的幀。並且隱藏了底層圖形處理的所有細節,通過提供的API就能簡單的使用了,無須關心OpenGL或者OpenGL ES是如何充分利用GPU的能力的,也不需要你知道GCD在其中發揮了怎樣的作用,Core Image處理了全部的細節。

Chroma Key Filter

在蘋果官方文件Core Image Programming Guide中,提到了Chroma Key Filter Recipe對於處理背景的範例

其中使用了HSV顏色模型,因為HSV模型,對於顏色範圍的表示,相比RGB更加友好。

大致過程處理過程:

  1. 建立一個對映希望移除顏色值範圍的立方體貼圖cubeMap,將目標顏色的Alpha置為0.0f
  2. 使用CIColorCube濾鏡和cubeMap對源影象進行顏色處理
  3. 獲取到經過CIColorCube處理的Core Image物件CIImage,轉換為Core Graphics中的CGImageRef物件,通過imageWithCGImage:獲取結果圖片

注意:第三步中,不可以直接使用imageWithCIImage:,因為得到的並不是一個標準的UIImage,如果直接拿來用,會出現不顯示的情況。

- (UIImage *)removeColorWithMinHueAngle:(float)minHueAngle maxHueAngle:(float)maxHueAngle image:(UIImage *)originalImage{
    CIImage *image = [CIImage imageWithCGImage:originalImage.CGImage];
    CIContext *context = [CIContext contextWithOptions:nil];// kCIContextUseSoftwareRenderer : CPURender
    /** 注意
     *  UIImage 通過CIimage初始化,得到的並不是一個通過類似CGImage的標準UIImage
     *  所以如果不用context進行渲染處理,是沒辦法正常顯示的
     */
    CIImage *renderBgImage = [self outputImageWithOriginalCIImage:image minHueAngle:minHueAngle maxHueAngle:maxHueAngle];
    CGImageRef renderImg = [context createCGImage:renderBgImage fromRect:image.extent];
    UIImage *renderImage = [UIImage imageWithCGImage:renderImg];
    return renderImage;
}

struct CubeMap {
    int length;
    float dimension;
    float *data;
};

- (CIImage *)outputImageWithOriginalCIImage:(CIImage *)originalImage minHueAngle:(float)minHueAngle maxHueAngle:(float)maxHueAngle{
    
    struct CubeMap map = createCubeMap(minHueAngle, maxHueAngle);
    const unsigned int size = 64;
    // Create memory with the cube data
    NSData *data = [NSData dataWithBytesNoCopy:map.data
                                        length:map.length
                                  freeWhenDone:YES];
    CIFilter *colorCube = [CIFilter filterWithName:@"CIColorCube"];
    [colorCube setValue:@(size) forKey:@"inputCubeDimension"];
    // Set data for cube
    [colorCube setValue:data forKey:@"inputCubeData"];
    
    [colorCube setValue:originalImage forKey:kCIInputImageKey];
    CIImage *result = [colorCube valueForKey:kCIOutputImageKey];
    
    return result;
}

struct CubeMap createCubeMap(float minHueAngle, float maxHueAngle) {
    const unsigned int size = 64;
    struct CubeMap map;
    map.length = size * size * size * sizeof (float) * 4;
    map.dimension = size;
    float *cubeData = (float *)malloc (map.length);
    float rgb[3], hsv[3], *c = cubeData;
    
    for (int z = 0; z < size; z++){
        rgb[2] = ((double)z)/(size-1); // Blue value
        for (int y = 0; y < size; y++){
            rgb[1] = ((double)y)/(size-1); // Green value
            for (int x = 0; x < size; x ++){
                rgb[0] = ((double)x)/(size-1); // Red value
                rgbToHSV(rgb,hsv);
                // Use the hue value to determine which to make transparent
                // The minimum and maximum hue angle depends on
                // the color you want to remove
                float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f: 1.0f;
                // Calculate premultiplied alpha values for the cube
                c[0] = rgb[0] * alpha;
                c[1] = rgb[1] * alpha;
                c[2] = rgb[2] * alpha;
                c[3] = alpha;
                c += 4; // advance our pointer into memory for the next color value
            }
        }
    }
    map.data = cubeData;
    return map;
}
複製程式碼

rgbToHSV在官方文件中並沒有提及,筆者在下文中提到的大佬的部落格中找到了相關轉換處理。感謝

void rgbToHSV(float *rgb, float *hsv) {
    float min, max, delta;
    float r = rgb[0], g = rgb[1], b = rgb[2];
    float *h = hsv, *s = hsv + 1, *v = hsv + 2;
    
    min = fmin(fmin(r, g), b );
    max = fmax(fmax(r, g), b );
    *v = max;
    delta = max - min;
    if( max != 0 )
        *s = delta / max;
    else {
        *s = 0;
        *h = -1;
        return;
    }
    if( r == max )
        *h = ( g - b ) / delta;
    else if( g == max )
        *h = 2 + ( b - r ) / delta;
    else
        *h = 4 + ( r - g ) / delta;
    *h *= 60;
    if( *h < 0 )
        *h += 360;
}
複製程式碼

接下來我們試一下,去除綠色背景的效果如何

iOS-摳圖:去除圖片中指定範圍顏色的三種方式

我們可以通過使用HSV工具,確定綠色HUE值的大概範圍為50-170

呼叫一下方法試一下

[[SPImageChromaFilterManager sharedManager] removeColorWithMinHueAngle:50 maxHueAngle:170 image:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"nb" ofType:@"jpeg"]]]
複製程式碼

效果

iOS-摳圖:去除圖片中指定範圍顏色的三種方式

效果還可以的樣子。

如果認真觀察HSV模型的同學也許會發現,我們通過指定色調角度(Hue)的方式,對於指定灰白黑顯得無能為力。我們不得不去用飽和度(Saturation)和明度(Value)去共同判斷,感興趣的同學可以在程式碼中判斷Alphafloat alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f: 1.0f;那裡試一下效果。(至於程式碼中為啥RGB和HSV這麼轉換,請百度他們的轉換,因為鶸筆者也不懂。哎,鶸不聊生)

對於Core Image感興趣的同學,請移步大佬的系列文章

iOS8 Core Image In Swift:自動改善影象以及內建濾鏡的使用 iOS8 Core Image In Swift:更復雜的濾鏡 iOS8 Core Image In Swift:人臉檢測以及馬賽克 iOS8 Core Image In Swift:視訊實時濾鏡

Core Graphics/Quarz 2D

上文中提到的基於OpenGlCore Image顯然功能十分強大,作為檢視另一基石的Core Graphics同樣強大。對他的探究,讓鶸筆者更多的瞭解到圖片的相關知識。所以在此處總結,供日後查閱。

如果對探究不感興趣的同學,請直接跳到文章最後 Masking an Image with Color 部分

Bitmap

侵刪
Quarz 2D官方文件中,對於BitMap有如下描述

A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.

32-bit and 16-bit pixel formats for CMYK and RGB color spaces in Quartz 2D

32-bit and 16-bit pixel formats for CMYK and RGB color spaces in Quartz 2D

回到我們的需求,對於去除圖片中的指定顏色,如果我們能夠讀取到每個畫素上的RGBA資訊,分別判斷他們的值,如果符合目標範圍,我們將他的Alpha值改為0,然後輸出成新的圖片,那麼我們就實現了類似上文中cubeMap的處理方式。

強大的Quarz 2D為我們提供了實現這種操作的能力,下面請看程式碼示例:

- (UIImage *)removeColorWithMaxR:(float)maxR minR:(float)minR maxG:(float)maxG minG:(float)minG maxB:(float)maxB minB:(float)minB image:(UIImage *)image{
    // 分配記憶體
    const int imageWidth = image.size.width;
    const int imageHeight = image.size.height;
    size_t bytesPerRow = imageWidth * 4;
    uint32_t* rgbImageBuf = (uint32_t*)malloc(bytesPerRow * imageHeight);
    
    // 建立context
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();// 色彩範圍的容器
    CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
    CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image.CGImage);
    
    
    // 遍歷畫素
    int pixelNum = imageWidth * imageHeight;
    uint32_t* pCurPtr = rgbImageBuf;
    for (int i = 0; i < pixelNum; i++, pCurPtr++)
    {
        uint8_t* ptr = (uint8_t*)pCurPtr;
        if (ptr[3] >= minR && ptr[3] <= maxR &&
            ptr[2] >= minG && ptr[2] <= maxG &&
            ptr[1] >= minB && ptr[1] <= maxB) {
            ptr[0] = 0;
        }else{
            printf("\n---->ptr0:%d ptr1:%d ptr2:%d ptr3:%d<----\n",ptr[0],ptr[1],ptr[2],ptr[3]);
        }
    }
    // 將記憶體轉成image
    CGDataProviderRef dataProvider =CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight, nil);
    CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight,8, 32, bytesPerRow, colorSpace,kCGImageAlphaLast |kCGBitmapByteOrder32Little, dataProvider,NULL,true,kCGRenderingIntentDefault);
    CGDataProviderRelease(dataProvider);
    UIImage* resultUIImage = [UIImage imageWithCGImage:imageRef];
    
    // 釋放
    CGImageRelease(imageRef);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    return resultUIImage;
}
複製程式碼

還記得我們在Core Image中提到的HSV模式的弊端嗎?那麼Quarz 2D則是直接利用RGBA的資訊進行處理,很好的規避了對黑白色不友好的問題,我們只需要設定一下RGB的範圍即可(因為黑白色在RGB顏色模式中,很好確定),我們可以大致封裝一下。如下

- (UIImage *)removeWhiteColorWithImage:(UIImage *)image{
    return [self removeColorWithMaxR:255 minR:250 maxG:255 minG:240 maxB:255 minB:240 image:image];
}
複製程式碼
- (UIImage *)removeBlackColorWithImage:(UIImage *)image{
    return [self removeColorWithMaxR:15 minR:0 maxG:15 minG:0 maxB:15 minB:0 image:image];
}
複製程式碼

看一下我們對於白色背景的處理效果對比

iOS-摳圖:去除圖片中指定範圍顏色的三種方式

看起來似乎還不錯,但是對於紗質的衣服,就顯得很不友好。看一下筆者做的幾組圖片的測試

iOS-摳圖:去除圖片中指定範圍顏色的三種方式

很顯然,如果不是白色背景,“衣衫襤褸”的效果非常明顯。這個問題,在筆者嘗試的三種方法中,無一倖免,如果哪位大佬知道好的處理方法,而且能告訴鶸,將不勝感激。(先放倆膝蓋在這兒)

除了上述問題外,這種對比每個畫素的方法,讀取出來的數值會同作圖時出現誤差。但是這種誤差肉眼基本不可見。

iOS-摳圖:去除圖片中指定範圍顏色的三種方式
如下圖中,我們作圖時,設定的RGB值分別為100/240/220 但是通過CG上述處理時,讀取出來的值則為92/241/220。對比圖中的“新的”“當前”,基本看不出色差。這點小問題各位知道就好,對實際去色效果影響並不大
iOS-摳圖:去除圖片中指定範圍顏色的三種方式

Masking an Image with Color

筆者嘗試過理解並使用上一種方法後,在重讀文件時發現了這個方法,簡直就像是發現了Father Apple的恩賜。直接上程式碼

- (UIImage *)removeColorWithMaxR:(float)maxR minR:(float)minR maxG:(float)maxG minG:(float)minG maxB:(float)maxB minB:(float)minB image:(UIImage *)image{

    const CGFloat myMaskingColors[6] = {minR, maxR,  minG, maxG, minB, maxB};
    CGImageRef ref = CGImageCreateWithMaskingColors(image.CGImage, myMaskingColors);
    return [UIImage imageWithCGImage:ref];
    
}
複製程式碼

官方文件點這兒

總結

HSV顏色模式相對於RGB模式而言,更利於我們摳除圖片中的彩色,而RGB則正好相反。筆者因為專案中,只需要去除白色背景,所以最終採用了最後一種方式。

相關文章