平成16年度前期 情報科学演習
今野担当回資料 3
画像処理プログラミングの基礎(空間フィルタ)


目次

画像の空間フィルタリングを題材として,汎用的な関数を作成して利 用するための C プログラミング技術を習得する。

1 ディジタル画像

コンピュータで扱われる画像は, 図 1 のように縦方向と横方向に碁盤の目のように 区切られたディジタル画像である。 ディジタル画像中の碁盤の目に区切られた小地区を画素(pixel)という。 各画素には整数の組$(i,j)$によって番地が与えられていて, 画像は各画素における明るさを表す数値(濃度)によって表現される。 濃度は一般に正の整数で表す 1
図 1: 画像の画素分解
\begin{figure}\begin{center}
\begin{tabular}{\vert c\vert c\vert c\vert c\vert ...
... (4,1) & (4,2) & (4,3) & (4,4) \\ \hline
\end{tabular} \end{center}\end{figure}

プログラム中でディジタル画像を取り扱うには二次元配列を用いればよい。 C では, 例えば

int img[64][128];
として二次元配列を宣言することにより, 縦64個,横128個の画素を持つ 画像を配列 img に格納できるようになる。 画素$(i,j)$における濃度はimg[i][j]で参照できる。

なお,二次元配列内の画像を表示するには,濃度に対応した明るさの光を画面に 点灯させる必要があるが,ここでは簡単化のために,濃度を数値として画面に出 力することによって,画像を表示したこととみなす。


2 空間フィルタリングのための積和関数の作成

2.1 空間フィルタリング

与えられた画像(原画像)に対してフィルタ処理を行い,原画像とは異なる画像 を得るための処理を,画像の空間フィルタリングと呼ぶ。以降,空間フィルタリ ングの対象となる画像を入力画像と呼び,フィルタリング後の画像を出力画像と 呼ぶ。

空間フィルタ

3 $\times $ 3 の空間フィルタの例として,ラプラシアン・フィルタを示す。 その構成は次のとおりである。
\begin{displaymath}
w = \left[ \begin{array}{rrr}
  0 & -1 & 0 \\
-1 & 4 & -1 \\
0 & -1 & 0
\end{array} \right]
\end{displaymath} (1)

空間フィルタにおける各数値をフィルタ係数と呼ぶ。

フィルタリングの手順

入力画像中の画素 $(i,j)$(画像の端点を除く)を中心とする 3 $\times $ 3 の 局所領域 (図 2) における各画素の濃度と,3 $\times $ 3 の 空間フィルタ上でこれらと対応する位置にあるフィルタ係数との積和を計算し, これを出力画像の点$(i,j)$における濃度とする。この処理は,入力画像を $s$ とし,出力画像を $o$ とすれば
\begin{displaymath}
o(i,j) = \sum_{k=0}^{2}\sum_{l=0}^{2} s(i+k-1,j+l-1)\ w(k,l)
\end{displaymath} (2)

を求めることである。ここでは,C の配列の添字と整合させるために,$w(0,0)$ が左上隅のフィルタ係数を,$w(2,2)$ を右下隅のフィルタ係数を表すこととし ている。

この積和計算を,画像の端点を除くすべての画素 $(i,j)$ に対して行うことで, 画像の空間フィルタリングが実行できる2

図 2: 画素$(i,j)$を中心とする 3 $\times $ 3 の局所領域
\begin{figure}\centering
\parbox{1em}{$\downarrow$\ \\ $i$}
\begin{tabular}{\v...
... $(i+1,j-1)$\ & $(i+1,j)$\ & $(i+1,j+1)$\ \\ \hline
\end{tabular}
\end{figure}

なお,ラプラシアン・フィルタによる空間フィルタリングは,入力画像の画素 $(i,j)$ を中心として,縦と横の 4 方向との差分をとるものであり,画像内の 図形のエッジ(明るさが急激に変化する場所)等を検出して出力するために用い ることができる。

2.2 積和計算の関数作成

空間フィルタリング処理では,フィルタ係数を変更することによって,様々な 画像処理を行うことができる。ここでは, 種々の空間フィルタリングプログラムで利用可能な積和計算の関数を作成し,そ の動作確認を行う。

問題 3-1

積和を計算する関数 productSum を,式 (2) の右辺 に基づき作成せよ。[je3-1.c]

そのために,まず図 3 の内容をファイル imgfilter.h に保存 せよ。次に,図 4の内容を imgfilter.c に保存せよ。 imgfilter.h と imgfilter.c は同じディレクトリに置くこと。 さらに,図 4において空白となっている,関数 productSum の 定義部を作成し,imgfilter.c を完成させよ。

図 3: ヘッダファイル imgfilter.h
\begin{figure}\centering
\small
\verbatiminput{srcgazo/imgfilter.h}
\end{figure}

関数 productSum は,積和計算の対象となる入力画像全体(2次元配列)を第 1 引数を通して,また空間フィルタの係数全部を第4引数を通して受け取ることと する3。productSum 内では,入 力画像の第 $i, j$ 画素を中心とする 3 $\times $ 3 領域と空間フィルタとの積 和を求めて,戻り値として返す。$i$$j$ は,各々,第 2 および第 3 引数 を通じて渡される。

図 4: 関数 productSum 作成用ソースファイル imgfilter.c
\begin{figure}\centering
\small
\verbatiminput{srcgazo/imgsample.c}\end{figure}

図 4には,関数 productSum の動作確認のために, productSum を呼び出して利用する関数 main の定義も含まれている。関数 main は,二次元配列 img に格納された画像と lap に格納された空間フィ ルタとの積和を,関数 productSum の呼び出しにより求めて表示する。

imgfilter.c をプログラムとして実行するには,

cc -DTEST imgfilter.c
として,コンパイルする必要がある。プログラムを実行し,結果が正しいこ とを確認せよ。

図 4の解説

#include
はプリプロセッサ命令の一つである。書式は
#include <file.h>
または
#include "file.h"
であり,ヘッダファイル file.h の内容を #include 命令の箇所に「取り込む」。プリプロセッサは,コンパイル の最初のフェーズで行われる処理を実行するものであり, #include 処理もコンパイルの最初の段階で行われる。ファ イル名を <file.h> で指定した場合には,処 理系によって定められた場所から file.h を探すのに 対し,"file.h" では,この命令が記述されたソースファ イルと同一のディレクトリから file.h の探索が始ま るので,自作のヘッダファイルを include するには後者を使 う。

ヘッダファイルは,複数のソースファイルで共通に利用する 関数のプロトタイプ宣言や記号定数の定義を記述するために 利用する。

多次元配列を受け取る関数
1次元配列を受け取る関数では,関数 の仮引数(パラメータ)に配列の要素数を明示する必要が ないのに対して,多次元配列では第2 次元以降の要素数を明示する必要がある。例えば,関数 productSum の第1仮引数が, int im[][MAXCOL]であるのはそのためである。

一方,配列を受け取る関数の実引数(関数呼び出しにおける 引数)では,配列の次元数に拘らず配列名のみ4を記述する。

#ifdef
はプリプロセッサ命令の一つであり,書式は
#ifdef name
である。この命令は,name が定義されている(ソース ファイル中に,既に #define name の記述があ る)ならば,対応する #endif までの間のコードを有 効にする。逆に name が定義されていなければ,コン パイラは #endif までのコードを無視する5

ソースファイル中に #define name と書いて name を定義するのと同じ効果は,コンパイル時にオプ ション -Dname を付加しても得られる。問題で は,imgfilter.c をコンパイルして,一つのプログラムとし て実行する必要があるので,関数 main の定義を有効にしな ければならない。オプション -DTEST を付けてコンパ イルするのは,そのためである。

imgfilter.c において,関数 main の定義をコンパイルオプ ション -D が無い場合に無効としているのは,後の問題にお いて,imgfilter.c 中の productSum を別のソースファイル から呼び出すためである。次の問題では,一つのプログラム を作成するために,複数のソースファイルにプログラムを分 割して記述し,これらを結合して実行ファイルを作成する (分割コンパイル)。その場合,同じ名前の関数(今 の場合は main)を,複数のソースファイルで定義することは 許されない6。 imgfilter.c 中の関数 productSum は,次の問題のためのプ ログラム用「部品」として作成しているのであり,同じファ イル内の main は,あくまで productSum のテスト用である。

配列の初期化
配列宣言時,配列の要素に値をセットする(初期化 する)ことができる。その場合,セットする値を { } で括 り,各値をカンマで区切る。2次元配列の場合には,図  4 のようにする。

3 エッジの抽出

3.1 入力画像ファイルの作成

図 5 は 濃度が 0,1,2 の 3 段階のごく簡単な ディジタル画像を標準出力に出力するプログラムである。 入力画像作成プログラム(図 5)を実行し,出力を適当な名前 のファイルに保存せよ。

図 5: 入力画像作成プログラム
\begin{figure}\centering
\small
\verbatiminput{srcgazo/rectimg.c}\end{figure}

3.2 エッジ抽出プログラムの作成

入力画像にラプラシアン・フィルタリングを行った上で,適当な後処理を施すこ とより,画像のエッジ抽出を行うことができる。

処理手順

  1. 2次元配列への入力画像の読み込み

    プログラム内で入力画像を生成せずに,既存のデータ画像を標準入 力から読み取ることとする。

    読み取りに scanf を使う場合, 区切り文字を含まない数字の並びを正しく読み取るには, 第一引数で数字の桁数を明示する必要がある点に注意。

  2. ラプラシアンフィルタリング

    imgfilter.c で定義した積和関数 productSum を用いる。フィルタ リング後の画像は,入力画像とは別の配列に格納する。

  3. 後処理

    ラプラシアンフィルタリングによって得られた画像からエッジ部分 のみを抽出するために,正の濃度を持つ画素を1 とし,それ以外の 画素では濃度を0とする後処理を行うこととする。

  4. エッジ抽出画像の表示

    標準出力への出力とする。

プログラム作成においては,上記の各処理部をプログラム中で分離して記述 することにより,プログラムの構造を明確にすることが重要である。

問題 3-2

入力画像作成プログラム(図 5)の出力画像を読み込んで,ラ プラシアンフィルタを用いたエッジ抽出を行って,結果を表示するプログラ ムを作成せよ。 [je3-2.c]

ソースファイルは imgfilter.c とは別に作成する。そのソースファイルから, imgfilter.c で定義した関数 productSum を呼び出して使う。 したがって,ソースファイルに productSum の定義を書かないこと。 プログラム作成に際しては,imgfilter.c 内の関数 main も参考にせよ。

一般に,関数を呼び出す(使う)ためには,その関数のプロトタイプ宣言が必要 であり,この問題の場合にはヘッダファイルをインクルードする必要がある。ま た,記号定数は,それを定義 (#define) したファイル内の,定義箇所以 降でのみ有効であるので,そのためにもヘッダファイルのインクルードが必要で ある。

複数のファイルに分割されたソースファイルから,実行可能ファイルを作成する ためにはコンパイラのコマンドラインの引数にソースファイルをすべて指定する。 例えば

cc laplacian.c imgfilter.c
等とする。

4 線分の検出

画像の中から必要な形状のみを強調する空間フィルタがある。 この種のフィルタの代表的なものとして線分を検出するフィルタを作成する。 例えば,縦線と横線を検出するフィルタを 3 $\times $ 3 の 空間フィルタで構成すると次のようになる。

\begin{displaymath}
w_v = \left[ \begin{array}{rrr}
0 & 1 & 0 \\
0 & 1 & 0 \\...
... 0 \\
1 & 1 & 1 \\
0 & 0 & 0
\end{array} \right] (横線)
\end{displaymath}

処理手順

エッジ抽出の場合と同様である。

線分抽出のための後処理

フィルタリング後に濃度が1より大きくなった画素では濃度を1とし, 濃度が1以下の画素では濃度を0として出力する。

問題 3-3

入力画像を読み込んで,縦線抽出を行うプログラムを作成せよ。このプ ログラムを使って,ラプラシアンフィルタリングでエッジ抽出を行った 後の画像を処理し,結果を確認せよ。 [je3-3.c]

画像の入出力に標準入出力を使うプログラムとしているので,パイプを使ったプ ログラムの実行が便利である。

問題 3-4

入力画像を読み込んで,横線抽出を行うプログラムを作成せよ。このプ ログラムを使って,ラプラシアンフィルタリングでエッジ抽出を行った 後の画像を処理し,結果を確認せよ。 [je3-4.c]



脚注

... 濃度は一般に正の整数で表す1
ここでは濃淡(グレースケール)画像のみ考える。 カラー画像の場合,各画素につき RGB 各色の濃度が必要である。
... 画像の空間フィルタリングが実行できる2
空間フィルタリングは 2 次 元の畳み込み演算である
... する3
C で配列を関数に渡す場合,正確には,配列要素すべてではなく 配列の先頭要素のアドレス(ポインタ)が渡される。
... 引数)では,配列の次元数に拘らず配列名のみ4
配 列名は,当該配列の先頭要素のアドレスを値とする定数であ る。
... までのコードを無視する5
C には __LINE__ や __FILE__ のような予め定義さ れている記号定数(定義済み名前)があるので, #define や コンパイラの -D オプションを使う場合,定義 済み名前と重複しないように注意する必要がある。処理系に 依存する定義済み名前もあるが,それらは通常 _ で始まる ので,自分で使う記号定数を _ で始めなければ問題無い。
... 許されない6
ただし,関数や変数のクラスを static にした場合には,同じソースファイル内からしかそれらを利 用できないため,異なるソースファイルにおける関数名や変 数名の重複は許される。しかしプログラムの本体である関数 main の重複はありえない。