猫でもわかるiPhoneで画像にフィルターをかける方法

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 猫でもわかるiPhoneで画像にフィルターをかける方法
このエントリーをはてなブックマークに追加
こんにちは!
ノンアルコールビールはビールと認めない派、gaoohです。

今回はiPhoneで簡単に画像にフィルターをかける方法を紹介します。 いわゆるカメラアプリによく搭載されている、撮った写真を白黒にしたりセピアにしたりする例のアレです。

基礎知識


まずは、そもそも画像を変化させるというのはどういうこと?を理解しなければなりません。
このあたりはデザイナーの人のほうが詳しかったりするので、もよりのデザイナーに聞くのが一番ですが、ざっくり説明すると色は 赤(A) 、緑(G)、青(B)、で表現することができます。
いわゆるRGB値というもので、HTMLやCSSなどでよくみかけるおなじみのものですね。
これに透過を表すアルファチャンネル(A)を加えてRGBAというものもあります。

さらに色相(hun)、彩度 (saturation)、明度(Value)、による色の表し方をHSVといいます。
さらにさらに、輝度(Y)、輝度と青色成分の差(U)、輝度と赤色成分の差(V)、による色を表す形式としてYUVというものがあります。 これらは表現方法の違いなので、RGBからHSVの値を求めたり、YUVからRGBの値を求めたりもできます。

もうやたら略語がでてきておなかいっぱいですが、ようは風景写真だろうが、子供の写真であろうが、結局のところ1枚の画像は1pxずつRGB値が存在しているわけで、それを一つずつ変化させてあげれば、白黒だったりセピアだったりに変化するわけです。

画像を白黒にしてみる


そもそも画像を白黒にするということは、理論上、RGBのそれぞれの値に差をなくして、明るさの差だけにしてしまえばいいということになります。
明るさである、輝度 y は以下の式で計算できます。

y = (77 * r + 28 * g + 151 * b) / 256;
このへんは公式なのでそういうものと理解してしまうのが楽です。 あとは計算結果の輝度yをRGBそれぞれにあてはめるだけです。具体的にコードになると以下のようになります。

+ (UIImage*) grayscale:(UIImage*) anImage {
    CGImageRef  imageRef;
    cgImage = anImage.CGImage;
    
    size_t width  = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);

    // ピクセルを構成するRGB各要素が何ビットで構成されている
    size_t                  bitsPerComponent;
    bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
    
    // ピクセル全体は何ビットで構成されているか
    size_t                  bitsPerPixel;
    bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
    
    // 画像の横1ライン分のデータが、何バイトで構成されているか
    size_t                  bytesPerRow;
    bytesPerRow = CGImageGetBytesPerRow(imageRef);
    
    // 画像の色空間
    CGColorSpaceRef         colorSpace;
    colorSpace = CGImageGetColorSpace(imageRef);
    
    // 画像のBitmap情報
    CGBitmapInfo            bitmapInfo;
    bitmapInfo = CGImageGetBitmapInfo(imageRef);
    
    // 画像がピクセル間の補完をしているか
    bool                    shouldInterpolate;
    shouldInterpolate = CGImageGetShouldInterpolate(imageRef);
    
  // 表示装置によって補正をしているか
    CGColorRenderingIntent  intent;
    intent = CGImageGetRenderingIntent(imageRef);
    
    // 画像のデータプロバイダを取得する
    CGDataProviderRef   dataProvider;
    dataProvider = CGImageGetDataProvider(imageRef);
    
    // データプロバイダから画像のbitmap生データ取得
    CFDataRef   data;
    UInt8*      buffer;
    data = CGDataProviderCopyData(dataProvider);
    buffer = (UInt8*)CFDataGetBytePtr(data);
    
    // 1ピクセルずつ画像を処理
    NSUInteger  x, y;
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            UInt8*  tmp;
            tmp = buffer + y * bytesPerRow + x * 4; // RGBAの4つ値をもっているので、1ピクセルごとに*4してずらす
            
            // RGB値を取得
            UInt8 red,green,blue;
            red = *(tmp + 0);
            green = *(tmp + 1);
            blue = *(tmp + 2);
            
      // 輝度計算
            UInt8 brightness;
            brightness = (77 * red + 28 * green + 151 * blue) / 256;
            
            *(tmp + 0) = brightness;
            *(tmp + 1) = brightness;
            *(tmp + 2) = brightness;
        }
    }
    
    // 効果を与えたデータ生成
    CFDataRef   effectedData;
    effectedData = CFDataCreate(NULL, buffer, CFDataGetLength(data));
    
    // 効果を与えたデータプロバイダを生成
    CGDataProviderRef   effectedDataProvider;
    effectedDataProvider = CGDataProviderCreateWithCFData(effectedData);
    
    // 画像を生成
    CGImageRef  effectedCgImage;
    UIImage*    effectedImage;
    effectedCgImage = CGImageCreate(
									width, height, 
									bitsPerComponent, bitsPerPixel, bytesPerRow, 
									colorSpace, bitmapInfo, effectedDataProvider, 
									NULL, shouldInterpolate, intent);
    effectedImage = [[UIImage alloc] initWithCGImage:effectedCgImage];
    
    // データの解放
    CGImageRelease(effectedCgImage);
    CFRelease(effectedDataProvider);
    CFRelease(effectedData);
    CFRelease(data);
	
    return effectedImage;

