CPP149 BMP画像の保存 |
|||||||||||||
![]() |
|||||||||||||
私は2008年の12月にC言語の本格的な学習を始めた。C言語の萌芽のころから、少しは気にしていたものの、BASICからF-BASICに移ることにより、ウィンドウ化の変化をなんとか吸収しようとしていたのだが、そのF-BASICも、そのあとの発展形を生み出さずに消えてゆくことになった。黒月解析研究所の最初の研究テーマは、スポーツの運動力学における立体的なシミュレーションだったが、これをF-BASICで行っていた。やがて、メインの研究テーマは、アインシュタインの特殊相対性理論の謎へと移ってゆき、コンピュータのキーボードによって、ほとんどWORDだけを操作することになった。2008年の11月ころに「幽霊変換」という論文を生み出すことで、一応の区切りを付けることができ、ここまでのことを普及させるという、次の段階のことを棚上げした。次は、一般相対性理論への反論材料となる、天文画像などの解析を行うため、BMP画像を取り込んで、この画像データを配列に書き込み、あれこれと加工して、より見やすくなるようにするということが目標となった。BMP画像を取り込むという処理をBASICで行えるかどうか分からなかったことと、C言語のテキストの中に、このことが行えると説明してあるページがあったので、C言語に関して、ほとんど初心者の状態であったが、学習を始めた。これらは、2008年の12月から2009年の2月ころのこと。C言語のプログラムでBMP画像を読み込み、それについて加工し、再び描写するということは、まもなく、できるようになった。すぐに、BASIC時代に学んであった、データ解析のノウハウを盛り込んで、いろいろな画像の中に潜んでいる、未知の事実を明らかにするということに集中した。このとき、読み込む画像をダイアログボックスで指定するという技術までは至っておらず、ペイントソフトを利用して、画像の名前をso.bmpと変え、この名前だけを読み取るプログラムの、加工部分だけを大きく膨らませていたのである。このようなダイアログボックスを利用できるようになったのが、2009年の8月ごろだったかもしれない。おそらく新技術と考えられるゴブリンアイを生み出し、これを、画像解析ソフトの大きな幹へと移植した後も、「BMP画像の保存」という、他の画像ソフトでは何ということもない技術が、私には、手の届かないものであった。 「BMP画像の保存」が成功したのは、ようやく10月になってからのことである。BMP画像を読みだして、より分かりやすく、見やすくなるように加工して、その解析結果を保存する。この流れを、ようやく完結することができた。このことが、かなり難しいことであったのは、もちろん、私の学習過程にも問題があるのだが、C言語の上級者のそばで学習することができないという、私のような状況にあるものにとって、唯一の手がかりである、学習のためのテキストというものが、このあたりの段階になると、急に乏しくなってくるからでもあった。そして、このように独学するものにとっては、わずかな知識の欠損が、大きな壁となってしまうのである。私のようなものにとっての問題の多くは、上級者たちが、やさしいことと思っているところに潜んでいる。「BMP画像の保存」についても、同じようなことが起こっていた。私は、上級者たちがかんたんに通って行った道のあちこちで、あまりに多すぎる道標の、どれを信じ、どれを選んでゆけばよいのかが、なかなか分からなかったのだ。 C言語で「BMP画像の保存」を行うときの処理の流れについて、まとめよう。その前に一言。私が開発環境としているのは、Borland
C++ Compiler である。C言語の入門書に付録として付いてあるものを利用している。これに関する導入については、高田美樹氏の「C言語スタートブック」が詳しく説明している。これは、C言語の入門者に対するテキストとしては名著である。 もう一言述べておく必要がある。C言語のプログラムでは、MS-DOSの画面で結果を表示する、ごくごく基本的なものに対して、ウインドウズの画面を用いるものでは、いろいろなことが異なってくる。プログラムを書いてあるテキストをsample1.cとし、これを置いてあるディレクトリをc:\mysrcとすると、前者をコンパイルするときには、コマンドプロンプトのウィンドウで、「c:\mysrc>bcc32 sample1.c」とすればよいが、ウインドウズの画面を用いるものでは「c:\mysrc>bcc32 –W sample1.c」となって、「-W」というオプションが必要となる。もちろん、同じプログラムテキストでは無理で、それなりの内容を書いておく必要がある。これらの事情を、私は、粂井康孝氏の「猫でもわかるゲームプログラミング」で学ぼうとしたが、これは失敗だったかもしれない。とはいえ、別の人のテキストで、もっと良いものも見つからなかった。粂井康孝氏のテキストの中では、どちらかというと、「猫でもわかるWindowsプログラミング」のほうを選ぶべきであったようだ。それでも、高田美樹氏のテキストと、粂井康孝氏のテキストの間には、大きなギャップがある。粂井康孝氏は、氏のホームページで、彼の著書を初心者用だと述べておられるが、初心者という言葉は、あまりに広義の意味をもっているようで、私が感じるところでは、「猫でもわかるゲームプログラミング」や「猫でもわかるWindowsプログラミング」は、中級者に対応するものだと思える。ここまでのことが理解できる「猫」は、おそらく存在しないだろう。上記のテキストなどを学んで、およそのことを理解し終えた段階であれば、これから述べることも、ある程度理解できるかもしれない。おそらく、これらの知識は、C言語の中級者レベルに対応していることだろう。 C言語のプログラムの文頭に置くコードとして、#include< > というものがあるが、ここには、次のすべてではないが、どれかが必要となる。これは、私のプログラムにおいて、「BMP画像の保存」が成功したものから取り出してきたものである。不要なものもあるかもしれない。
次に、広義の変数として定義すべきものが、何かあるかもしれない。また、メイン関数やウインドウプロシージャ関数に関しての問題については、上記のテキストなどで学んでほしい。このあとは、BMP画像を保存する関数のことについてのみ述べる。 BMP画像のデータは、大きく分けると2つ、厳密に分けると3つの種類となっている。この3種類について、各1行ずつの、データ書き込みコードを準備して実行することができれば、BMP画像を保存することができる。 BMP画像のデータを大きく2つに分けるというとき、一つ目は画像データの特徴を記したヘッダのデータであり、二つ目が実際の画像データである。3つに分けるというときは、このヘッダのデータを、さらに、ファイルヘッダと情報ヘッダという2つに分ける。 これらのデータの並びを、実際に見ることができる。BMP画像を一つ用意し(図1)、Windows OSのコンピュータに入っているメモ帳へと、マウスでドラッグしてゆきドロップオンしてみると、メモ帳の画面に、文字化けのような、文字と記号が入り混じったものが描かれる(図2)。他のBMP画像で繰り返すと、同じようなものが描かれる(図3)が、これらを観察すると、文の初めのあたりが、なんとなく似ていることが分かるだろう。まずBMとあって、空白やら、数字や記号が散在して、さらに空白が続き、突然、密集した記号の群れが並び始める。このときの、密集した記号の群れのあたりから最後までが、実際の画像データであり、BMからこのあたりまでがヘッダのデータである。 |
|||||||||||||
![]() |
|||||||||||||
もう少し厳密に調べるときは、何らかのバイナリ・エディタを使うことになる。私は、今回、ベクターで公開されているstir131(フリーソフト)をダウンロードして使ってみた。実行ファイル名はStirling.exeである。図4は、Stirlingを走らせ、読み込み画像ファイルとしてamagoi.bmpを指定しようとしているところである。そして、図5は、この操作の実行結果である。ADDRESSの行は、この画像ファイルが、このコンピュータのメモリーのどこに保存されているかということを記したもののようだ。次の行からが、amagoi.bmpのデータということになる。「BM」から、最初の「ヌ」の前までが、おそらくヘッダ部分である。「ヌ・」「ネ・」「ヒ・」「ハ・」などの部分は、amagoi.bmpの画像にある、周囲の肌色部分を表現しているのだろう。ヘッダに戻って、左の機械語表記のところを見ると、「00」が多くある。これは、数字の0を表わしている。「42」が「B」で、「4D」が「M」に対応している。機械語は16進数であるから、42, 43, 44, 45, 46, 47, 48, 49, 4A, 4B, 4C, 4Dと並ぶのだろう。これで「42」を「B」として数えてゆくと、「4D」で、めでたく「M」となる。「9A」と「BC」が漢字の「埔」となり、「36」が数字の「6」か(?)。しかし、このような探索をやっていても、目的のプログラムへと結びつかない。少し方針を変えよう。 |
|||||||||||||
![]() |
|||||||||||||
BMP画像のデータが、大きく分けてヘッダデータと画像データに分かれることが分かった。それでは、これらの各データの、さらに詳しい内容を調べよう。この目的のため、私は、次のようなウェブページを見つけて、ここから知識を吸収した。
これらの内容を繰りかえすのは控えようと思う。これらは、よくまとまっており、プログラムではないので、バグの心配はない。実際のプログラムにおいて、ファイルヘッダと情報ヘッダが、どのように取りあつかわれるのかということを知ってから、これらの詳しい知識を参照すると、これらの知識も、うまく利用できる。 ここでは、とりあえず、ファイルヘッダが14byteであり、Windowsでは情報ヘッダが40byteであることに触れておこう。合計して、ヘッダデータはWindows のとき、54byteとなる。なるほど、図5の「42」から始まって、4行目の、「C7」の前にある「00」までで、ちょうど54個である。 次は、「BMP画像の保存」という目的に沿った、C言語によるプログラムを探すことになる。私は、この目的に対して、次のようなウェブページを見つけることができた。
これらの3つのウェブページに掲載されていたプログラムについては、これらを一つずつ、私のプログラムの中に書き込み、引数やポインタの名称などを修正して、「通れ」と念じながらコンパイルしてみたものの、いずれもエラーの表示が現れて、うまくいかなかった。エラーの中には、原文によるミスプリもあったし、こちらの学習能力の低さに由来する誤解などもあったが、決定的に、意味不明となってしまうエラーが残ってしまい、これらの利用を一時保留していた。 結果的に、私は、あるとき、BMP画像を保存することに成功したのであるが、このとき、上記のサンプルプログラムを読み比べ、共通して書かれている要素だけを選別し、自分なりに、最小限度のものだけを残すことにより、エラーの数を絞りこんでゆくことができた。最後にはエラーが表示されなくなったので、これで「通った」と思ったものの、カレントディレクトリに現れた画像ファイルの中は空っぽのままであった。この、エラーなしのエラーという、パラドックスのような状態から抜け出すヒントが、②に書かれていることに気づいて、ようやく、画像データに満たされているBMPファイルを、新たに書き込むことができた。ただし、このときの画像は、もとの画像に対して、逆立ち状態であった。この原因はすぐに分かった。②の著者が気を利かせすぎていたのである。画像データは、ディスプレイに描いたときの、左下から右上に向かって並べられている。これを、ディスプレイの座標に合わせて、左上からのものに変えてしまうと、余計な御世話になってしまって、画像が逆立ちしてしまうのだ。Windows のOSが、このときの並べ替えをやってくれるので、こちらは、そのままの並びにしておくべきなのである。 物語が突然ラストのところへと飛んでしまった。もう一度、BMP画像を保存する関数の、C言語による表記のところへと戻ろう。 BMP画像のデータは、ファイルヘッダと情報ヘッダと画像データの3つに分けられる。このときの2種のヘッダの内容を指定するには、次のようにプログラムに書く必要がある。
このときの大文字部分は変更できない。ときどきテキストにBMPFILEHEADERやBMPINFOHEADERと書かれていることもあるが、私が使っているBorland C++ Compiler は、この短縮形を認識してはくれなかった。小文字のbfやbiは、変えられるようだが、ここでは、これらの形で指定しておく。 このように指定すると、画像ファイルのヘッダのための、C言語において、あらかじめ定義されている、構造体を利用することができる。少し戻って、この構造体へと入力するための値を、先に指定しておく必要がある。上記の②が中心的なものになるので、それに準じて、次のように指定する。
ここで、xとyについては、保存しようとしている画像によって決められる値である。私の解析プログラムでは、×64の窓を使ったとき、横15画素×縦10画素のものを、それぞれ64倍にするので、960×640となる。このとき、x=960でy=640である。これらの値については、広域変数でbmp_wとbmp_hを定義しておき、そこへと入力してある。 画像データを取りこんで加工してから再現するとき、このときのxの値が4で割り切れるものでないと、うまく描けない。このことを防ぐための方法の一つとして、②の著者はpadという値を設定して、これを利用している。
私は、任意のサイズの画像を取り込んだとき、4で割って、余り数がでると、このときに削り取ってしまうので、私のプログラムの中では、padについての処理は不要となり、pad=0となるだろう。上記のpadの式は、少しおかしい。これだと、pad=3しか出てこない。うまく行った私のプログラムで確認してみると、次のように書いている。これが正解のようだ。x=100を代入するとpad=0となる。x=101ならpad=1で、x=102のときはpad=2で、x=103のときはpad=3となる。
上記のimgsizeでxに3が掛けられているのは、1画素の色に対して、青と緑と赤の色値が、lpBMP[3*k ], lpBMP[3*k +1], lpBMP[3*k +2]と、連続した数字で並べられるようになっているからである。私のプログラムでは、これらのディスプレイ用の配列とは別に、処理用の配列を、青緑赤の色別に、isob[k], isog[k], isor[k]と設定しておいて、ここへと、lpBMP[ ]の値を入力したり、戻したりしている。 ともあれ、私のプログラムにおいては、xはすでに4で割り切れる値となっているので、pad=0 でimgsize = (x * 3) * y である。 ようやく準備が済んだ。ヘッダの構造体の説明へと戻ろう。まず、ファイルヘッダの構造体の要素から。
1行目の記述は②のもの。私は、この書式の意味がよく分かっていないのだが、これで、”BM”という文字が入力できるらしい。sizeof( )というコードは( )内のもののサイズを求めるもののようだ。0が代入されている2つの変数は、将来の使用のために残してあるもので、まだ使われていない。Windowsの場合、sizeof(BITMAPFILEHEADER)=14でsizeof(BITMAPINFOHEADER)=40となっているので、bf.bfOffBits=54と書いておいてもよい。 次に、情報ヘッダの構造体の要素を見よう。
この構造体で、よく使われるのはbi.biWidth とbi.biHeightであり、それに伴って、bi.biSizeImageを決めておく必要がある。 ③のプログラムでは、次のように定義した後で、bi.biWidth = x などの、あらためて指定すべき変数の値を入力している。私は、上記システムで成功したので、この書式が通用するかどうかについては、テストできていない。おそらくエラーではなかったと思う。
さて、ヘッダの変数を指定したら、さっそく、これらの値をファイルに書き込むことにしよう。②ではfwrite( )というものを使っているが、①と③がWriteFile( )というものを使っているので、多数決ではないが、③のプログラムをベースとしていたため、私は、WriteFile( )の書式で成功した。このとき、次のような準備も必要となる。hFile というハンドルで保存のためのファイルをCreateFile( )で作っておく。dwBytesは、ここまでのところで、特に何も指定しないようだ。WriteFile( )の解説が「猫でもわかるWindowsプログラミング」のp258にある。WriteFile(a, b, c, d, e)とすると、aはファイルハンドル、bはバッファ、cは書き込むバイト数、dは書き込んだバイト数、eはオーバーラップ構造体とある。なるほど、dwBytesには、あまり重要な意味はないようだ。「猫でもわかるWindowsプログラミング」ではfwrite( )の検索項目がなく、「猫でもわかるC言語プログラム」のほうにはある。このfwrite( )は、ウィンドウオプション「-W」が必要なプログラムでは、あまり使われていないようだ。しかし、使えるか使えないかをテストしたわけではないから、使えないと決まったわけではない。
「BMP画像の保存」に必須の3要素に関して、2つをクリアーした。あとは画像データの本体そのものである。②の著者は、この目的のため、あるテクニックを使っている。最終的に私のプログラムがうまくいったのは、ここのところのテクニックの意味が分かったことによる。②の著者は、まず、関数の最初の定義部分で、「画像データとその先頭ポインタ」を定義している。それから、上記のヘッダ出力が終わったころで、「出力バッファ確保」という処理を行っている。ここでは、一行に等号(=)が二度現れており、このような書式をC言語で見たのは初めてだったので、強く印象に残った。この行は誤りではなく、きちんと通っている。ここで著者がやろうとしていたことを真に理解できたのは、このプログラムにおけるチャレンジを保留した後2ヶ月たってからのことだった。
一つのポインタに対して、二つの記号を与えるのは、片方だけをどんどん変えていきながら、このポインタへと入力した値の全体については、変化させなかったほうの記号で処理するためなのである。②の著者のプログラムを参考にしつつ、①と③の表現も考慮して、私のプログラムで、うまくいったものを、次に記そう。私のプログラムでは、広域変数として、bufを他の用途のために定義しているので、混乱をさけるため、bufのところをg_imgに置き換えてある。この記号は③のプログラムからの遺伝である。
これで説明は終わったことになるが、話を前後させているところもあり、実際にプログラムを組むときには雛型があったほうがよいだろう。私のプログラムにおいて、BMP画像を保存する関数のところを書いておこう。AからBまでの部分は、保存のためのディレクトリと保存名を指定するダイアログボックスのためのものである。ここで定義されていない変数(おそらく、bmp_wとbmp_h)は、広域変数として定義してある。
(2009.010.11 Written by Kinohito KULOTSUKI) |
|||||||||||||
![]() |