読者です 読者をやめる 読者になる 読者になる

iPhoneで画像処理の高速化実験

iPhone

iPhoneでOpenCVが使えることを知ったので、ついでに画像処理の高速化について今までもやもやしていたことをはっきりさせようと思い立ち実験してみた。

題材は画像のモザイク化

f:id:y_310:20100131002511p:image:left
f:id:y_310:20100131002831p:image

githubにあったこちら
niw/iphone_opencv_test · GitHub
のサンプルコードからforkさせてもらってモザイク化処理を追加した。
y310/iphone_opencv_test · GitHub
コンパイルオプションはXcodeのデフォルトで-Os(最も高速で最小)、iPhoneOS3.1.2のReleaseビルドで実験した。

Step1 とりあえず何も考えずに実装

モザイク処理メソッドから抜粋。
元画像のmSize*mSize領域のRGB平均値を計算し、出力先画像の同領域をその平均値で埋める、という処理。

int mSize = [mosaicSize intValue];
IplImage *img = [self CreateIplImageFromUIImage:imageView.image];
IplImage *dst = cvCreateImage(cvGetSize(img), img->depth, img->nChannels);
unsigned int r, g, b;
int p, pp, c;

for (int i = 0; i < img->height; i += mSize) {
    for (int j = 0; j < img->width; j += mSize) {
        r = b = g = 0;
        p = (i * img->width + j) * 3;
        c = 0;
        for (int k = 0; k < mSize; k++) {
            if (i + k < img->height) {
                for (int l = 0; l < mSize; l++) {
                    if (j + l < img->width) {
                        pp = p + (k * img->width + l) * 3;
                        b += (unsigned char)img->imageData[pp];
                        g += (unsigned char)img->imageData[pp + 1];
                        r += (unsigned char)img->imageData[pp + 2];
                        c++;
                    }
                }
            }
        }
        r /= c;
        g /= c;
        b /= c;
        
        for (int k = 0; k < mSize; k++) {
            if (i + k < img->height) {
                for (int l = 0; l < mSize; l++) {
                    if (j + l < img->width) {
                        pp = p + (k * img->width + l) * 3;
                        dst->imageData[pp] = (char)r;
                        dst->imageData[pp + 1] = (char)g;
                        dst->imageData[pp + 2] = (char)b;
                    }
                }
            }
        }
    }
}
cvReleaseImage(&img);
imageView.image = [self UIImageFromIplImage:dst];
cvReleaseImage(&dst);

この処理でmSizeを2,4,8,16,32,64,128で順番に実行した時の結果(5回の平均、単位は秒)

Step1
2 0.2202906
4 0.1995558
8 0.1747878
16 0.1725956
32 0.1691068
64 0.166092
128 0.1662554
average: 0.1812404

これを基準に高速化してみる。

Step2 img->widthをwidthに変換(heightも)

forループなどで使っているimg->widthが毎回imgからwidthをたどっていて効率が悪いのではないかと考え、ループの前で値を保存する用に変更。

int width = img->width;
int height = img->height;

for (int i = 0; i < height; i += mSize) {
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        c = 0;
        for (int k = 0; k < mSize; k++) {
            if (i + k < height) {
                for (int l = 0; l < mSize; l++) {
                    if (j + l < width) {
                        pp = p + (k * width + l) * 3;
                        b += (unsigned char)img->imageData[pp];
                        g += (unsigned char)img->imageData[pp + 1];
                        r += (unsigned char)img->imageData[pp + 2];
                        c++;
                    }
                }
            }
        }
        r /= c;
        g /= c;
        b /= c;
        
        for (int k = 0; k < mSize; k++) {
            if (i + k < height) {
                for (int l = 0; l < mSize; l++) {
                    if (j + l < width) {
                        pp = p + (k * width + l) * 3;
                        dst->imageData[pp] = (char)r;
                        dst->imageData[pp + 1] = (char)g;
                        dst->imageData[pp + 2] = (char)b;
                    }
                }
            }
        }
    }
}

実行結果

Step1 Step2
2 0.2202906 0.2058164
4 0.1995558 0.1892952
8 0.1747878 0.1648164
16 0.1725956 0.1648778
32 0.1691068 0.163123
64 0.166092 0.1677026
128 0.1662554 0.1650716
average: 0.1812404 0.174386

ちょっと早くなった。

Step3 ifの削減

画像サイズがmSizeで割り切れない時のためにif (i + k < height)という形でループ内でifの判定を行っていた部分を、事前にkMax,lMaxにループの最大値を保存することで削除した。