画像をセピアにしてみる


次はセピアです。セピアって元々イカ墨を原料にした顔料の名前らしいですね。個人的にはイカスミスパゲッティなんかよりイカ腸焼きが好きです。

セピアもモノクロと実はあまり変わっていなくて、セピアという色と、あとは色の明るさに変換してあげればいいだけです。
セピアの色というのはきちんと定義されていてRGBでいうと R = 107, G = 74 B = 43 という値です。
Rを100%とするとGは約70%、Bは約40%なのでその割合を保持していればセピア調になっちゃうわけですね。

コードはほぼ白黒と同じで画像処理の部分だけ以下のようにしてあげればOKです。

    NSUInteger  x, y;
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            UInt8*  tmp;
            tmp = buffer + y * bytesPerRow + x * 4; // RGBAの4つ値をもっているので、1ピクセルごとに*4してずらす
            
            // RGB値を取得
            UInt8 red,green,blue;
            red = *(tmp + 0);
            green = *(tmp + 1);
            blue = *(tmp + 2);

            // 輝度計算
            UInt8 brightness;
            brightness = (77 * red + 28 * green + 151 * blue) / 256;
            
            *(tmp + 0) = brightness;
            *(tmp + 1) = brightness * 0.7;
            *(tmp + 2) = brightness * 0.4;
        }
    }

もっと高度な画像変換


はい、ここまでできると「画像をトイカメラ風にしたい!」とか「モザイクかけたい!」とかいろいろと欲がでてくるわけです。

基本的に考え方はモノクロやセピアと同じように1pxずつそれにあった計算式があるので、変換していけばいいのですが……高度な処理になればなるほど当然計算はややこしくなります。それはそれで必要なのですが、場合によってはもうちょっと楽をしたい、そんなときにはImageMagickを使うという手があります。

Webサービスなどでは画像のリサイズなんかでけっこう当たり前につかっているImageMagick。かくいう私もiPhoneアプリを作り始める前は頼りっきりで、中で何をしているかなんてまったく気にせず使ってました。これをiPhone上でも使える用にコンパイルしたものがあり、それを使うとお手軽に変換できます。

http://www.cloudgoessocial.net/2010/02/10/imagemagick-for-iphone-via-snowleopard/

導入方法


自分でコンパイルすることも可能ですが、配布元でサンプルプロジェクトが用意されており、そこからまるっとコピーしてとりあえず使ってみることもできます。

