iOS-抠图:去除图片中指定范围内颜色的三种方式

实际项目场景:去除图片的纯白色背景图,获得一张透明底图片用于拼图功能

介绍两种途径的三种处理方式(不知道为啥想起了孔乙己),具体性能鶸并未对比,如果有大佬能告知,不胜感激。

  • Core Image
  • Core Graphics/Quarz 2D

Core Image

Core Image是一个很强大的框架。它可以让你简单地应用各种滤镜来处理图像,比如修改鲜艳程度,色泽,或者曝光。 它利用GPU(或者CPU)来非常快速、甚至实时地处理图像数据和视频的帧。并且隐藏了底层图形处理的所有细节,通过提供的API就能简单的使用了,无须关心OpenGL或者OpenGL ES是如何充分利用GPU的能力的,也不需要你知道GCD在其中发挥了怎样的作用,Core Image处理了全部的细节。

chroma_key

在苹果官方文档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;
}

接下来我们试一下,去除绿色背景的效果如何


苍老师

我们可以通过使用HSV工具,确定绿色HUE值的大概范围为50-170

调用一下方法试一下

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

效果

效果

效果还可以的样子。

如果认真观察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

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];
}

看一下我们对于白色背景的处理效果对比

看起来似乎还不错,但是对于纱质的衣服,就显得很不友好。看一下笔者做的几组图片的测试

很显然,如果不是白色背景,“衣衫褴褛”的效果非常明显。这个问题,在笔者尝试的三种方法中,无一幸免,如果哪位大佬知道好的处理方法,而且能告诉鶸,将不胜感激。(先放俩膝盖在这儿)

除了上述问题外,这种对比每个像素的方法,读取出来的数值会同作图时出现误差。但是这种误差肉眼基本不可见。

如下图中,我们作图时,设置的RGB值分别为100/240/220 但是通过CG上述处理时,读取出来的值则为92/241/220。对比图中的“新的”“当前”,基本看不出色差。这点小问题各位知道就好,对实际去色效果影响并不大

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则正好相反。笔者因为项目中,只需要去除白色背景,所以最终采用了最后一种方式。

掘金
博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容