int kMax, lMax;
for (int i = 0; i < height; i += mSize) {
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        c = 0;
        kMax = i + mSize < height ? mSize : height - i;
        lMax = j + mSize < width  ? mSize : width - j;
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                pp = p + (k * width + l) * 3;
                b += (unsigned char)img->imageData[pp];
                g += (unsigned char)img->imageData[pp + 1];
                r += (unsigned char)img->imageData[pp + 2];
                c++;
            }
        }
        r /= c;
        g /= c;
        b /= c;
        
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                pp = p + (k * width + l) * 3;
                dst->imageData[pp] = (char)r;
                dst->imageData[pp + 1] = (char)g;
                dst->imageData[pp + 2] = (char)b;
            }
        }
    }
}

実行結果

Step1 Step2 Step3
2 0.2202906 0.2058164 0.1998938
4 0.1995558 0.1892952 0.1865738
8 0.1747878 0.1648164 0.1517498
16 0.1725956 0.1648778 0.1577388
32 0.1691068 0.163123 0.1486748
64 0.166092 0.1677026 0.1521254
128 0.1662554 0.1650716 0.1549348
average: 0.1812404 0.174386 0.1645274

Step2から約0.01秒早くなった。

Step4 img->imageDataも削減&c++をc=kMax*lMaxに

width,heightと同じ考え方でimg->imageDataもimgImageDataに変換。
あと対象領域の平均値計算に使っていたcを毎回ループ内でカウントするのをやめてkMax*lMaxで計算。(最初に気付けと思ったがStep1の段階ではifの判定で数が変わるため事前には計算できなかった)

char *imgImageData = img->imageData;
char *dstImageData = dst->imageData;
for (int i = 0; i < height; i += mSize) {
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        kMax = i + mSize < height ? mSize : height - i;
        lMax = j + mSize < width  ? mSize : width - j;
        c = kMax * lMax;
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                pp = p + (k * width + l) * 3;
                b += (unsigned char)imgImageData[pp];
                g += (unsigned char)imgImageData[pp + 1];
                r += (unsigned char)imgImageData[pp + 2];
            }
        }
        r /= c;
        g /= c;
        b /= c;
        
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                pp = p + (k * width + l) * 3;
                dstImageData[pp] = (char)r;
                dstImageData[pp + 1] = (char)g;
                dstImageData[pp + 2] = (char)b;
            }
        }
    }
}

実行結果

Step1 Step2 Step3 Step4
2 0.2202906 0.2058164 0.1998938 0.1796986
4 0.1995558 0.1892952 0.1865738 0.1683434
8 0.1747878 0.1648164 0.1517498 0.148594
16 0.1725956 0.1648778 0.1577388 0.1463726
32 0.1691068 0.163123 0.1486748 0.1627986
64 0.166092 0.1677026 0.1521254 0.1537092
128 0.1662554 0.1650716 0.1549348 0.151777
average: 0.1812404 0.174386 0.1645274 0.1587564

ここまでで初期状態から約0.023秒速くなった。

Step5 ppの計算回数を削減

k-lループの内側で毎回現在の注目ピクセル座標ppを計算していたが、kループの内側で計算すればlループ内はインクリメントだけでいける。

for (int i = 0; i < height; i += mSize) {
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        kMax = i + mSize < height ? mSize : height - i;
        lMax = j + mSize < width  ? mSize : width - j;
        c = kMax * lMax;
        for (int k = 0; k < kMax; k++) {
            pp = p + (k * width * 3);
            for (int l = 0; l < lMax; l++) {
                b += (unsigned char)imgImageData[pp++];
                g += (unsigned char)imgImageData[pp++];
                r += (unsigned char)imgImageData[pp++];
            }
        }
        r /= c;
        g /= c;
        b /= c;
        
        for (int k = 0; k < kMax; k++) {
            pp = p + (k * width * 3);
            for (int l = 0; l < lMax; l++) {
                dstImageData[pp++] = (char)r;
                dstImageData[pp++] = (char)g;
                dstImageData[pp++] = (char)b;
            }
        }
    }
}

実行結果

Step1 Step2 Step3 Step4 Step5
2 0.2202906 0.2058164 0.1998938 0.1796986 0.1845956
4 0.1995558 0.1892952 0.1865738 0.1683434 0.1641452
8 0.1747878 0.1648164 0.1517498 0.148594 0.1551546
16 0.1725956 0.1648778 0.1577388 0.1463726 0.1454828
32 0.1691068 0.163123 0.1486748 0.1627986 0.147119
64 0.166092 0.1677026 0.1521254 0.1537092 0.1495658
128 0.1662554 0.1650716 0.1549348 0.151777 0.1533584
average: 0.1812404 0.174386 0.1645274 0.1587564 0.15706

一応速くなった…かな?ちょっと誤差かもしれない微妙な差。

Step6 掛け算を削減

掛け算より足し算の方が速いと聞いたことがあったのでppの計算から掛け算をなくしてみる。
Step5でppは繰り返しの度にwidth*3を増やす処理と同値だったので毎回width*3を足すようにする。

