重複コードを避けたい
2017年8月、画像の膨張・収縮アルゴリズムについて調べたときに、双方のコードの重複が気になりました。
関数ポインタによる実装を考えてみます。
膨張処理も収縮処理も、Y座標とX座標によるラスタスキャン・ループの部分は同じコードで重複しています。実際、片方のコードはコピペして作成してしまいました。
簡単なコードなので、重複していることは気になりませんし、このコードのままで良いと考えています。しかし、もう少し複雑なコードだった場合はどうでしょうか?いつまでも、コピペを続けますか?
関数ポインタによる実装
重複を避けるひとつの方法は、意味のある重複部分を関数にすることです。しかし、今回の場合はある一部の処理が重複しているのではなく、ある一部の処理が異なっているのです。こんなとき、C言語の熟練プログラマーが考える方法は関数ポインタによる実装です。
異なる処理の部分を関数化して関数ポインタとして引数で渡すことにより、ひとつの処理として実現できます。
3つの関数のインターフェースを示します。
- Func_dilation() : 画像膨張
- Func_erosion() : 画像収縮
- Image_loop_func() : 画像のラスタスキャン・ループ
C++言語によるアルゴリズムコードを示します。
unsigned char Func_dilation(unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur) { unsigned char ux = 0x00; if (uc != 0x00) { ux = 0xFF; } if (ua != 0x00) { ux = 0xFF; } if (ub != 0x00) { ux = 0xFF; } if (ul != 0x00) { ux = 0xFF; } if (ur != 0x00) { ux = 0xFF; } // return ux; }
unsigned char Func_erosion(unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur) { unsigned char ux = 0xFF; if (uc != 0xFF) { ux = 0x00; } if (ua != 0xFF) { ux = 0x00; } if (ub != 0xFF) { ux = 0x00; } if (ul != 0xFF) { ux = 0x00; } if (ur != 0xFF) { ux = 0x00; } // return ux; }
typedef unsigned char (*FUNC)(unsigned char ,unsigned char ,unsigned char ,unsigned char ,unsigned char); // void Image_loop_func( int iw ,int ih ,const unsigned char *pucSrc ,unsigned char *pucDst ,FUNC pFunc ) { for(int iy=1; iy<ih-1; iy++) { for(int ix=1; ix<iw-1; ix++) { unsigned char uc = pucSrc[ (iy+0)*iw + (ix+0) ]; // 中央 unsigned char ua = pucSrc[ (iy-1)*iw + (ix+0) ]; // 上 unsigned char ub = pucSrc[ (iy+1)*iw + (ix+0) ]; // 下 unsigned char ul = pucSrc[ (iy+0)*iw + (ix-1) ]; // 左 unsigned char ur = pucSrc[ (iy+0)*iw + (ix+1) ]; // 右 // unsigned char ux = pFunc( uc ,ua ,ub ,ul ,ur ); pucDst[ iy*iw + ix ] = ux; } } }
// 画像膨張 Image_loop_func( iw ,ih ,pucSrc ,pucDst ,Func_dilation );
// 画像収縮 Image_loop_func( iw ,ih ,pucSrc ,pucDst ,Func_erosion );
関数ポインタによる実装の仕組み
膨張と収縮で異なる処理部分を関数化(Func_dilation、Func_erosion)して、画像のラスタスキャン・ループに関数ポインタで渡すことにより、膨張アルゴリズムと収縮アルゴリズムのメイン部分を共通のコードで実現することができました。
コンパイラが生成するx86アセンブラコードを確認します。
- 画像膨張処理は、膨張関数(Func_dilation)のポインタをスタックにPUSHして、Image_loop_func()をコールします。
- 画像収縮処理は、収縮関数(Func_erosion)のポインタをスタックにPUSHして、Image_loop_func()をコールします。
- Image_loop_func()では、ラスタスキャン・ループしながら1画素ごとに pFunc() をコールします。pFunc()は、スタックで渡した関数ポインタを間接コールしています。
関数ポインタによる実装は、ラスタスキャン・ループの重複コードを除去できましたが、1画素ごとに関数ポインタを間接コールしているので、4K画像などの大画像を処理する場合はオーバーヘッドが気になります。
画像膨張・収縮のような簡単なコードの場合、無理して重複コードを除去する必要はないと考えます。もう少し複雑なコードだった場合、関数ポインタによる実装を選択するかどうかは、ソースコードの保守とパフォーマンスのトレードオフかもしれません。
もう少し工夫することができないか、考えてみたいと思います。