English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

iOSで画像の背景色を取り除く方法

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

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

Core Image Core Graphics/Quarz 2D Core Image

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

在苹果官方文档Core Image Programming Guide中,提到了Chroma Key Filter Recipe对于处理背景的范例

其中使用了HSV颜色模型,因为HSV模型,对于颜色范围的表示,相比RGB更加友好。

大致过程处理过程:

创建一个映射希望移除颜色值范围的立方体贴图cubeMap,将目标颜色的Alpha置为0.0f 使用CIColorCube滤镜和cubeMap对源图像进行颜色处理获取到经过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;
 // メモリをキューブデータで作成
 NSData *data = [NSData dataWithBytesNoCopy:map.data
   length:map.length
   freeWhenDone:YES];
 CIFilter *colorCube = [CIFilter filterWithName:@"CIColorCube"];
 [colorCube setValue:@(size) forKey:@"inputCubeDimension"];
 // 为立方体设置数据
 [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); // 蓝色值
 for (int y = 0; y < size; y++{
 rgb[1] = ((double)y)/(size-1); // 绿色值
 for (int x = 0; x < size; x ++{
 rgb[0] = ((double)x)/(size-1); // 红色值
 rgbToHSV(rgb,hsv);
 // 使用色调值来确定要使其透明的部分
 // 最小和最大色调角度取决于
 // 您想要移除的颜色
 float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle)63; 0.0f: 1.0f;
 // 立方体的预乘alpha值进行计算
 c[0] = rgb[0] * alpha;
 c[1] = rgb[1] * alpha;
 c[2] = rgb[2] * alpha;
 c[3] = alpha;
 c += 4; // メモリ内の次の色値にポインタを進めます
 }
 }
 }
 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)を一緒に判断する必要があります。興味がある人はコードを確認してください。Alpha float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) &63; 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

前述のOpenGlに基づくCore Imageは機能が非常に強力です。ビューのもう一つの基盤であるCore Graphics同様に強力です。その探求は、鶸の筆者が画像に関する多くの知識を深めることになりました。したがって、今後の参照のためにここで要約します。

興味がない方には、最後に「画像を色でマスクする」部分に飛んでください

Bitmap


Quarz 2Dの公式ドキュメントでは、BitMapは以下のように説明されています

ビットマップ画像(またはサンプリング画像)はピクセル(またはサンプル)の配列です。各ピクセルは画像の単一のポイントを表します。JPEG、TIFF、PNGグラフィックファイルはビットマップ画像の例です。

32-ビットおよび 16-QuartzのCMYKおよびRGB色空間のためのビットピクセル形式 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];
}

白色背景の処理効果の比較を見てみましょう

見た目は良いかもしれませんが、織物の衣服に対してはあまり良い効果ではありません。筆者が作成したいくつかの画像のテストを見てみましょう

明らかに、白い背景でない場合、「衣衫褴褛」の効果は非常に明瞭です。この問題は、筆者が試した3つの方法すべてで避けられませんでした。もし、良い処理方法を知っている方で、鶸に教えてくれるのであれば、大変感謝します。(まずは膝を2つここに置きます)

上記の問題に加えて、各ピクセルを比較するこの方法で読み取られた値は、描画時と誤差が生じます。しかし、この誤差は肉眼ではほとんど見えません。


以下の図のように、描画時に設定したRGB値は100/240/220 しかし、CGで上記の処理を行った場合、読み取られた値は92/241/220。画像の「新しい」「現在」の比較では、色の違いはほとんど見られません。この小さな問題は、皆さんに知っておくだけで十分です。実際の去色効果に大きな影響はありません。

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 はその逆です。このプロジェクトでは、白色の背景を取り除く必要があるので、最終的には最後の方法を選択しました。

おすすめ