for (int i = 0; i < height; i += mSize) {
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        kMax = i + mSize < height ? mSize : height - i;
        lMax = j + mSize < width  ? mSize : width - j;
        c = kMax * lMax;
        pp = offset = p;
        for (int k = 0; k < kMax; k++) {	
            for (int l = 0; l < lMax; l++) {
                b += (unsigned char)imgImageData[pp++];
                g += (unsigned char)imgImageData[pp++];
                r += (unsigned char)imgImageData[pp++];
            }
            offset += stride;
            pp = offset;
        }
        r /= c;
        g /= c;
        b /= c;
        pp = offset = p;
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                dstImageData[pp++] = (char)r;
                dstImageData[pp++] = (char)g;
                dstImageData[pp++] = (char)b;
            }
            offset += stride;
            pp = offset;
        }
    }
}

実行結果

Step1 Step2 Step3 Step4 Step5 Step6
2 0.2202906 0.2058164 0.1998938 0.1796986 0.1845956 0.186088
4 0.1995558 0.1892952 0.1865738 0.1683434 0.1641452 0.1658398
8 0.1747878 0.1648164 0.1517498 0.148594 0.1551546 0.1556316
16 0.1725956 0.1648778 0.1577388 0.1463726 0.1454828 0.14568
32 0.1691068 0.163123 0.1486748 0.1627986 0.147119 0.1497236
64 0.166092 0.1677026 0.1521254 0.1537092 0.1495658 0.1495576
128 0.1662554 0.1650716 0.1549348 0.151777 0.1533584 0.1559454
average: 0.1812404 0.174386 0.1645274 0.1587564 0.15706 0.1583524

遅くなった…。1行の掛け算だったのが足し算と代入の2行に別れたせいだろうか。
理由は分からないがとりあえずあまり効果はないようだ。

Step7 構造体を使って代入を減らす

出力先画像への代入処理が同じ値をRGBの3回分毎回やるのは非効率な気がするのでunsigned charのRGB値を保持する構造体を作って、1回で構造体まるごと代入するようにしてみた。
正直この方法はchar*のimageDataを無理矢理struct Rgb*にキャストしてたりするので危険だと思う。
構造体もきっちりunsigned char*3つ分になる保証はないはず。

struct Rgb *dstImageData = (struct Rgb*)dst->imageData;
for (int i = 0; i < height; i += mSize) {
    kMax = i + mSize < height ? mSize : height - i;
    for (int j = 0; j < width; j += mSize) {
        r = b = g = 0;
        p = (i * width + j) * 3;
        lMax = j + mSize < width  ? mSize : width - j;
        c = kMax * lMax;
        pp = offset = p;
        for (int k = 0; k < kMax; k++) {	
            for (int l = 0; l < lMax; l++) {
                b += (unsigned char)imgImageData[pp++];
                g += (unsigned char)imgImageData[pp++];
                r += (unsigned char)imgImageData[pp++];
            }
            offset += stride;
            pp = offset;
        }
        rgb.r = r / c;
        rgb.g = g / c;
        rgb.b = b / c;
        pp = offset = i * width + j;
        for (int k = 0; k < kMax; k++) {
            for (int l = 0; l < lMax; l++) {
                dstImageData[pp++] = rgb;
            }
            offset += width;
            pp = offset;
        }
    }
}

実行結果

Step1 Step2 Step3 Step4 Step5 Step6 Step7
2 0.2202906 0.2058164 0.1998938 0.1796986 0.1845956 0.186088 0.2409104
4 0.1995558 0.1892952 0.1865738 0.1683434 0.1641452 0.1658398 0.203338
8 0.1747878 0.1648164 0.1517498 0.148594 0.1551546 0.1556316 0.184218
16 0.1725956 0.1648778 0.1577388 0.1463726 0.1454828 0.14568 0.1747662
32 0.1691068 0.163123 0.1486748 0.1627986 0.147119 0.1497236 0.1759188
64 0.166092 0.1677026 0.1521254 0.1537092 0.1495658 0.1495576 0.1728034
128 0.1662554 0.1650716 0.1549348 0.151777 0.1533584 0.1559454 0.1736168
average: 0.1812404 0.174386 0.1645274 0.1587564 0.15706 0.1583524 0.1893676

さらに遅くなった…。というかStep1より遅い。なんでこんなに遅くなるんだろうか。
そろそろアセンブラレベルでちゃんと見ないといけない気がしてきた。

とりあえずまとめ

今までなんとなくこう書いた方が速くなるかなぁと思っていたことが、実際効果があったり逆効果だったりすることがわかった。
結局この辺どれが本当に正しいかはマシン語レベルで見ないとわからなそう。まだまだコンパイラの最適化だけに頼ってちゃいけないんですね。

追記

通りすがりさんからアイディアをいただいたので組み込んでみた。

