プログラミング演習3
集中講義版
-2日目資料&課題
花泉 弘
この回の目標
1.画像ファイルの構造を知る
2.画像ファイル(バイナリファイル)を読み込む
・読み込みには1日目に作成したreadcharline( ) を使用します
3.画像を作成して出力する
4.読み込んだ画像データを処理できる
・画像上の座標(ラインとカラム)で画素を指定して濃度値を得る
・指定された領域の画素濃度の平均値をR,G,Bごとに求める
1.画像ファイルの中身を知る その1
・・・ ・・・ ・・・ ・・・ ・・・ 白黒画像のイメージ: 画素が並んでいる それぞれの画素は濃淡値を持つ 0~255 (データは1byte/画素) 幅(width) 高さ (he ight ) カラー画像の場合は、三原色の 組合せで色を表すため、白黒画像の 3枚分の容量が必要になる。 カラー画像では、白黒画像の画素1個に対して、対応する RGB各成分合わせて3つ分の数値が連続して記録されている (RGBの順)。これらの数値を画素濃度と呼びます。それぞれ 赤緑青の波長帯(spectral band)における観測値なので、これ らの画像を3バンドデータと呼んだり、バンド数は3である、な どと言います。センサによっては可視から近赤外までN個の波 長帯で観測するものもあり、これらはNバンドデータと呼ばれ ます。 画像の情報 ・ヘッダー情報:画像のサイズ(幅と高さ)や1画素のビット数、バンド数(カラー 3、白黒1) ・データ本体:幅×高さ×バンド数分のバイナリデータ(テキストではない) これらの情報がファイルに記録されている2.画像ファイルの構造を知る その2
pnm ファイルとよばれる種類の画像を例にとって、ファイルの構造を見ていきます。 pnm ファイルには、pgm ファイル、ppm ファイル、pbm ファイルがあります。 ・pgm ファイル:白黒画像で1画素を8ビット(0~255、0x00 ~ 0xff)で表す。拡張子は .pgm ・ppm ファイル:カラー画像で3バンド、8ビット/画素。拡張子は .ppm ・pbm ファイル:2値画像。1ビット/画素。拡張子は .pbm 構造:ファイルの初めにヘッダー部(テキストデータ)があり、 続いて画像データ(バイナリデータ)が順に詰まっている ヘッダー部 画像データ (1024 x 768 x 3 bytes) P6↓ 1024 768↓ 255↓ マジックナンバー 6:ppm, 5:pgm, 4:pbm width height 画素値の最大値 この例では 1 byte/pixel3.画像ファイルの構造を知る その3
pmmファイルは、pnmファイル形式を拡張して、3つを超えた数の観測波長帯(バンド)の画像を 同じ形式で取り扱えるようにしたもので、 マジックナンバーは0、次の行に画像の幅、高さ、バンド数が入り、最後に濃度の最大値が 書かれています。 ヘッダー部 画像データ (1024 x 768 x 4 bytes) P0↓ 1024 768 4↓ 255↓width height band
画素値の最大値
例題2-1 画像ファイル(pnm)の構造を確認しなさい
①では、確認してみましょう。ブラウザを使って、好きな画像をダウンロードしてください。 以前ダウンロードしたものがあれば、それで構いません。 ②ダウンロードしたものが .jpg ファイルであれば、pnm ファイルに変換します。 ③画像の入っているディレクトリに移動 ④画像を確認。 .jpg ファイルです。 ⑤変換には convert を用います。 ⑥確認します。 .ppm と .pgm ファイルができて いることがわかります。 ⑦画像も見てみます。 *pnm ファイルとは ppm, pgm, pbm ファイルの総称です 拡張子で区別例題2-1 つづき
① less コマンドを使います。 ② 「y」 を入力します。 ③ヘッダー部が確認できました。 マジックナンバーの違いに注意 ④ 「q」 を入力して終了します。4.画像ファイル(バイナリファイル)を読み込む
画像ファイルのオープンには fopen()を用います。ここでは、バイナリファイルを 扱うので、fopen(file.ppm, “rb”) のように、「b」を追加して指定します。 テキストファイルは、「r」だったわけですが、違いがわかりますか? r:入力時は ‘¥r’,’¥n’ →’¥n’ へ変換、(ファイルへの)出力時には’¥n’ → ‘¥r’,’¥n’への変換 rb:何もしない の違いがあります。 ヘッダー部(テキスト) 画像データ(バイナリ) (1024×768×3 bytes) pnm/pmm形式の画像ファイルは左図の ように、最初にテキストデータがあり、その 後ろにバイナリデータがあります。 方針: テキストデータに関しては、行末の ’¥n’ に着目し、 その内容から読み込むべき後半の画像データの 個数を知り、その個数分バッファに蓄える。 注意: 画素データはunsigned char 型の配列に代入します。 (関数等の宣言もchar 型からunsigned char 型へ変更)チェック用の main 文 ex202/main_charline.c
例題2-2 画像データのヘッダー部を読み込む
あとで画像を読むので配列は大きめに bを追加して ここは3限定で pmm と ppmの違いがわかりますか? 作成した readcharline() を使って、pnm/pmm ファイルのヘッダー部を読んでみましょう。 指定したバイト数を読み込むだけでなく、1ライン分のデータも読めることがわかります。さて、ここまでの処理で pnm/pmm 画像のヘッダー部を文字列と
して読み込むことができました。ただ、文字列のままでは使えま
せんのでそれらを数値として認識することが必要です。
Cでは keyboard からの入力用の関数として scanf(“%d”, &k); を
用意していますが、それと同様にバッファからの読み取りには、
sscanf(buf, “%d”, &k); が使えます。
pnm/pmm 画像をから3回続けて1行分読み取ることを考えます。
buf には文字列としてそれぞれの回の読み込みに対して例えば
P6
1024 768
255
が入るので、1回目に1行分読み取ったところで、
buf[0] == ‘P’ が真なら pnm/pmm であることがわかり、さらに
buf[1] == ‘6’ が真なら ppm 画像で band = 3 であることがわかり、
buf[1] == ‘5’ が真なら pgm 画像で band = 1 とわかります。また、
buf[1] == ‘0’ が真なら pmm 画像なので、未決ということで、
仮にband = 0 としておきます。
2回目に1行分読み取ったところで、
band > 0 が真ならすでに pnm 画像であることがわかっているので
sscanf(buf, “%d%d”, &width, &height);
とすることで、width = 1024, height = 768 といった具合にint 型の
変数width と height に数値を格納することができます。
同様に、band == 0 が真であれば
sscanf(buf, “%d%d%d”, &width, &height, &band);
とすることで、width, height に加えて band にも正しい値が入ります。
3つの値がそろったら、続けて
3回目に1行分読み取ります。その時の値が255であれば、1画素
1バイトでこの画像が構成されていることがわかります。なお、この
値は65535 のこともあり、この場合は、1画素2バイトで構成されて
いることになります。当然、画像を読み込むバッファもunsigned char
ではなくて、unsigned short int になります。
話を戻します。3つの値がそろったら、読み込むべき画素数は
width * height * band のように求めることができます。この値を設
定すれば、buf に画像データを読み込むことができます。
例題2-3 画像データを読み込む
結局のところ、pnm/pmm ファイルを読み込むには、テキスト部を読み込んで ・ファイルの種類(pgm/ppm/pmm)を判別し(pgm/ppmの場合はバンド数がわかる) ・画像の幅と高さ(とpmmの場合は、バンド数)を知る。 ・濃度の最大値も知る 求めた幅、高さ、バンド数の情報から ・連続して必要なバイト数分だけデータを読み込む という流れで画像を読み込むことができる。 *imagefile : 画像ファイルの名前が格納された配列 *img : 画像を格納するバッファ(大きめに確保しておく; これまでは *buf であった) *width, *height, *band : 画像の幅、高さ、バンド数戻り値:正常読み取りはEXIT_SUCCESS、異常はEXIT_FAILURE(ファイルが存在しない、 pnm/pmmでない、読み込みエラー(個数分だけデータがない)) int readimage(char *imagefile, unsigned char *img, int *width, int *height, int *band)
を作成しなさい。仕様は以下の通り。main側からは
k = readimage(filename, img, &width, &height, &band); のように用いる。
以下、演習題ごとにディレクトリを区別して(例えば、ex202 など)に作成すること。
例題2-3(続き) 画像ファイルの読み込み
ex203/readimage.c 負数を与えて1行目を読む 正数を与えて画素数分読む 2行目を読む 4行目を読む ここは、unsigned char で例題2-3(続き) 動作確認(画像を表示させてみる)
使い方
$ imgout mountain.ppm | xv - & で画像が表示されたらOK.
いかがですか? 無事画像は出力されましたか?
ここまでで、一応は画像を読み込むことができるようになりました。
例題2-3 では、画像の出力の仕方についても少し書いてあったの
ですが、後ほど、詳しく見ていきます。
まず、ちょっと不便なところが残っているreadimage( )をもう少し便
利にすることを考えます。
不便な点とは、readimage( ) を呼んで width, height, band を知る
前に、画像を読み込むための配列の大きさを決定しなければなら
ない点です。width, height, band を知ってから配列を設定すること
ができれば、すごくすっきりするはずです。
読み込んだ画素データを格納する配列を動的に作成するには、malloc( )や calloc( ) を用い ます。画素濃度を格納する配列(ポインタ)を img とすると
img = calloc(割り当てる要素数, 1要素あたりのバイト数) か、もしくは img = malloc(必要バイト数)
のように用います。これらの例では要素数=width * height * band で、1要素あたりの
バイト数は sizeof(unsigned char) で求めます。malloc( ) の必要バイト数はそれらの積になり ます。どちらも、成功すると割り当てられたメモリ領域の先頭アドレスが img に格納され、失 敗すると NULL が格納されます。calloc( ) の場合には、メモリは全ビット0にクリアされます。 malloc( ) の場合は、何もしないので以前の内容が残ったままになっています。
calloc( ) や malloc( ) で割り当てられたメモリ領域が不要になった場合は、free( ) で開放
します。プログラムが終了すると自動的にfree( ) が行われるので、特に意識する必要はあり ません。以下のように用います。
If((img = calloc(width * height * band, sizeof(unsigned char))) == NULL){//メモリの割り当て
fprintf(stderr, “memory allocation error¥n”);
return (NULL); //これを含む関数の戻り値がunsigned char型のポインタなのでNULLを返す
}
何かの処理
free(img); //メモリの解放(通常は意識しなくてもよい)
5.動的なメモリ領域の割り当て
例題2-4 ファイル名を与えてポインタで受ける
演習2-3 では、画像の大きさを想定して、main 側で大き目の配列を用意していましたが、 ここでは、width, height, band が得られた後に、動的に配列用のメモリを確保します。 さっそく、calloc( ) を使ってみましょう。せっかくなので、readimage( ) を改造していきます。
int readimage(char *imagefile, unsigned char *img, int *width, int *height, int *band) を作成。main側からは
k = readimage(filename, img, &width, &height, &band); のように用いる。
unsigned char *readimage2(char *imagefile, int *width, int *height, int *band) を作成。main側からは
unsigned char *img; の宣言をした後、
img = readimage2(filename, &width, &height, &band); のように用いる。
ファイル名を与えて、幅、高さ、バンド数と共に、画像本体を受け取る形になり、 非常にすっきりする
改造前(例題2-3)
例題2-4 (続き)画像ファイルの読み込み (すっきり版)
ex204/readimage2.c
j 個の unsigned char 領域 (合計 j バイト)を確保
例題2-4 (続き) 動作確認(画像を表示させてみる)
使い方
$ imgout2 mountain.ppm | xv - & で画像が表示されたらOK.
例題2-5 さらに一般化する(#で始まる行の読み飛ばし)
pnm/pmm ファイルには、場合によっては、下の例のようにコメントが付加されるので、 コメントをスキップするように改造する(ex205 に readimage3()として作成する)。
例題2-5 (続き) #行の読み飛ばし
ex205/readimage3.c
do while 文で、行頭が#の 場合に読み飛ばす
例題2-5 (続き)動作確認(画像を表示させてみる)
ex205/imgout3.c
使い方
$ imgout3 mountain.ppm | xv - & で画像が表示されたらOK.
6.ここまで作成してきた画像読み込み関数の簡単なまとめ
readimage( ): 関数にファイル名を渡す。関数側でファイルをオープンし、読み取った画像のサイズ情報 に基づいて、main 側で定義した配列に画素濃度を格納して戻る。 readimage2( ) 関数にファイル名を渡す。関数側でファイルをオープンし、読み取った画像のサイズ情報 に基づいて、関数側で画素濃度を格納する配列にメモリを動的に割り当て、その配列の ポインタを戻す。 readimage3( ) 関数にファイル名を渡し、関数側でファイルをオープンする。画像のサイズ情報を読み 取る際に、画像ヘッダー部の#を読み飛ばす。関数側で画素濃度を格納する配列に 動的にメモリを割り当てその配列のポインタを戻すところは readimage2( ) と同じ。 readimage2( ) 以降では、画素濃度を格納する配列を動的に作成していますが、malloc( )や calloc( ) を用います。画素濃度を格納する配列(ポインタ)を img とするとimg = calloc(割り当てる要素数, 1要素あたりのバイト数) / img = malloc(必要バイト数) のように用います。これらの例では要素数=width * height * band で、1要素あたりの
バイト数は sizeof(unsigned char) で求めます。malloc( ) の必要バイト数はその積になります。 どちらも、成功すると割り当てられたメモリ領域の先頭アドレスが img に格納され、失敗する と NULL が格納されます。calloc( ) の場合に限り、メモリは全ビット0にクリアされます。
7.画像ファイルの出力
これまで、何気なく画像を出力してきましたが、読み込んだ画像ファイルや処理した結果を 画像として表示させたり、ファイルとして保存する方法として、以下の3つの手法を試してみ ます。 1.標準出力に書き出す(今まで使っていたもので、リダイレクトを使って、ファイルに保存し たり、パイプを使って表示ソフトへ流し込んだりできます)。 2.ファイルをオープンして書き出す 3.popen()を使って、表示ソフトへ直接送り込む 1.標準出力に書き出す ファイルが生成 リダイレクト パイプ いろいろに使えて便利 だが、ファイルは1個しか 出力できない。 ソースは次ページ imgout は次ページ参照例題2-6 標準出力に書き出す
上で作ったものを 早速使っています この部分が出力部 メモリの開放を 忘れずに。 ただし、この例の ようにすぐに終了 するのであれば、 不要です。次の 2,3の例でも同様 です。 この内容をex206/imgout4.c に 保存し、$ gcccomp imgout4 ↓ 動作確認 4 4例題2-7 ファイルをオープンして書き出す
ファイルをオープン ファイルクローズ printf() が fprintf(fp, ) になっただけ 動作確認 ex207/imgout5.c に保存例題2-8 popen()を使って、表示ソフトへ直接送り込む
慣れないと感じがつかめ ないかもしれませんが、 大変便利なものです。 動作確認 コマンドをオープンする形です。 その他は、fopen()と変わりません。 ただし、画像が出たら処理はそこで ストップしています。xvを終了させ ないと、次に進みません。 ex208/imgout6.c に保存8.画像データの処理
-画素データへのアクセスー
…..
unsigned char *img, *pa, *pb, *pc, *pd; int I, j, k, width, height, band; ………. img = readimage3(…. x y width * band heigh t (xs, ys) (xe, ye) ① ② ③ 左のような宣言と画像の読み込みを行った とすると、読み込まれ画像データは、左下の ようになっている。
(xs, ys) – (xe, ye) で決まる矩形領域内の 画素へのアクセスを考える。 ポインタ img は画像データの開始点を 保持しているので、別の変数を使用する。 3種類のポインタを使用する。 pa : 初期値が②ysで、ループが回るたびに 次の行④へ移動する(+= width * band)。 pb : ③(②+band * xs)から始まり、band 数ずつ 進む。③の次は⑤に移動する(+= band)。 pc : pb ③から始まり、band の数だけ 増分1で移動する③ の次は⑥(+= 1)。 pa = img + ys * width * band; // 初期値
pa += width * band; //増分 pb = pa + xs * band; // 初期値 pb += band; //増分 pc = pb; // 初期値 pc++; //増分 ⑤ ④ ⑥
例題2-9 画像ファイル入力し、ネガポジ変換した
画像を表示させなさい。
ある画素の濃度(ピクセルの持つ値)をd とするとき、ネガポジ変換はその画素 の濃度を255 – d とすることである。
画素値のアクセスは3重ループを用いる。
for( i = 0; i < height; i++){ //line 数に対するループ
for( j = 0; j < width; j++ ){ //column 数に対するループ for( k = 0; k < band; k++){ //band 数に対するループ
ここに処理の手続きを書く
} } }
例題2-9 (続き) stdout へ出力する方法で行います
ex209/nega.c
動作確認
画像全体なので
i = 0, j = 0 かつ i < height, j < width
(xs, ys) – (xe, ye) の矩形領域なら i = ys, j = xs かつ i <= ye, j <= xe
x y width * band heigh t (xs, ys) (xe, ye) ① ② ③ 3種類のポインタを使用する。 pa : 初期値が②ysで、ループが回るたびに 次の行④へ移動する(+= width * band)。 pb : ③(②+band * xs)から始まり、band 数ずつ 進む。③の次は⑤に移動する(+= band)。 pc : pb ③から始まり、band の数だけ 増分1で移動する③ の次は⑥(+= 1)。 pa = img + ys * width * band; // 初期値
pa += width * band; //増分 pb = pa + xs * band; // 初期値 pb += band; //増分 pc = pb; // 初期値 pc++; //増分 ⑤ ④ ⑥ この部分は、非常に重要なので、もう一度ポインターの設定法とプログラム上で それらがどのようにふるまうのかを確認してください。 i = ys i <= ye J = xs j <= xe 画像全体 矩形領域