プログラム
Win32APIを用いてプログラムを作成します。分割・統合法による処理はSplitMerge
クラスにまとめました。以下では要点を解説します。なお、SplitMerge
クラスは、サンプルファイルの「SplitAndMerge.cpp」と「SplitAndMerge.h」で定義しています。
データ構造
部分領域をどのように表現するのか、というのは大きなポイントです。最も処理効率が良いのは、頂点座標だけを保存する方法でしょう。しかし、この方法では「分割は簡単だが統合は困難」、という問題があります。仮に解決できたとしても、ある座標はどの部分領域に属しているのか、という基本的な判定さえ困難です。
これでは使い勝手が悪いですよね。そこで今回は、ラベリングによって各部分領域を区別することにします。ラベリングは、仕組みは簡単ですが、時間のかかる処理なので、高速化に注力する必要があります。
SplitMergeクラスの初期設定
SplitMerge
クラスは、以下の手順でメンバ関数を呼び出します。
- 画像の入力
- 分割、統合、微小領域統合の閾値設定
- 初期領域の設定、処理開始
SetImage
メンバ関数SetThreshold
メンバ関数SplitAndMerge
メンバ関数画像の入力
処理対象の画像には、DIB(デバイス独立ビットマップ)を入力して下さい。DIBの原点座標は左下ですが、表示する時はデバイスコンテキストに描画するので、原点座標が左上になるように変換しています。ここではなく、描画する時に変換する方法もあります。changeChannel
メンバ関数は、実際の処理で使用する表色系(色を数値的に表すための体系)に変換した画像を作成します。今回は濃度画像を作成することになります。
// 処理したい画像を入力する void SplitMerge::SetImage(const BYTE *c_pImage, DWORD width,DWORD height,BYTE byte) { if(c_pImage==NULL || width==0 || height==0) return; // error // 24ビットまたは32ビットだけ受理する if(byte!=3 && byte!=4) return; m_w=width; m_h=height; m_byte=byte; // m_len は一行のバイト数 m_len=(width*byte%4) ? width*byte+(4-width*byte%4) : width*byte; // 入力画像をコピーする if(m_pImage) free(m_pImage); m_pImage=(BYTE*)malloc(m_len*height); for(DWORD y=0;y<height;y++) { // 左上を原点に memcpy(&m_pImage[y*m_len],&c_pImage[(height-1-y)*m_len],m_len); } // 実際の処理で使用する表色系に変換した画像を作成する changeChannel(); // ラベリング用のメモリ領域を確保する if(m_pLabel) free(m_pLabel); m_pLabel=(DWORD*)malloc(sizeof(DWORD)*width*height); memset(m_pLabel,0,sizeof(DWORD)*width*height); }
m_pChannel
が濃度画像です。入力画像m_pImage
はそのまま保存しておき、以降の処理はm_pChannel
に対して行われます。濃度以外の画像を作成する場合もchangeChannel
メンバ関数に記述して下さい。また、処理対象の画像は一つだけ、という制約はありません。複数の処理画像を作成したい場合はm_pChannel2
、m_pChannel3
などを追加して下さい。
// 実際の処理で使用する表色系に変換した画像を作成する void SplitMerge::changeChannel(void) { if(m_pImage==NULL) return; // error if(m_pChannel) free(m_pChannel); m_pChannel=(BYTE*)malloc(m_w*m_h); BYTE r,g,b; DWORD x,y; for(y=0;y<m_h;y++){ for(x=0;x<m_w;x++){ r=m_pImage[x*m_byte+2+y*m_len]; g=m_pImage[x*m_byte+1+y*m_len]; b=m_pImage[x*m_byte +y*m_len]; // 濃度に変換 m_pChannel[x+y*m_w]=(BYTE)((r+g+b)/3.0+0.5); } } }
分割、統合、微小領域統合の閾値設定
SetThreshold
メンバ関数では、各処理の判断基準となる境界値(閾値)の設定を行います。
引数 | 用途 |
smoothHistogram | 分割の時に作成するヒストグラムを平滑化する回数です。 |
split | 分割の閾値です。濃度ヒストグラムの山の面積がsplit パーセントより小さければ分割します。 |
merge | 統合の閾値です。隣接する濃度平均値の差がmerge より小さければ統合します。 |
minSize | 微小領域統合の閾値です。ある領域の面積がminSize より小さければ統合します。 |
// 閾値を設定する void SplitMerge::SetThreshold(BYTE smoothHistogram, double split,double merge,DWORD minSize) { if(split<=0 || split>1) return; // error if(merge<=0 || merge>255) return; // error m_smooth=smoothHistogram; // ヒストグラムを平滑化する回数 m_split=split; // 山の面積の割合 m_merge=merge; // 濃度平均値の差 m_minSize=minSize; // 領域の最小面積 }
初期領域の設定、処理開始
引数で実際に処理する領域(初期領域)の座標を設定します。この領域外は処理対象外となります。通常は、画像四隅の座標を設定しますが、一部の範囲だけを処理したい場合は、その座標を設定して下さい。不要な領域を除外することで、処理速度が向上します。
最下部のCreateThread
関数に注目して下さい。実際の処理はサブスレッドで行います。スレッド方式を採用した理由は、領域分割の処理は大変時間がかかるため、ビジー状態によってユーザーに不安を与えてしまう可能性があるからです。そのためスレッド化することで、全体の処理時間は若干増加しますが、処理の途中経過を表示することで体感時間は短くしています。
ただし、DOSプログラムから呼び出す場合は、画像を表示することが無いので、スレッド化する意味がありません。むしろ逆効果です。スレッド化を解除する場合は、スレッドプロシージャであるsmThreadProc
メンバ関数のプログラムをCreateThread
関数の位置に記述して下さい。TerminateThread
関数はスレッドを強制終了させる関数です。あまり実行して欲しくないようなことがヘルプに記述されていますが、他に良い方法がないので使っています。私の環境では正常に動作しました。
// 分割・統合 void SplitMerge::SplitAndMerge(DWORD left, DWORD top,DWORD right,DWORD bottom) { if(m_pChannel==NULL || m_pLabel==NULL) return; // error if(left>right || top>bottom) return; // error if(right>=m_w || bottom>=m_h) return; // error if(m_split==0 || m_merge==0) return; // error m_l=left; m_t=top; m_r=right; m_b=bottom; // 処理する領域 if(m_hThread) { TerminateThread(m_hThread,0); // スレッド強制終了 DWORD code; GetExitCodeThread(m_hThread,&code); while(code==STILL_ACTIVE) // スレッド終了まで待機 { Sleep(1); GetExitCodeThread(m_hThread,&code); } CloseHandle(m_hThread); } DWORD dwThreadID; m_hThread=CreateThread(NULL,0,smThreadProc,this,0,&dwThreadID); }
スレッドプロシージャ
smThreadProc
はSplitMerge
クラスのメンバ関数です。メンバ関数をスレッドプロシージャとして用いる場合は、static
メンバ関数にしなければなりません。static
メンバ関数にするためには、宣言にstatic
を付けます。定義には付けられません。static
メンバ関数は普通の関数と同じように振る舞います。また、非static
メンバには通常アクセスできませんが、クラスオブジェクトのポインタをsmThreadProc
メンバ関数の引数に渡すことで、このポインタからクラスオブジェクトの内部にアクセスすることが出来ます。非公開メンバにもアクセスできるのが、普通の関数にクラスオブジェクトのポインタを渡した時との違いです。
class SplitMerge { private: …… static DWORD WINAPI smThreadProc(void *lpParameter); …… }; // 分割・統合スレッド DWORD WINAPI SplitMerge::smThreadProc(void *lpParameter) { SplitMerge *p=(SplitMerge*)lpParameter; p->m_complete=false; // 分割 p->m_number=1; // 処理済みの領域には 0 以外の番号を付ける p->split(p->m_l,p->m_t,p->m_r,p->m_b); // 統合準備 p->prepare(); // 統合 DWORD n; do{ n=p->merge(); }while(n); // 微小領域の統合 p->absorb(); p->m_complete=true; return 0; }
分割
分割は単純な再帰処理で実現できます。再帰は苦手な方も多いと思いますが、これは簡単なので大丈夫です。再帰を理解するポイントは終了処理を明確にすることです。今回の終了条件は、分割判定で否定された、です。分割の可否はifSplit
メンバ関数で判定します。
各部分領域にはラベリングで番号を付けますが、いつラベリングするかで処理効率が違ってきます。理論を素直に考えると、分割する毎にラベリングすることになりますが、それでは同じ領域に何度もラベリングし直すことになります。そこで、無駄がない方法として、再帰から脱出する時にラベリングします。ただし、再帰を一つ脱出する度に、既に分割され、ラベリングされた領域を参照することになるので、注意が必要です。ここで再びラベリングしたら、致命的な不具合を引き起こします。しかしこの問題は、m_pLabel
の初期値が0
であることを利用すれば、容易に回避できます。
// 分割 void SplitMerge::split(DWORD left,DWORD top,DWORD right,DWORD bottom) { if(left>right || top>bottom) return; // error if(right>=m_w || bottom>=m_h) return; // error // 更に分割する?もうしない? if(ifSplit(left,top,right,bottom)==false) { if(m_pLabel[left+top*m_w]==0) // まだラベリングしてない { labeling(left,top,right,bottom); m_n++; // 分割数 } return; // 再帰脱出 } DWORD x=(left+right)/2,y=(top+bottom)/2; split(left,top,x,y); // 左上 split(x+1,top,right,y); // 右上 split(left,y+1,x,bottom); // 左下 split(x+1,y+1,right,bottom); // 右下 }
ifSplit
メンバ関数は理論のままなので、ソースファイルを確認してください。ちなみに、現在、最も処理に時間がかかっているのは、この分割判定だと思われます。良いアイディアをお持ちの方は改良してみて下さい。
labeling
メンバ関数も単純なので、ソースファイルを確認してください。複雑な形状の領域をラベリングする時は工夫が必要ですが、分割する時は必ず四角形であり、しかも四隅の座標がわかっているという理想状態なので、特に考えるべきことはありません。
統合準備
分割の次は統合ですが、その前に統合処理を高速化するための準備をしましょう。統合していくに従って、領域の形状は複雑になっていきます。もし何の工夫もしなかったらどうなるでしょうか。
まず、領域の始点と終点がわかりません。このままでは、統合される領域をラベリングするために画像全体を走査したり、統合条件である平均濃度値をいちいち計算しなければなりません。これは大きな無駄です。
これらの無駄を解消するために、以下のような値を保存します
- 各部分領域の左上(始点)と右下(終点)の座標を保存する
- 各部分領域の平均濃度値を保存する
- 統合された新しい部分領域の平均濃度値を計算し直すために、各部分領域の面積を保存する
ただし、今回の統合条件である平均濃度値のように、統合後の新しい平均濃度値を計算で求められるものばかりではありません。調べ直さなければならない特徴量もあります。
// 統合の時に使う値を計算 void SplitMerge::prepare(void) { if(m_pAverage) free(m_pAverage); // 領域の平均濃度値 if(m_pSize) free(m_pSize); // 領域の面積 if(m_pStartX) free(m_pStartX); // 領域の左上 x 座標 if(m_pStartY) free(m_pStartY); // 領域の左上 y 座標 if(m_pEndX) free(m_pEndX); // 領域の右下の x 座標 if(m_pEndY) free(m_pEndY); // 領域の右下の y 座標 // m_number は領域の個数 m_pAverage=(double*)malloc(sizeof(double)*m_number); m_pSize=(DWORD*)malloc(sizeof(DWORD)*m_number); m_pStartX=(DWORD*)malloc(sizeof(DWORD)*m_number); m_pStartY=(DWORD*)malloc(sizeof(DWORD)*m_number); m_pEndX=(DWORD*)malloc(sizeof(DWORD)*m_number); m_pEndY=(DWORD*)malloc(sizeof(DWORD)*m_number); memset(m_pAverage,0,sizeof(double)*m_number); memset(m_pSize,0,sizeof(DWORD)*m_number); memset(m_pStartX,0,sizeof(DWORD)*m_number); memset(m_pStartY,0,sizeof(DWORD)*m_number); memset(m_pEndX,0,sizeof(DWORD)*m_number); memset(m_pEndY,0,sizeof(DWORD)*m_number); DWORD x,y,z; for(y=m_t;y<=m_b;y++){ for(x=m_l;x<=m_r;x++){ z=x+y*m_w; if(m_pSize[ m_pLabel[z] ]==0) { // 始点 m_pStartX[ m_pLabel[z] ]=x; m_pStartY[ m_pLabel[z] ]=y; } // 終点 m_pEndX[ m_pLabel[z] ]=x; m_pEndY[ m_pLabel[z] ]=y; // 合計濃度値をとりあえず求める m_pAverage[ m_pLabel[z] ]+=m_pChannel[z]; m_pSize[ m_pLabel[z] ]++; // 面積 } } for(DWORD i=1;i<m_number;i++){ // 0 は処理していない領域の番号 if(m_pSize[i]==0) continue; // error m_pAverage[i]/=m_pSize[i]; // 平均濃度値 } }
統合
統合処理は、隣接する部分領域を探すところから始まります。m_t
、m_b
、m_l
、m_r
はSplitAndMerge
メンバ関数で設定した初期領域の座標です。この領域外は処理対象外です。隣接する方向としては、上下左右の四方向が考えられますが、左上から右下に走査する場合は、右と下だけ調べれば、全ての隣接する部分領域を調べられることになります。
統合はラベリングし直すことに相当します。どちらの部分領域をラベリングし直せば良いかといえば、面積の小さい方をラベリングし直した方が、処理効率が良いでしょう。また、下記のプログラムでは、自領域の方がラベリングし直された可能性を考慮するため、右を調べた後に、自領域の番号m_pLabel[x+y*m_w]
を再取得しています。
// 統合 DWORD SplitMerge::merge(void) { DWORD c,r,b,n=0; DWORD x,y; for(y=m_t;y<=m_b;y++){ for(x=m_l;x<=m_r;x++){ c=m_pLabel[x+y*m_w]; if(x+1<m_r) { r=m_pLabel[x+1+y*m_w]; // 右を調べる if(c!=r && ifMerge(c,r)) { labeling(c,r); n++; } } c=m_pLabel[x+y*m_w]; // 統合されたかもしれないので if(y+1<m_b) { b=m_pLabel[x+(y+1)*m_w]; // 下を調べる if(c!=b && ifMerge(c,b)) { labeling(c,b); n++; } } } } m_n-=n; return n; // 統合数 }
統合判定を担当しているメンバ関数ifMerge
は理論のままなので、ソースファイルを確認してください。labeling
メンバ関数については次で解説しますが、これは分割の時に呼び出したlabeling
メンバ関数とは別の関数です(多重定義)。
ラベリング
下記のプログラムをご覧下さい。「#1」で面積の小さい領域がどちらであるかを調べています。「#2」で面積の小さい領域の番号を面積の大きい領域の番号で置き換えています。走査の始点と終点は、統合準備で調べておいた領域の左上と右下の座標です。ただし、その間の行は初期領域の左端から右端まで走査する必要があり、まだ無駄があります。「#3」では統合準備で調べた、平均濃度値、面積、領域の左上と右下の座標を、統合された新しい領域のそれに合うように計算し直しています。平均濃度値m_pAverage
には誤差が含まれている(かもしれない)ので、計算し直すことで誤差が累積していきます。気になるのであれば、平均濃度値の代わりに合計濃度値を用いれば良いでしょう。二つの領域の左上と右下の座標の大小は式1から求められます。左上ほど小さな値になり、右下ほど大きな値になります。
x座標 + y座標 * 画像の幅
// 番号付け(統合する時に呼び出す) void SplitMerge::labeling(DWORD number1,DWORD number2) { // ================================================== #1 DWORD sml,lrg; if(m_pSize[number1]<m_pSize[number2]) { // 面積の小さい領域と大きい領域の番号 sml=number1; lrg=number2; } else { sml=number2; lrg=number1; } // ================================================== #2 DWORD x,y; DWORD xs,xe; for(y=m_pStartY[sml];y<=m_pEndY[sml];y++){ if(y==m_pStartY[sml]) xs=m_pStartX[sml]; else xs=m_l; // 処理領域の左端 if(y==m_pEndY[sml]) xe=m_pEndX[sml]; else xe=m_r; // 処理領域の右端 for(x=xs;x<=xe;x++){ if(m_pLabel[x+y*m_w]==sml){ // 面積の小さい領域を置き換える m_pLabel[x+y*m_w]=lrg; } } } // ================================================== #3 double d=m_pAverage[sml]*m_pSize[sml] +m_pAverage[lrg]*m_pSize[lrg]; DWORD u=m_pSize[sml]+m_pSize[lrg]; m_pAverage[lrg]=d/u; // 新しい領域の平均濃度値 m_pSize[lrg]=u; // 新しい領域の面積 m_pAverage[sml]=0; // 統合されたので無効な領域とする m_pSize[sml]=0; // 統合されたので無効な領域とする if(m_pStartX[sml]+m_pStartY[sml]*m_w < m_pStartX[lrg]+m_pStartY[lrg]*m_w) { // 新しい領域の左上座標 m_pStartX[lrg]=m_pStartX[sml]; m_pStartY[lrg]=m_pStartY[sml]; } // 統合されたので無効な領域とする m_pStartX[sml]=0; m_pStartY[sml]=0; if(m_pEndX[sml]+m_pEndY[sml]*m_w > m_pEndX[lrg]+m_pEndY[lrg]*m_w) { // 新しい領域の右下座標 m_pEndX[lrg]=m_pEndX[sml]; m_pEndY[lrg]=m_pEndY[sml]; } // 統合されたので無効な領域とする m_pEndX[sml]=0; m_pEndY[sml]=0; }
微小領域の統合
微小領域の統合処理では、隣接している領域を見つけることが肝になります。隣接している領域は自領域の輪郭をトレースすることで見つけます。そして、全ての隣接している領域の中で、最も性質が近い領域と統合します。
// 面積の小さな領域を統合 void SplitMerge::absorb(void) { if(m_minSize==0) return; DWORD a,b,n; do{ n=0; for(b=1;b<m_number;b++) { if(m_pSize[b]>0 && m_pSize[b]<m_minSize) { // 隣接する最も平均濃度値の近い領域の番号を返す a=trace(b); if(a){ labeling(a,b); n++; } } } } while(n); }
輪郭トレース
輪郭をトレースする手順は以下のようになります。
- トレースの開始座標を設定する
- 現在の座標を中心に、左回り、または右回りに、他領域から自領域の番号に変化する位置を探す
- 変化した位置が輪郭なので、現在の座標を変化した位置に更新する
- 上記「2.」、「3.」の処理を、トレースの開始位置に戻ってくるまで繰り返す
図4をご覧下さい。色付きの領域が自領域です。5番(現在の座標)を中心に、2番から左回りに調べた場合を考えてみましょう。2→1、1→4、4→7、と調べて、7→8 で他領域から自領域に変化しているので、7番の位置が輪郭だとわかります。
実際には、常に同じ番号から調べ始めるわけではなく、新しい輪郭の位置に応じて、調べ始める番号を設定します。これは高速化のためというより、無限ループに陥ることを回避するためです。詳しくは『C言語で学ぶ実践画像処理』(オーム社)などをご覧下さい。
プログラムはちょっと面倒ですが、ポイントは、switch
文のcase
で輪郭が見つかるまでbreak
を実行しないことです。この部分は分割・統合法とは直接関係ないので、読み飛ばしても構いません。
// 隣接する最も平均濃度値の近い領域の番号を返す DWORD SplitMerge::trace(DWORD target) { int xStart=m_pStartX[target]-1,yStart=m_pStartY[target]; int x=xStart,y=yStart; DWORD from=0,to,n=0; BYTE vec=2; // 次に調べる位置 double sub,min=256; DWORD answer=0; // 左回りに輪郭トレース while(true) { if(n && x==xStart && y==yStart) // 始点に戻ってきた { return answer; } if(n>m_w*m_h) return 0; // 無限ループの可能性大 // 優先順位を付けて調べる // (闇雲に調べると無限ループに陥る危険あり) switch(vec) { case 6: // 右中が輪郭かな? from=(x+1>=0 && x+1<m_w && y>=0 && y<m_h)? m_pLabel[x+1+y*m_w]:0; to=(x+1>=0 && x+1<m_w && y-1>=0 && y-1<m_h)? m_pLabel[x+1+(y-1)*m_w]:0; if(from!=target && to==target) { x=x+1; /* y=y; */ vec=7; break; } case 3: // 右上が輪郭かな? from=(x+1>=0 && x+1<m_w && y-1>=0 && y-1<m_h)? m_pLabel[x+1+(y-1)*m_w]:0; to=(x>=0 && x<m_w && y-1>=0 && y-1<m_h)? _pLabel[x+(y-1)*m_w]:0; if(from!=target && to==target) { x=x+1; y=y-1; vec=8; break; } case 2: // 中上が輪郭かな? from=(x>=0 && x<m_w && y-1>=0 && y-1<m_h)? m_pLabel[x+(y-1)*m_w]:0; to=(x-1>=0 && x-1<m_w && y-1>=0 && y-1<m_h)? m_pLabel[x-1+(y-1)*m_w]:0; if(from!=target && to==target) { /* x=x; */ y=y-1; vec=9; break; } case 1: // 左上が輪郭かな? from=(x-1>=0 && x-1<m_w && y-1>=0 && y-1<m_h)? m_pLabel[x-1+(y-1)*m_w]:0; to=(x-1>=0 && x-1<m_w && y>=0 && y<m_h)? m_pLabel[x-1+y*m_w]:0; if(from!=target && to==target) { x=x-1; y=y-1; vec=6; break; } case 4: // 左中が輪郭かな? from=(x-1>=0 && x-1<m_w && y>=0 && y<m_h)? m_pLabel[x-1+y*m_w]:0; to=(x-1>=0 && x-1<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x-1+(y+1)*m_w]:0; if(from!=target && to==target) { x=x-1; /* y=y; */ vec=3; break; } case 7: // 左下が輪郭かな? from=(x-1>=0 && x-1<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x-1+(y+1)*m_w]:0; to=(x>=0 && x<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x+(y+1)*m_w]:0; if(from!=target && to==target) { x=x-1; y=y+1; vec=2; break; } case 8: // 中下が輪郭かな? from=(x>=0 && x<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x+(y+1)*m_w]:0; to=(x+1>=0 && x+1<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x+1+(y+1)*m_w]:0; if(from!=target && to==target) { /* x=x; */ y=y+1; vec=1; break; } case 9: // 右下が輪郭かな? from=(x+1>=0 && x+1<m_w && y+1>=0 && y+1<m_h)? m_pLabel[x+1+(y+1)*m_w]:0; to=(x+1>=0 && x+1<m_w && y>=0 && y<m_h)? m_pLabel[x+1+y*m_w]:0; if(from!=target && to==target) { x=x+1; y=y+1; vec=4; break; } vec=6; continue; // 一番上へ } if(from) // 0 以外なら有効な領域の番号 { sub=fabs(m_pAverage[target]-m_pAverage[from]); if(sub<min){ min=sub; answer=from; } } n++; // 輪郭の画素数(周囲長ではない) } }
描画
描画はPaintLine
メンバ関数が担当しています。ウィンドウプロシージャはWM_PAINT
メッセージを処理するごとにPaintLine
メンバ関数を呼び出します。描画にはSetPixel
関数を用いていますが、SetPixel
関数は低速なので、メインスレッドに割り当てられた時間内に処理が終わらないことが多々あります。DIBに描画した方が遙かに高速なのですが、使い勝手を優先して、このようにしました。
// 分割線を描画(本当は線じゃなくて点) void SplitMerge::PaintLine(HDC hdc,COLORREF color) { if(m_pLabel==NULL) return; // error if(m_r==0 || m_b==0) return; // error DWORD x,y; // 横方向の境界に点を打つ for(y=m_t;y<=m_b;y++){ for(x=m_l;x<=m_r-1;x++){ if(m_pLabel[x+y*m_w]!=m_pLabel[x+1+y*m_w]){ SetPixel(hdc,x,y,color); } } } // 縦方向の境界に点を打つ for(x=m_l;x<=m_r;x++){ for(y=m_t;y<=m_b-1;y++){ if(m_pLabel[x+y*m_w]!=m_pLabel[x+(y+1)*m_w]){ SetPixel(hdc,x,y,color); } } } }
0
またはNULL
だと思い込んでいる方もいるかもしれません(過去の私だけかもしれませんが)。しかし、クラス型変数も普通の変数と同じで、動的変数として宣言されれば不定値で初期化され、静的変数として宣言されれば0
またはNULL
で初期化されます。従って、初期化処理は省略しちゃいけないよ、という教訓を得たのでした。ウィンドウプロシージャ
下記のプログラムは抜粋ですが、このようにSplitMerge
クラスは使います。BmpIO
クラスの主なメンバ関数については付属のドキュメントをご覧下さい。
static BmpIO s_bio; static SplitMerge s_sm; case WM_KEYDOWN: s_bio.LoadPicture(hWnd,BIT24); s_sm.SetImage(s_bio.GetPixel(),s_bio.GetWidth(), s_bio.GetHeight(),s_bio.GetBit()/8); s_sm.SetThreshold(2,0.9,10,48); s_sm.SplitAndMerge(0,0,s_bio.GetWidth()-1,s_bio.GetHeight()-1); return 0; HDC hdc; PAINTSTRUCT ps; case WM_PAINT: hdc=BeginPaint(hWnd,&ps); BitBlt(hdc,0,0,s_bio.GetWidth(),s_bio.GetHeight(), s_bio.GetHDC(),0,0,SRCCOPY); s_sm.PaintLine(hdc,RGB(255,0,0)); EndPaint(hWnd,&ps); return 0;
まとめ
領域分割の主な目的は、分割して得られた領域の形、つまり対象物の概形からそれが何であるか、を判断することです。ピクセル単位の変化は重要ではありません。従って、分割・統合法においても、精度を上げたいからと細かく分割して統合すれば良いというものではありません。過分割で概形が崩れる危険があるからです。最適な分割および統合の閾値を探すには、用途に合った実験を行って調べていくという地道な作業が必要です。
概形を得るための別のアプローチとしては、画像自体をぼかしてから処理するという方法もあります。この場合は、ガウスフィルタなどで大きくぼかした画像を処理するとよいでしょう。
今回は、分割および統合の判断基準となる画像の性質として、濃度値を用いました。しかし、単一の色成分だけを用いた場合、細かく分割して統合した結果と減色処理の結果とが同じになり、お勧めできません。結果は同じになるのに、処理時間は何倍も遅くなるからです。
同様に、適度に分割したとしても、同じような結果になります。処理に時間がかかる、という欠点を補うためにも、例えば、複数の色成分を用いるといった、分割・統合法の利点をもっと活かすべきです。
色そのものにこだわる必要もありません。色のパターン、つまり模様に注目したり、その形に注目してみるのも面白いでしょう。
他にも、ヒストグラム以外の判定基準を用いたり、微分した画像の特徴量を用いてみたり、など色々と工夫してみて下さい。個人的には、色そのものを用いた以外の方法を開発して欲しいです。
お分かりのように、分割・統合法の欠点は、処理に時間がかかることです。いろいろ高速化は可能ですが、分割や統合の判定方法が複雑になるにつれて、処理時間も増加します。しかし、複雑で自由な条件を設定できる事こそが分割・統合法の利点なので、他の手法では真似できないような条件を設定し、処理に時間がかかるという欠点を補いましょう。
最後に、分割・統合法の使用例として、人物画像から服を抽出するという処理ができるかどうか考えてみましょう。ここでの人物画像とは、背景を除去して人物だけが写っている画像のこととします。人物は正面を向いて立っており、姿勢は自由とします。
この問題の第一目的は、服の概形を得ることなので(模様や柄の抽出は、とりあえず考えません)、当然、シャツとズボンは別々の領域として分割されなければなりません。しかし、色はもちろん、模様や柄、そして人物の姿勢によって形さえも様々な、シャツとズボンを別々の領域として分割することは、かなりの難問になります。
実は、様々な要因に左右されない性質が見つかっていないのが現状であり、同時に、分割・統合法の限界を感じさせます。研究は現在でも行われていますので、皆さんも一緒に考えて頂ければと思います。
皆様のご意見・ご感想を是非ともお聞かせ下さい。フォーラムまでお願いします。
参考資料
- 『画像工学(増補)-画像のエレクトロニクス-』 南敏・中村納 著、コロナ社、2000年4月
- 工学院大学 平成15年度卒業論文 『新しいテクスチャ特徴を用いた領域分割の検討の検討』 平尾卓也 著
- 『C言語で学ぶ実践画像処理』 井上誠喜・八木伸行・林正樹・中須英輔・三谷公二・奥井誠人 著、オーム社、1999年11月