ビットマップを操作して画像をモノトーンに変換する : Objective-C プログラミング

PROGRAM


ビットマップを操作して画像をモノトーンに変換する

iPhone アプリで使うボタン画像を 1 色だけ用意して、シーンに合わせて色を変えて使う方法はないかと調べていたところ、CIFilter で画像をモノトーンに変換する 方法があることを知りました。

ただ、この方法だと黒色で作成した画像に着色できなかったため フラグメントシェーダーで画像をモノトーンに変換する 方法を試してイメージ通りに変換することができたのですけど、@kamiyan さんに見せたところ、フラグメントシェーダーの使い方としては間違ってないが CPU での処理でも十分実用的なレベルとのことでした。

そこで、今回は Core Graphics の機能を使ってビットマップをモノトーン化してみることにしました。

Core Graphics には CGImageGetDataProvider のような関数が用意されているので、これを使って CGImage データを CFData に変換してピクセルデータを直接操作することができます。

また CGImage データは CGImageGetWidth や CGImageGetBitsPerComponent などの関数を使って画像の構成情報を取得することができます。本来であればこれらの値に厳密に操作しないといけないのでしょうけど、今回は 8 bit RGBA カラーの画像を期待したプログラムに偏った作りになっていると思います。

// image を monochromeColor で単調化した画像を返します。

- (UIImage*)monochromeImageWithImage:(UIImage*)image monochromeColor:(UIColor*)monochromeColor

{

UIImage* monochromeImage;

 

// UIColor から red, green, blue, alpha を取得しています。UIColor の用意の仕方によっては取得に失敗することがあります。

CGFloat monochromeRed;

CGFloat monochromeGreen;

CGFloat monochromeBlue;

CGFloat monochromeAlpha;

 

[monochromeColor getRed:&monochromeRed green:&monochromeGreen blue:&monochromeBlue alpha:&monochromeAlpha];

 

// UIImage を CGImage に変換して、画像情報を取得します。

CGImageRef imageRef = image.CGImage;

 

size_t imageWidth = CGImageGetWidth(imageRef);

size_t imageHeight = CGImageGetHeight(imageRef);

size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);

size_t bitsPerPixcel = CGImageGetBitsPerPixel(imageRef);

size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);

CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);

CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

bool shouldInterpolate = CGImageGetShouldInterpolate(imageRef);

CGColorRenderingIntent renderingIntent = CGImageGetRenderingIntent(imageRef);

 

// CGImage をビットマップデータに変換します。

CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);

CFDataRef data = CGDataProviderCopyData(dataProvider);

 

Byte* buffer = (Byte*)CFDataGetBytePtr(data);

size_t bytesPerPixcel = bitsPerPixcel / 8;

 

// 色を単調化します。

float coefficientRed = 1.0f - monochromeRed;

float coefficientGreen = 1.0f - monochromeGreen;

float coefficientBlue = 1.0f - monochromeBlue;

float coefficientAlpha = monochromeAlpha;

 

// 縦方向を先頭ピクセルから imageHeight 個のピクセルを繰り返します。

for (NSInteger y = 0; y < imageHeight; y++)

{

// 横方向を先頭ピクセルから imageWidth 個のピクセルを繰り返します。

for (NSInteger x = 0; x < imageWidth; x++)

{

// ピクセルひとつひとつを単調化するために、もとのピクセルデータ (RGBA) を取得します。

Byte* row = buffer + (y * bytesPerRow) + (x * bytesPerPixcel);

 

Byte* byteRed = row + 0;

Byte* byteGreen = row + 1;

Byte* byteBlue = row + 2;

Byte* byteAlpha = row + 3;

 

// 8 bit 要素は 0 から 255 なので、それを 0.0 から 1.0 に変換します。

float red = (float)*byteRed / 255.0f;

float green = (float)*byteGreen / 255.0f;

float blue = (float)*byteBlue / 255.0f;

float alpha = (float)*byteAlpha / 255.0f;

 

// 色を単調化します。

float brightness = MAX(MAX(red, green), blue);

float level = powf(brightness, 3.0f * intensity);

 

red = monochromeRed + level * coefficientRed;

green = monochromeGreen + level * coefficientGreen;

blue = monochromeBlue + level * coefficientBlue;

alpha = alpha * coefficientAlpha;

 

// 単調化後の色を、念のため 0.0 から 1.0 の範囲に収まるように丸めます。

red = MIN(MAX(0.0, red), 1.0);

green = MIN(MAX(0.0, green), 1.0);

blue = MIN(MAX(0.0, blue), 1.0);

alpha = MIN(MAX(0.0, alpha), 1.0);

 

// 最後に、色を 0 から 255 までの 8 bit 値にしてメモリに書き戻します。

*byteRed = (Byte)(255.0f * red);

*byteGreen = (Byte)(255.0f * green);

*byteBlue = (Byte)(255.0f * blue);

*byteAlpha = (Byte)(255.0f * alpha);

}

}

 

// 単調化したデータから CGImage を生成します。このとき kCGImageAlphaLast を指定しないと透明度が考慮されないようでした。

CFDataRef resultData = CFDataCreate(NULL, buffer, CFDataGetLength(data));

CGDataProviderRef resultDataProvider = CGDataProviderCreateWithCFData(resultData);

CGImageRef resultImageRef = CGImageCreate(imageWidth, imageHeight, bitsPerComponent, bitsPerPixcel, bytesPerRow, colorSpace, bitmapInfo | kCGImageAlphaLast, resultDataProvider, NULL, shouldInterpolate, renderingIntent);

 

CGDataProviderRelease(resultDataProvider);

CFRelease(resultData);

CFRelease(data);

 

// CGImage を UIImage に変換します。

monochromeImage = [[UIImage alloc] initWithCGImage:resultImageRef scale:image.scale orientation:image.imageOrientation];

 

CGImageRelease(resultImageRef);

 

// これで、単調化された画像を UIImage で取得することができました。

return monochromeImage;

}

ここで実行している単調化プログラムは、フラグメントシェーダーで画像をモノトーンに変換する 方法で実装したときのフラグメントシェーダープログラムで書いた変換と同じです。

今回のプログラムではそういったピクセルデータの編集よりも、Core Graphics での画像データの取得やデータから画像を生成するところが少し難しく思えました。

 

要所としては、CGImage のデータを取得するために CGImageGetDataProvider という関数を使うところでしょうか。

ピクセルデータは 8 bit RGBA の画像であれば、先頭から順に R, G, B, A と 1 バイトずつのデータで格納されているようで、取り出すことは簡単です。

ハマったのは、編集し終わった画像データを CGImage に変換する CGImageCreate 関数のところで、ここのビットマップ情報として、もとの画像から取得した bitmapInfo を渡しただけでは alpha の値が考慮されない画像が生成されるところでした。

取得した bitmapInfo に kCGImageAlphaLast を追加して、最後の要素が不透明度であることを明示することで、透明度が加味された画像を生成することができました。

 

また、UIColor の RGBA 要素を取得する getRed:green:blue:alpha メソッドも少し注意が必要です。

UIColor から RGB を取得する でも記した通り、このメソッドでは色の値を取得できないことがあります。取得できる UIColor を使うことにするか、CGColorGetComponents 関数を使って色の要素を自分で取り出すようにします。

[ もどる ]