まず、ライブラリであるIMディレクトリをコピーし、Frameworks以下に既存ファイルの追加でプロジェクトに組み込みます。

2011-02-22_1148

次にコンパイル時のリンカフラグに以下のものを追加します。

-lMagickCore -lMagickWand -lbz2 -lz -ljpeg -lpng

2011-02-22_1140

最後に「ヘッダ検索パス」と「ライブラリ検索パス」に "$(SRCROOT)/" を追加します。「再帰的」にチェックも入れてください。

2011-02-22_1153

使い方はサンプルプロジェクトを参考にするのが一番はやいです。 サンプルプロジェクトには一部の使い方しか載っていませんが、当然いろいろできます。

月夜風


 // factor 偏移係数。1.5が標準。
 MagickBooleanType MagickBlueShiftImage(
     MagickWand* wand,
     const double factor
 )

Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 17 13

油絵風


 // radius 半径(単位:ピクセル数)。0を指定すると適切な半径を選択します。 
MagickBooleanType MagickOilPaintImage(
    MagickWand* wand,
    const double radius
)


Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 16 28

スケッチ風


// radius ガウス演算用の中心を含まない半径(単位:ピクセル数)。0を指定すると適切な半径を選択します。
// sigma ガウス演算用の標準偏差(単位:ピクセル数)。
// angle モーションブラーの角度。
MagickBooleanType MagickSketchImage(
    MagickWand* wand,
    const double radius,
    const double sigma,
    const double angle
)

Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 14 54

細かい調整値はほぼデフォルトを使っているので、その調整次第でいろいろ変化しますので試してみてください。

トイカメラ風


ImageMagick応用編です。さすがにトイカメラ風だと直で変換できるような関数はないので、既存のものを、いろいろと組み合わせます。

トイカメラ風の画像にするにはいろいろ手法はありますが、とりあえずコントラストをあげて、彩度をあげます。

コード的には以下のようになります。
※エラー処理や細かいことなどは考慮していないので、そのへんは適時がんばって下さい。

CGImageRef createStandardImage(CGImageRef image) {
	const size_t width = CGImageGetWidth(image);
	const size_t height = CGImageGetHeight(image);
	CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
	CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8, 4*width, space,
											 kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedFirst);
	CGColorSpaceRelease(space);
	CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), image);
	CGImageRef dstImage = CGBitmapContextCreateImage(ctx);
	CGContextRelease(ctx);
	return dstImage;
}

+ (UIImage*) imagemagick:(UIImage*) anImage {
    CGImageRef  imageRef;
    imageRef = anImage.CGImage;
	
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
 
    // 画像データをNSDataに変換
    const char *map = "ARGB"; // カラーモード
    const StorageType inputStorage = CharPixel;
    CGImageRef standardized = createStandardImage(imageRef);
   
    NSData *srcData = (NSData *) CGDataProviderCopyData(CGImageGetDataProvider(standardized));
    CGImageRelease(standardized);
    const void *bytes = [srcData bytes];

    MagickWandGenesis();
    MagickWand *magick_wand_local= NewMagickWand();

    // 画像NSDataの情報をmagick_wand_localに挿入
    MagickConstituteImage(magick_wand_local, width, height, map, inputStorage, bytes);

  // コントラストを上げる
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    // 明度、彩度、色相をそれぞれ現在を100とした場合として調整
    MagickModulateImage(magick_wand_local, 100, 250, 100);

    // magick_wand_localを元に画像を生成
    const int bitmapBytesPerRow = (width * strlen(map));
    const int bitmapByteCount = (bitmapBytesPerRow * height);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    char *trgt_image = malloc(bitmapByteCount);
    MagickExportImagePixels(magick_wand_local, 0, 0, width, height, map, CharPixel, trgt_image);
    magick_wand_local = DestroyMagickWand(magick_wand_local);
    MagickWandTerminus();
    
    CGContextRef context = CGBitmapContextCreate (trgt_image,
												  width,
												  height,
												  8, // bits per component
												  bitmapBytesPerRow,
												  colorSpace,
												  kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorSpace);
    CGImageRef cgimage = CGBitmapContextCreateImage(context);
    UIImage *image = [[UIImage alloc] initWithCGImage:cgimage];
    CGImageRelease(cgimage);
    CGContextRelease(context);
    [srcData release];
    free(trgt_image);
    return image;
} 

