端っこを攻める

 ある対象セルの値を、その近傍の8セルの値によって決定したい。
画像処理などのプログラムではよく見かける処理で、3*3マスのフィルタを処理対象画像にラスタスキャンで適用し、画素値を計算したりする。
 今回は、ライフゲームを作成する最中に、このフィルタを盤面上のすべてのマスに適用して、次ターン盤面を計算したい。
 3*3の範囲を処理すること自体は簡単で、横方向の差をdi、縦方向の差をdjとか定義して、
 for(int dj = -1; dj < 2; dj++){
        for(int di = -1;di < 2;di++){
             /*
                ここで近傍cellの計算処理
            */
        }
    }
みたいな具合にすれば良い。ただし、愚直にこれを適用していこうとすると問題にぶち当たる。というのも、この2重forループを適用するには、対象セルの上下左右にセルが存在している必要があるのである。ただ、実際の盤面には盤面の端とか4隅とかが存在しているわけで、こういった部分にはこれがそのままでは適用できない。
 そこでまずは、素直に、素朴で愚直な方法を考えた。盤面を次のような9フィールドに区切って場合分けを行う。
①左上の隅。左のラインと上のラインがないので、0<=dj<=1 , 0<=di<=1 の範囲で周囲のセルを調べるようにする。
②上の端。上のラインがないので、0<=dj<=1, -1<=di<=1 の範囲で周囲のセルを調べるようにする。
③右上の隅。右のラインと上のラインがないので、0<=dj<=1 , -1<=di<=0の範囲で周囲のセルを調べるようにする。
④左の端。左のラインがないので、-1<=dj<=1 , 0<=di<=1 の範囲で周囲のセルを調べるようにする。
⑤真ん中。これは -1<=dj<=1 , -1<=di<=1 の範囲で周囲のセルを調べるようにする。
⑥右の端。右のラインがないので、-1<=dj<=1 , -1<=di<=0 の範囲で周囲のセルを調べるようにする。
⑦左下の隅。左のラインと下のラインがないので、-1<=dj<=0, 0<=di<=1 の範囲で周囲のセルを調べるようにする。
⑧下の端。白のラインがないので、 -1<=dj<=0 , -1<=di<=1の範囲で周囲のセルを調べるようにする。
⑨右下の隅。右のラインと下のラインがないので、-1<=dj<=0 , -1<=di<=0 の範囲で周囲のセルを調べるようにする。

これを実装するとif文を9本並べて場合分けをすることになる。しかも、各if文に同じような二重forループが書かれることになる。
うーん、冗長!

ということで、もう少し賢くやる方法がある。
それが、処理するセルを、一回り大きい枠で囲んでしまう方法である。
今回のような9セルの場合は幅1セル枠で囲んでおき、空白セルと同じ値にしておく。ライフゲームの場合は死んだ状態のセルが空白セル、画像処理の場合は画素値0の画素がこれに相当する。周囲を囲んでおいて、その枠内だけにフィルタを適用することで、場合分けをせずとも、存在しないセルが参照される状況を防ぐことができる。
今回のケースでは、外側の2行と2列を犠牲にして、
for(int j=1; j<height-1; j++){
    for(int i=1;i<width-1; i++){
     for(int dj = -1; dj < 2; dj++){
            for(int di = -1;di < 2;di++){
                 /*
                ここで近傍cellの計算処理
                */
            }
        }
    }
}
みたいな4重ループでラスタスキャンをかけて、盤面全体の更新をはかることができた。(実際のプログラムでは近傍のチェック部分は別関数に切り分けるべき)

端っこを攻めるときは、こうしたちょっとしたテクニックを使うことで、コード量を削減できる。

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。