ATmega128(AVR)とCMOS-EYEで線化画像処理
|
はじめに
ここでは AVRの中の、ATmega128を搭載したマイコンボード BTC068(ベストテクノロジー製)と浅草ギ研製CMOSイメージセンサーボードCMOS-EYEを使って、線化画像処理を行います。
尚、このページは「ATmega128...バス接続及びシリアル接続」のページので作成した環境を使用しています。ハード接続、画像へのアクセス方法などは事前にこちらを参照願います。また、線化処理の前段階として、ニ値化処理を行う必要があります。ニ値化処理とその関数については、「ATmega128...ニ値化画像処理」のページを参照願います。
※このページで紹介する内容はあくまでも一例です。個別の作成のご相談ご質問はお答えできませんのでご了承下さい。このページと同じ内容についてのご質問についてはロボット掲示板にてお願いいたします。
用語について
このページで説明する用語について解説します。
CMOS-EYEはマイコンと接続することが前提となっております。以下の説明で「マイコン」と書いたところは、CMOS-EYEに接続するユーザー側のマイコンを示します。マイコン側の端子名は極力、そのマイコンのデータシートに書かれている名前を記載しています。
マイコンチップが載ったCPUボードを「マイコンボード」とします。マイコンボードの端子名は極力、そのマイコンボードの取扱説明に書かれている名称を記載しています。
CMOS-EYE上にも制御用のマイコンが搭載されていますが、これは「CMOS-EYE上のマイコン」とします。(バス接続時には特にこの「CMOS-EYE上のマイコン」を意識する必要はありません。)
CMOS-EYEからはいろいろな端子が出ています。アドレスを指定する端子の集まりを「アドレスバス」とします。又、データを指定したり受けたりする端子の集まりを「データバス」とします。
線化画像処理についての概要とソースプログラム
まず、ソースはこちら。
CMOSEYE_ATmega128_Line.c (右クリックで保存)
文字などの画像をニ値化した場合、そのままだと線が太すぎて計算するのが大変になりますので、文字判定などを行う前に「線幅1の細い線」に変換する場合が多いです。線幅1の線にしておけば、後段で「線の端が何個あるか?」、「交差する点があるか?」、「角度が一定以上の点があるか」などの判定が可能になります。
線化するには画面を1ピクセルづつスキャンし、そのピクセルの周囲の状況によって自分のピクセルが消せるか?を判定していきます。自分の周りのピクセルよって計算を行うことを「近傍(きんぼう)処理」と言います。
自分が消せるかの判断材料として、自分の周りに何個の図が接しているかということを計算する必要もあります。この何個接しているかを「連結数」と言います。
また、自分が消せるかを判断するのに、前回行った処理を記憶しておいて、その状況により判断することも必要になります。マイコンには、沢山の画面のデータを記憶するほどのRAM容量はありませんので、前回処理の記憶はCMOS-EYEのSRAM上に展開することになります。
これらの詳細は次節から説明します。
元画像−>ニ値化−>線化 の例
以下では、二値化した後の画像について説明します。「画素がある」、「対象がある」と書いてあるところは黒(0)ピクセルを表します。その他は背景の白(255)になります。一部、対象があるピクセルを色わけで表示しているところもありますので注意して下さい。実際には黒(0)か白(255)の2つの値しか扱いません。
周囲8近傍処理
自分に接する周囲のピクセルは8個あります。この8個を使って計算するのを「8近傍処理」と言います。
周囲8ピクセルは1ピクセル8ビット長のデータを持っているので unsigned char 型の8個の配列変数に入れて処理します。この場合、for文などでループをまわす場合にどの位置をどの配列番号に入れるかという問題があります。画面を左上からスキャン処理していく場合(ラスタスキャンと言ったりします。)に、「自分位置から反時計回りに処理」することが多い(詳しくは画像処理の書籍などを参照)ので、配列番号は右から反時計周りに、次の通りにします。
配列は0番から始まるので終りは7番、で8個の配列です。
(配列番号の付け方)
CMOS-EYEの場合は、1画面が1行づつの連続した場所に格納されます。下は80x60白黒の場合の格納イメージです。
よって、自分の位置の上のピクセルのアドレスは「自分のアドレス−画面横幅」、下のピクセルは「自分のアドレス+横幅」となります。
ということで、8近傍の値を取得する関数を下のように作りました。
配列はポインタ渡しになっています。例えば、今、1000番地の周囲8ピクセルの値を取得したいとします。画面は80x60だったとします。呼び出し元プログラムで、
unsigned char a[8];
という配列を作っておき、この中に8近傍データを入れたい場合は、
unsigned char* pa;
pa = a;
getPix8(pa, 1000, 80);
とすると、a[ ] 配列に周囲8近傍データが入ります。
尚、次に説明する連結数の計算のために、配列は9個の長さにしておいた方が良いです。配列を1個増やして宣言して、上のプログラムを
unsigned char a[9];
としても結果は変わりません。最後の a[8] に入る値は不定となります。(が、a[0]-a[7]までしか見なければ良い)
連結数の計算
自ピクセルに、何個の画像がくっついているか?を「連結数」と言い、近傍処理では多用します。どのように使うかは後の節で説明するとして、ここでは、連結数の計算方法を説明します。
まずは、8近傍でどのような連結数のパターンがあるか考えます。下のパターンは一例で、それぞれ左右や上下が反対になった場合なども考えられます。
<連結数のパターン>
連結数は「自分のピクセルの周りをスキャンし、そのピクセルが黒で、一個前のピクセルが白だった個数」を計算することで求められます。
今、8近傍のデータを取得したとします。ニ値化画像なので値は黒か白しかありません。配列は右から時計回りに番号がついていますが、0番から始めると「1つまえ」が判定できないので1番から反時計周りにスキャンします。最後は一個足りないので、8番の配列を追加して0番をコピーしておきます。
黒は0、白は255のニ値になります。この部分を関数化したのが下になります。
これも、配列をポインタで渡してます。1番の配列から初めて8番の配列で終わりますので行343のfor文はi=1から始めます。最後の値は行341で0番をコピーして追加しています。
画面コピーの関数
プログラムを作っていく中で、画面をコピーして保存するという処理が多かったのでこの部分も関数化しました。
fmAddrに格納されている画像を toAddrへコピーします。1画面文ループしてコピーしますので、1画面の大きさをmySizeで渡しています。
DATA_DIR、getPixData関数、setPixData関数などは前のページを参照願います。この関数を動かす前にCEをLOWにする必要があります。
データバスの方向切替や、CEの処理を getPixData や、setPixData 関数内で行えばよいと思うかもしれませんが、その通りでした。バス方向切替は関数外で行った方が処理がダブらないで良いか?とおもってこのようにしてましたが、プログラムを書いている時にバス方向切替忘れなどでハマることが多かったです。SRAMに対して読み書きできない?と思ったらバス方向切替忘れの可能性がありますので注意して下さい(よくあったので。)。データ方向切替がダブっても、そんなにスピードには影響しないようです。
線化画像処理のロジック
前置きが長くなりましたが、いよいよ線化処理の本体部分です。このロジックは少々複雑なので、プログラム経験が少ない方は理解に時間がかかるかもしれませんので、根気よく読んでください(私も作るのに非常に時間がかかりました。)。
基本的な動きは、画面を左上からスキャンしていって、黒の場合に、そのピクセルが白にできるか?という処理の繰り返しになります。元画像はすでに二値化されているものを使います。黒が値0、白が値255になっています(Viewerで見たときに見やすいので。Viewerについては前ページを参照。)
元画像の例。(二値化済み)
1)周囲の黒ピクセル数を検討する
まず、周囲8近傍で黒が何個あるか?という条件を見てみます。周囲8近傍に黒がある場合の例を下に示します。
各個数の例はあくまでも一例で、これ以外にも沢山のパターンがあります。まずは消去法でいきます。
1個の場合:必ず自ピクセルが端になるので、白にできない。
2個の場合:上の例の場合は、白にしても斜め線が保存されるので白にできる。他の場合は不明。
3個の場合:上の例の場合は白にできるが、他の場合は不明
4個の場合:上の例の場合は白にできるが、他の場合は不明
5個の場合:上の例の場合は白にできるが、他の場合は不明
6個の場合:上の例の場合は白にすると右上のピククセルがヒゲのように成長する可能性が高い。他のパターンの場合は、線が切断されてしまう。
7個の場合:7個はこのパターンしかない。右上と右下方向にヒゲが成長する。
8個の場合:8個はこのパターンしかない。白にすると穴があくので白にできない。
となります。ここで、
■絶対白にできない:1個、7個、8個
■微妙:6個
■白にできるパターンがある:2〜5個
となります。尚、0個の場合、つまり自ピクセルだけが黒の場合は雑音なので白にして消します。
2)連結数を検討する
ここまでで、周囲8近傍の黒数は2〜5個に限定して考えます。次に、連結数を見てみます。下は2〜5個時の例で、連結数を表示したものです。これらを見ると「連結数=1以外は線が切断される」ということがわかります。よって、自ピクセルを白にできるかの判断に「周囲8近傍の連結数が1」という条件を追加します。(下の例以外にもパターンがありますが、全て、連結数1以外は切断されるようになります。)
3)過去のスキャン情報を検討する
これら2つの検討で線化できそうな気配ですが、実は、左上からスキャンしてくるので、今までのスキャンですでに周囲が白に変わっている場合があります。よって、今まで白にしていたかどうか?というのも検討する必要があります。
過去情報を検討しなかった場合の例(この他にもいろいろなパターンがあります。)
上3つと左が白にされていた場合でも、その結果、連結数が1であれば線は切れません。よって、線が切れない条件として「元画像の上3つと左を白にしてみても連結数=1の場合には白にできる」という条件を追加します。
ここで、「今まで白にしていた可能性のあるピクセル」は、左上からスキャンするので上3つと左だけを検討すればよいことになります。
このように過去の情報も検討する必要があるので、線化の場合には2画面分の領域をSRAM上に展開して計算する必要があります。
4)線化が終了したかの判定
ここまでの作業で、太い線の周り1ピクセルが削られます。元の線が2〜3ピクセル以上の太さだった場合は一回のスキャンでは終わりませんので、「白にできるピクセルを発見できなくなるまで続ける」という条件を追加します。
実際のプログラムに移ります。ここでは、元画像のある領域の先頭アドレスを stAddr 作業領域(結果格納領域)の先頭アドレスを workAddr
とします。元画像であるニ値化画像は別に保存しておく必要がないので、一回目終了後に、workAddr をstAddrにコピーして、同じ作業をつづけます。(これにより、元のニ値化画像は失われる。)元の二値化画像を残したい場合はあらかじめ別な領域にコピーしておくか、作業領域をもう一つ使うか、ということになります。
ということで線化部分を関数化したのが下の部分です。
細かい説明は今までのことと、コメントを参照してください。
画像処理結果
実際に、ソースプログラムを動かしたものを、CMOS−EYE Viewerで見てみます。
撮影画像(0番地から格納)
元画像があったところ(4800番地から格納)
結果画像(9600番地から格納)
ということで無事、線化ができました。
4800番地には元は0番地の二値化画像が入っていましたが、処理の途中で結果画像と同じになります。念のため、このサンプルプログラムでは二値化後に19200番地に二値化画像をコピーしておきましたのでそれも表示します。
元画像(19200番地へ事前にコピーしておいたニ値化画像)
気になる時間ですが、前ページまでのようにTEST端子をオシロで測定して、線化関数の前後でTESTをHIGH/LOWさせて時間を計測しました。
処理時間は元のニ値化画像の線の太さに大きく左右されます。二値化後に黒が多いと時間が多くかかります。この画像の例ですと、写真のように約3.4秒かかっています。このように、処理内容が複雑だと処理時間は結構かかることになります。
ちなみに、H8Tinyで同じような画像を処理した場合は大体5秒ぐらいかかっていました。
おわりに
今回はニ値化画像の線化を行いました。CMOS-EYEの内蔵プログラムでは、線化画像から端点、分岐点、交差点の数と位置などを計算するものもあります。
端点は、線化が終わった画像をもう一度スキャンすると、連結数が1の点が端点となります。分岐点は連結数3、交差点は連結数4で求められます。
交差点は、線化の過程で、2ピクセルぐらいの距離の分岐点x2になることもありますので、分岐点が接近しているところは場合によっては交差点1つにまとめる必要があります。また、それ以外でも色々と、すんなりいかないケースがありますので注意して下さい。
線の方向が急激に変わるところを角として検出することも可能です。
線化画像後にこのような処理を行うことで、線図形の特徴を抽出して、文字検出など、その図形がどのようなものかをプログラムで判定することもできるでしょう。これらについてはここでは解説しませんが、画像処理本などを参考に挑戦してみてください。
2006年9月12日
|
|
|