void mosaic(char *imgImageData, char *dstImageData, int width, int sX, int sY, int w, int h, int wmSize, int hmSize, int stride) {
	int dp = width * 3;
	int dpp = wmSize * 3;
	unsigned int r, g, b;
	int p, pp, c;
	c = wmSize * hmSize;
	
	for (int y = sY; y < h; y++) {
		p = y * dp * hmSize;
		for (int x = sX; x < w; x++, p += dpp) {
			r = b = g = 0;
			pp = p;
			for (int yy = 0; yy < hmSize; yy++) {
				for (int xx = 0; xx < wmSize; xx++) {
					b += (unsigned char)imgImageData[pp++];
					g += (unsigned char)imgImageData[pp++];
					r += (unsigned char)imgImageData[pp++];
				}
				pp += stride;
			}
			r /= c;
			g /= c;
			b /= c;
			
			pp = p;
			for (int yy = 0; yy < hmSize; yy++) {
				for (int xx = 0; xx < wmSize; xx++) {
					dstImageData[pp++] = (char)r;
					dstImageData[pp++] = (char)g;
					dstImageData[pp++] = (char)b;
				}
				pp += stride;
			}
		}
	}
}

{
...
    int wBlockCnt = width / mSize;
    int hBlockCnt = height / mSize;

    /* 割り切れる領域 */
    mosaic(imgImageData, dstImageData, width, 0, 0, wBlockCnt, hBlockCnt, mSize, mSize, (width - mSize) * 3);

    /* 割り切れない領域 */
    if ( width % mSize > 0) {
        mosaic(imgImageData, dstImageData, width, wBlockCnt, 0, wBlockCnt + 1, hBlockCnt, width % mSize, mSize, (width - (width % mSize)) * 3);
    }
    if ( height % mSize > 0) {
        mosaic(imgImageData, dstImageData, width, 0, hBlockCnt, wBlockCnt, hBlockCnt + 1, mSize, height % mSize, (width - mSize) * 3);
    }
    if ( width % mSize > 0 && height % mSize > 0) {
        mosaic(imgImageData, dstImageData, width, wBlockCnt, hBlockCnt, wBlockCnt + 1, hBlockCnt + 1, width % mSize, height % mSize, (width - (width % mSize)) * 3);
    }
...
}
Step1 Step2 Step3 Step4 Step5 Step6 Step7 Step8
2 0.2202906 0.2058164 0.1998938 0.1796986 0.1845956 0.186088 0.2409104 0.1631222
4 0.1995558 0.1892952 0.1865738 0.1683434 0.1641452 0.1658398 0.203338 0.15106
8 0.1747878 0.1648164 0.1517498 0.148594 0.1551546 0.1556316 0.184218 0.1435718
16 0.1725956 0.1648778 0.1577388 0.1463726 0.1454828 0.14568 0.1747662 0.142803
32 0.1691068 0.163123 0.1486748 0.1627986 0.147119 0.1497236 0.1759188 0.1379126
64 0.166092 0.1677026 0.1521254 0.1537092 0.1495658 0.1495576 0.1728034 0.13246
128 0.1662554 0.1650716 0.1549348 0.151777 0.1533584 0.1559454 0.1736168 0.1457272
average: 0.1812404 0.174386 0.1645274 0.1587564 0.15706 0.1583524 0.1893676 0.1452366

最速記録更新!余計なif文がなくなったのが大きいのかな。

さらに追記

さらに改良コードを貼っていただいたので取り込んでみました。
が、Step1より遅い結果に...正確な原因はわかりませんが剰余演算とif文がループ内に増えた分と配列の要素参照が増えたところあたりがアルゴリズムの改善効果以上に効いてるのでしょうか。

Step1 Step2 Step3 Step4 Step5 Step6 Step7 Step8 Step9
2 0.2202906 0.2058164 0.1998938 0.1796986 0.1845956 0.186088 0.2409104 0.1631222 0.297052
4 0.1995558 0.1892952 0.1865738 0.1683434 0.1641452 0.1658398 0.203338 0.15106 0.26692
8 0.1747878 0.1648164 0.1517498 0.148594 0.1551546 0.1556316 0.184218 0.1435718 0.2529658
16 0.1725956 0.1648778 0.1577388 0.1463726 0.1454828 0.14568 0.1747662 0.142803 0.2481548
32 0.1691068 0.163123 0.1486748 0.1627986 0.147119 0.1497236 0.1759188 0.1379126 0.2526098
64 0.166092 0.1677026 0.1521254 0.1537092 0.1495658 0.1495576 0.1728034 0.13246 0.248334
128 0.1662554 0.1650716 0.1549348 0.151777 0.1533584 0.1559454 0.1736168 0.1457272 0.2471866
average: 0.1812404 0.174386 0.1645274 0.1587564 0.15706 0.1583524 0.1893676 0.1452366 0.2590316