Before:
Photo 2月 22, 12 21 38
After1:
Photo 2月 22, 12 45 37

ちょっとそれっぽくなりましたね。なんか三毛部分が強化された気がしますが。

よりらしくするために画像の四隅をちょっと暗くしてみます。
コントラストと再度の調整の後に以下のように付け加えます。

 // コントラストを上げる
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    // 明度、再度、色相をそれぞれ現在を100とした場合として調整
    MagickModulateImage(magick_wand_local, 100, 250, 100);

    PixelWand *color = NewPixelWand();
    PixelSetColor(color, "transparent");

    // 画像と同じ大きさで黒い楕円形を描く
    MagickWand * black_frame_wand_local= NewMagickWand();
    MagickNewImage(black_frame_wand_local, width, height, color);
    DrawingWand *draw_wand = NewDrawingWand();
    DrawEllipse(draw_wand, width/2, height/2, width/2-10, height/2-10, 0, 360);
    MagickDrawImage(black_frame_wand_local, draw_wand);
    
    // 黒い楕円形画像にぼかしを入れる
    MagickBlurImageChannel(black_frame_wand_local, AllChannels, 0, 30);

    // 元画像と合成して、黒い楕円形画像をmaskとした画像を生成
  MagickCompositeImage(black_frame_wand_local, magick_wand_local, InCompositeOp, 0, 0);

    PixelWand *pw1 = NewPixelWand();
    PixelSetColor(pw1, "#00ff00");
    PixelWand *pw2 = NewPixelWand();
    PixelSetColor(pw2, "#ff0000");
    MagickTintImage(magick_wand_local, pw1, pw2);
	
    MagickCompositeImage(magick_wand_local, black_frame_wand_local, OverCompositeOp, 0, 0);

After2:
Photo 2月 22, 14 17 43
こんな感じです。とりあえずやってみた感じなので、パラメータの調整などで、よりそれっぽくなると思います。

パフォーマンス


とてもお手軽ですが、iPhone上で使うとなるとやはりパフォーマンスが気になります。以下が一番シンプルに同じ画像をセピア化した場合の処理時間です。

※テストに使った画像は600x800のものです。

シュミレーターでの値

imagemagickのMagickSepiaToneImageを利用
0.293929 sec
imagemagickを利用せず単純計算
0.003878 sec

iPhone3Gでの値

imagemagickのMagickSepiaToneImageを利用
1.975130 sec
imagemagickを利用せず単純計算
0.191766 sec

iPhone4での値

imagemagickのMagickSepiaToneImageを利用
0.412554 sec
imagemagickを利用せず単純計算
0.008401 sec

端末の状態や画像によっても値は上下するので参考程度ですが、単純な変換だとやはり生の計算にはかなわない部分はあります。
ただユーザに時間がかかっていると感じさせないUIでカバーするとか、方法はいろいろあるかなと思います。

なお、ライブラリをごそっと追加すると最終的なバイナリサイズで 5、6 M が増えることになります。iPhoneアプリは 20Mを超えると3G回線でのダウンロードはできなくなり、wifiやPC経由でのインストールしかできなくなるのでその点も気をつけたいところです。

ではでは駆け足で画像変換について説明しましたが、カメラアプリはiPhoneアプリの中でも大激戦地区。だからこそみなさんいろいろ工夫している分野なので、だいぶ奥が深い!だからこそいろいろおもしろいので、ちょっと週末あそんでみると楽しいですニャ!
レスポンス

このエントリーをはてなブックマークに追加