きたくち

はい。

UIImageにラプラシアンフィルタをかけてエッジ検出をする

する。

まず簡単にラプラシアンフィルタについて。
ラプラシアンフィルタとは画像のラプラシアン(2階の偏微分)の演算結果のことを言うんだとか。これを求めることによって画像の特徴、つまりエッジを検出することができるらしい。
画像の(x,y)にあるピクセルをf = f(x,y)と表せば、2階の偏微分なので
\nabla^2 f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2}
これがラプラシアンフィルタということに。電磁気学でおなじみのあれ。
この右辺に偏微分の定義を当てはめてサンプリング間隔1で近似してあげると、最終的にラプラシアンフィルタは
\nabla^2 f \approx f(x+1,y) + f(x,y+1) -4f(x,y) + f(x-1,y) + f(x,y-1)
このような形になる。
この近似式は、現在注目しているピクセルf(x,y)の上下左右4つのピクセルを使ってラプラシアンフィルタを求めているので、4近傍ラプラシアンフィルタと呼ばれているみたい。今回のコードで利用するのはこれをベースにさらに斜め方向も考慮した、8近傍ラプラシアンフィルタ。
8近傍ラプラシアンフィルタは、4近傍に斜め方向の4点f(x-1,y-1), f(x+1,y-1), f(x-1,y+1), f(x+1,y+1)が加わるので、その分現在注目しているピクセルf(x,y)については4倍から8倍に増やしてあげることになる。これは式として導出するのは少々厄介なようなので(ソース知恵袋)、イメージでとらえるまでに留めた。というか逃げた。

ラプラシアンフィルタの詳しい導出過程は下記リンク先の記事でリンクが貼られている企画書を見るととってもよくわかります。お世話になりましたありがとうございました!
ラプラシアンフィルタ完成 - 2010年夏休み自由研究 - rexpit blog

そんなわけでここでようやくUIImageにラプラシアンフィルタをかけてエッジ検出をするコード。

    // 抽出対象画像のpixel値の取得
    UIImage *inImg = [UIImage imageNamed:@"foo.jpg"];
    CGImageRef inCgImage = [inImg CGImage];
    size_t bytesPerRow = CGImageGetBytesPerRow(inCgImage);
    CGDataProviderRef inDataProvider = CGImageGetDataProvider(inCgImage);
    CFDataRef inData = CGDataProviderCopyData(inDataProvider);
    UInt8 *inPixels = (UInt8*)CFDataGetBytePtr(inData);
    
    // 出力バッファ用に同じ画像でもう1つUIImageを作っておく
    UIImage *outImg = [UIImage imageNamed:@"foo.jpg"];
    CGImageRef outCgImage = [outImg CGImage];
    CGDataProviderRef outDataProvider = CGImageGetDataProvider(outCgImage);
    CFDataRef outData = CGDataProviderCopyData(outDataProvider);
    UInt8 *outPixels = (UInt8*)CFDataGetBytePtr(outData);
    
    // 画像処理
    // グレースケール化
    for (int y = 0 ; y < inImg.size.height; y++){
        for (int x = 0; x < inImg.size.width; x++){
            UInt8* buf = inPixels + y * bytesPerRow + x * 4;
            UInt8 gray = ( *(buf + 0) + *(buf + 1) + *(buf + 2))/3;
            *(buf + 0) = gray;
            *(buf + 1) = gray;
            *(buf + 2) = gray;
        }
    }
    
    // 8近傍ラプラシアンフィルタをかけてエッジ抽出
    for (int y = 1 ; y < inImg.size.height-1; y++){
        for (int x = 1; x < inImg.size.width-1; x++){
            int c1=0,c2=0,c3=0,weight;
            // ラプラシアンフィルタをかける
            for(int i=-1; i<=1; i++){
                for(int j=-1; j<=1;j++){
                    UInt8 *p = inPixels + (y+i) * bytesPerRow + (x+j) * 4;
                    if(i==0&&j==0){ weight = -8; }
                    else{ weight = 1; }
                    c1+= *(p+0) * weight;
                    c2+= *(p+1) * weight;
                    c3+= *(p+2) * weight;
                }
            }
            // 結果が負の場合もあるので絶対値をとる
            c1 = abs(c1);
            c2 = abs(c2);
            c3 = abs(c3);
            // 量子化の上限255を超えたときは255におさえる
            if(c1>255) c1=255;
            if(c2>255) c2=255;
            if(c3>255) c3=255;
            // 出力用UIImageのpixel値に結果を格納する
            UInt8 *buf = outPixels + y * bytesPerRow + x * 4;
            *(buf + 0) = (UInt8)c1;
            *(buf + 1) = (UInt8)c2;
            *(buf + 2) = (UInt8)c3;
        }
    }
    
    // pixel値からUIImageの再合成
    CFDataRef resultData = CFDataCreate(NULL, outPixels, CFDataGetLength(outData));
    CGDataProviderRef resultDataProvider = CGDataProviderCreateWithCFData(resultData);
    CGImageRef resultCgImage = CGImageCreate(
                                             CGImageGetWidth(inCgImage), CGImageGetHeight(inCgImage),
                                             CGImageGetBitsPerComponent(inCgImage), CGImageGetBitsPerPixel(inCgImage), bytesPerRow,
                                             CGImageGetColorSpace(inCgImage), CGImageGetBitmapInfo(inCgImage), resultDataProvider,
                                             NULL, CGImageGetShouldInterpolate(inCgImage), CGImageGetRenderingIntent(inCgImage));
    UIImage *result = [[[UIImage alloc] initWithCGImage:resultCgImage] autorelease];
    
    // 後処理
    CGImageRelease(resultCgImage);
    CFRelease(resultDataProvider);
    CFRelease(resultData);
    
    CFRelease(inData);
    CFRelease(outData);

UIImageのピクセルをいじる方法は画像処理に使えるUIImageのTips10個をそのまま。
特に説明するようなところは無いけれど、求めたラプラシアンフィルタは別のバッファに書きこんでいかなくちゃだめというところが大事。ずっと元画像を読み込んだバッファに1ピクセルごと上書きしてて「できねー」とかやってたのは私。


なるほど!

でもなんだか同じ画像でUIImageわざわざ2つ作ってるのが気持ち悪いので、どなたかもっとかっこいいやり方教えて下さい!

あと端の1ピクセルについては考慮してないので注意。

ちなみにこのコード、実機だとちゃんとエッジ検出された画像が出てきますがシミュレーターだと残念なことになります。
エッジ検出されると こんな感じ になります。

おわり。
本当にやりたい事はこれを使ってポアソン画像合成を実装することなので、もう一苦労しそう。

(初めてtex記法つかってみたけどなんか気持ち悪いっすね)