1
2011
年度 機械工学総合演習第二
計算機演習
8
グラフィックスと画像処理
担当:谷川智洋 講師,鳴海拓志 助教
TA
:井村純,小川恭平,木山亮
http://www.cyber.t.u-tokyo.ac.jp/˜tani/class/mech enshu/
2011
年
7
月
7
日
1
演習の目的
本演習では,X Window System上で動作するGUI(Graphical User Interface)のプログラムを作成するこ とで,GUIを扱う上で不可欠なイベント駆動型(event driven)プログラミングの考え方を学習する.また, 画像ファイルの取り扱いやネットワークプログラムの作成を通して,最終的に普段我々が使っているアプリ ケーションに近いグラフィカルなプログラム作成を目指す.
2
Gtk+
による
GUI
プログラミングの基礎
2.1
GUI
プログラミング
GUIプログラミングは,マウスなどのポインティングデバイスを介した入力と,グラフィカルな出力を求 められるので,これまで学んできた文字ベースのプログラミングに比べると,本質的な機能の部分よりもユー ザーインターフェースの実装により多くの労力をとられてしまう.また,これまで学んできた,文字ベースの 逐次処理型のプログラムとは異なり,GUI環境では,プログラムは常にユーザーの入力を待機し,ユーザーか らの操作があって初めて機能が呼び出される「イベント駆動型(event driven)」のプログラミングとなり,プ ログラミングの手法も大きく異なってくる. GUIツールキットは,GUIプログラミングに伴う煩わしい作業を簡素化するために用いられるライブラ リである.多くの開発者はGUIツールキットを用いてGUIアプリケーションの開発を行なっている.GUIツールキットには,ボタン,ダイアログボックス,スクロールバー,メニューなどGUIを構成する数多くの” ウィジェット(widget=部品)”があらかじめ用意されていて,プログラムのソースコードからはほんの数行 の呼び出しの手続きを書くだけで,美しくデザインされたこれらのウィジェットを自分のプログラムで利用す ることができる. 近年,数多くの優れたGUIツールキットが公開されているが,今回はその中でも急速に普及しつつある Gtk+を取り上げて,GUIプログラミングを実践する.本演習で取り上げるのはGtk+のみであるが,他の GUIツールキットを用いても基本的なプログラミングスタイルは似ており,演習で学んだことは,今後,あら ゆる場面で役立てることができる.
2 2 Gtk+によるGUIプログラミングの基礎
2.2
Gtk
アプリケーションの処理の流れ
まずは,簡単な例題プログラムを使って,GUIプログラミングにおけるウィジェットの作成,イベントの 処理について見てみよう.最初にサンプルのsample1.cのコンパイルを行って,実行ファイルを作成する. 以下ののWebページ(http://www.cyber.t.u-tokyo.ac.jp/˜tani/class/mech enshu/)からリンクをたどり,
enshu2010gui1.tar.gzをダウンロードし,以下のコマンドを実行する. % tar xvzf enshu2011gui.tar.gz % cd 2011gui % make sample1 でき上がったファイルを実行し,図1のようなウィンドウが現れれば,コンパイルは成功である. 図1 sample1.c実行時の画面 Gtk+を利用するには,Gtk+ライブラリのヘッダファイルgtk.hをソースコードの冒頭でインクルードす る必要がある.sample1.c 12行目の#include<gtk/gtk.h>がそれにあたる.gtk.hには,Gtk+を扱うのに 必要な関数,構造体,マクロ等が定義されている.*1 次に54行目以降のメイン関数を見てみよう.Gtkアプリケーションにおける処理の流れは,基本的には 初期化 → ウィジェットの生成 → 入力待機状態の開始 のようになっている.以下でそれぞれの処理について説明する. 初期化 初期化の処理では主に,プログラム呼び出し時の引数の処理,ロケールの設定(文字コードなどの言語に関 する設定),設定ファイルの読み込み等を行なう.最も単純なGtkプログラムのメイン関数は大体以下のよう になるはずである. *1実際は gtk.h 自体は,さらに細かい機能ごとに定義されたヘッダファイルをインクルードしているだけである.興味のある人は一 度 gtk.h を覗いてみるとよい.
2.3 ウィジェット 3
int main (int argc, char* argv[]) { { gtk_set_local(); /* ロケールの設定 */ gtk_init(&argc, &argv); /* ライブラリ初期化と引数処理 */ gtk_get_rc_parse("設定ファイル"); /* Gtk設定ファイルの読みこみ */ ... } sample1.cではロケールの設定と設定ファイルの読みこみは省略してある.gtk_init()へは,アプリケー ション自体への引数のリストargvがそのまま渡され,アプリケーション呼び出しの際に指定できるGtkオプ ションが処理される.処理された引数は消去され,それ以外の引数は引数リストに残るので,gtk_init()の あとで,アプリケーション固有の引数を処理すればよい. ウィジェットの生成 Gtk+をはじめとするGUIツールキットではウィンドウやボタンなどの要素を「ウィジェット(部品)」と 呼ぶ.ウィジェットには,ボタンやテキストボックス,スライダーなど目に見えるGUI部品の他に,パッキ ングボックスと呼ばれるウィジェットを配置を管理するための枠も含まれ(後述),作成したウィジェットを パッキングボックスへ格納(=パック)することでGUIのレイアウトを行なう. 基本的にGtk+でのGUIの構築は、ウィジェットを格納するGtkWidget型の構造体を確保し,パッキン グボックスに格納したあとgtk_widget_show()画面に表示する,という手順を踏む.gtk_widget_show() を呼び出す順番はとくに決りはないが,gtk_widget_show()が呼ばれても,その下にあるウィジェットが表 示されないかぎり表示できないので,一番最後にメインウィンドウを表示するように記述すると,全てのウィ ジェットが一度に表示されるようになる. 入力待機状態の開始 全てのウィジェットを作成したあとは,GUIプログラムはユーザーからの入力などのイベントを待機し,イ ベントが生じた場合に必要な処理を行なう関数を呼び出すという状態に入る.Gtkプログラミングにおいては 開発者が待機状態のコードを自前で用意する必要はなく,125行目にあるようにgtk_main()を呼び出すだけ でよい.プログラムを終了するためのイベントが生じるとgtk_main()を抜けだし,プログラムはmain()関 数の最後まで到達して終了する.
2.3
ウィジェット
Gtk+で最も単純な3つのウィジェットである,ウィンドウ,ラベル,ボタンの使い方を述べる. windowウィジェット Gtk+のアプリケーションで最初に作成すべきウィジェットは,メインウィンドウそのもので,sample1.c では67∼69行目にあたる.メインウィンドウの作成は以下のように行なう.4 2 Gtk+によるGUIプログラミングの基礎 /* ウィジェットの宣言 */ GtkWidget *window; /* 新しいwindow ウィジェットの確保 */ window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* ウィンドウタイトルの設定 */ gtk_window_set_title(GTK_WINDOW(window), "ウィンドウタイトル"); /* ウィンドウの枠から中味までの間隔の設定 */ gtk_container_border_width(GTK_CONTAINER(window), 10); labelウィジェット ウィンドウ上に文字列を表示するためにはlabelウィジェットを用いる.labelウィジェットの作成は以下 のようなコードを書けば良い. /* ウィジェットの宣言 */ GtkWidget *label; /* 新しいlabel ウィジェットの確保,文字列の設定 */ label = gtk_label_new("ラベル文字列"); sample3.c 78∼80行目ではウィンドウにあらわれるボタンを押した回数の表示のためのlabelウィジェット を定義している.labelの文字列は,アプリケーションの動作中に,動的に変更することもできる.この場合, gtk_label_set(GTK_LABEL(label), "新しいラベル"); とすればよい. buttonウィジェット ボタンの表示も,これまで見て来たウィジェットと同様に /* ウィジェットの宣言 */ GtkWidget *button; /* 新しいbutton ウィジェットの確保,文字列の設定 */ button = gtk_button_new_with_label("ボタンに表示される文字列"); で作成できる.ただし,ボタンの場合ユーザーの操作という“イベント” 発生に伴う処理を開発者が自ら定義 してやらなければならない(次節参照).
2.4 コールバック関数の設定 5
2.4
コールバック関数の設定
イベント処理の細かい概念や実装は,GUIツールキットやOSによって多少異なる.Gtk+では基本的に イベント発生時に“シグナル”が発行されるようになっており,開発者はシグナル発生時に行なうべき手続を 記述した関数(シグナルハンドラ)を登録することによってウィジェット操作時の動作を定義する.このイベ ントに対応づけられたシグナルハンドラのように,プログラムに対する入力等の通知を処理する手続をコール バックと呼ぶ. コールバックの登録は主に2種類ある.これはコールバック関数への引数の数によって使いわける.より汎 用性が高いのはgint gtk_signal_connect( GtkObject *object, gchar *name, GtkSignalFunc func, gpointer func_data ); object : シグナルを発行するGTKオブジェクト(ウィジェット) name : シグナル名 func : コールバック関数 func_data : コールバック関数へ渡すデータへのポインタ で,この場合コールバック関数の型式は,
void callback_func( GtkWidget *widget, gpointer data ); widget : シグナルを発行したウィジェット
data : コールバックへ渡されるデータへのポインタ
となる.一方,より簡便なのは,
gint gtk_signal_connect_object( GtkObject *object, gchar *name, GtkSignalFunc func, GtkObject *slot_object ); object : シグナルを発行するGTKオブジェクト(ウィジェット) name : シグナル名 func : コールバック関数 slot_object : funcへの最初の引数として渡すGTKオブジェクト で,この場合コールバック関数は,
6 2 Gtk+によるGUIプログラミングの基礎
void callback_func( GtkWidget *widget ); widget : シグナルを発行したウィジェット である.つまり,コールバック関数に何かしらのデータを引数として渡したい場合は,gtk_signal_connect(), それ以外の場合は,gtk_signal_connect_object()を使えばよい. シグナル名はあらかじめ定義されており,表1のようなものがある. 表1 Gtk+のイベントの例 イベント名 説明 pressed マウスボタンが押された released マウスボタンが離された clicked マウスボタンがクリックされた enter マウスポインタが重なった leave マウスポインタが外れた sample1.cでは,ボタンが押されるとカウンター変数counterをインクリメントし,さらにラベルの表示 を変更して,ボタンが押された回数をカウントするようにしている. ◇ 課題1◇ 1. sample1.cをコンパイルし,実行せよ. 2. sample1.cにbutton2を追加し,カウンターを減らす機能を実装せよ(図2). 図2 課題1.2実行時の画面
2.5 ウィジェットの配置 7
2.5
ウィジェットの配置
より多機能なGUIプログラムを作るためには、複数のウィジェットを思い通りの位置に配置する必要があ る.しかし、各ウィジェットの位置を座標の絶対値で指定するだけでは,ユーザーの操作によってウィンド ウのサイズやGUIの概観,フォントの種類が変更されると,開発者の意図どおりの配置を保つことが難しく なる.また,ウィジェットの配置の調整に多くの労力を要してしまう.そこで,Gtk+をはじめとして多くの GUIツールキットでは“パッキング”という操作によってウィジェットの配置を行なう.Gtk+での基本的な ウィジェットの配置を理解するためには,パッキングボックスの概念を知る必要がある. パッキングの操作では,“見えない箱”の中にウィジェットを詰め(=パック),さらにその箱をより大きな 箱に詰めていって,多数のウィジェットを高率良くウィンドウの中に整理して並べるのが基本である.この“ 見えない箱”を“パッキングボックス”と呼ぶ.パッキングボックスには以下の2種類がある. 図3 水平ボックスと垂直ボックス 図4 パッキングボックスを更にパックするこ とで,自由にウィジェットをレイアウトするこ とができる. 水平ボックス 水 平 ボ ッ ク ス は ウ ィ ジ ェ ッ ト を ,横 に 並 べ て 配 置 す る .水 平 ボ ッ ク ス の 作 成 は gtk_hbox_new() で 行 な う .水 平 ボ ッ ク ス の 場 合 ,ウ ィ ジ ェ ッ ト は gtk_box_pack_start() で パックした順に左から右へ,あるいはgtk_box_pack_end()でパックした順に右から左へ詰めこま れる. 垂直ボックス 垂 直 ボ ッ ク ス は ウ ィ ジ ェ ッ ト を ,縦 に 並 べ て 配 置 す る .垂 直 ボ ッ ク ス の 作 成 は gtk_vbox_new() で 行 な う .垂 直 ボ ッ ク ス の 場 合 ,ウ ィ ジ ェ ッ ト は gtk_box_pack_start() で パックした順に上から下へ,あるいはgtk_box_pack_end()でパックした順に下から上へ詰めこま れる.8 3 画像処理プログラミング ◇ 課題2◇ 1. sample2.cのプログラムを元に電卓の数字ボタン及びC(クリア)ボタンの挙動を実現せよ. (図5) 2. 演算(+,-,*,/)ボタンのコールバック関数を実装せよ. 3. 追加した各ボタンにコールバック関数を自由に割り当てよ.(オプション) 図5 課題2.1実行時の画面
3
画像処理プログラミング
3.1
画像の表示
まずは,簡単な例題プログラムを使って,画像の表示方法を学ぼう.サンプルのsample3.cのコンパイルを 行って,実行ファイルを作成する.でき上がったファイルsample3を実行し,スクリーン上にウィンドウが 現れれば,コンパイルは成功である.3.2 画像ファイルの読みこみ 9 ◇ 課題3◇ 1. サンプルプログラムsample3.cをコンパイルして実行せよ. 2. sample3.c内の関数image_proportion()を書き換え,図6のように画面の上から徐々に明 るくなるような画像が表示されるようにせよ. 3. sample3.c内の関数image_skew()を書き換え,図7のように画面の左上から徐々に明るく なるような画像が表示されるようにせよ. 4. sample3.c内の関数image_radial()を書き換え,図8のような画面の中心から徐々に明る くなるような画像が表示されるようにせよ.(オプション) 注意: 画像作成関数はサンプル内の関数image_processing()で呼び出されている。そのため関 数の中身を書き換えた後、image_processing()内で呼び出す関数を変更させること。そうしない と結果が反映されない。 図6 課題3.2の実行画面 図7 課題3.3の実行画面 図8 課題3.4の実行画面
3.2
画像ファイルの読みこみ
画像ファイルには,PNG, JPEG, TIFF, BMP, GIFなど様々な型式があるが,本演習ではプログラムから 扱いやすいPNM(Portable aNyMap)型式の一種であるPGM(Portable Grayscale Map)型式を用いる.既 にデバイスプログラミングの演習で行ったように,フルカラー画像を扱うPPM(Portable Pixel Map)形式の 先頭部分は,以下のようになっている. P6 ← ファイル形式識別記号 256 256 ← x方向のピクセル数 y方向のピクセル数 255 ← 最大輝度 ...(バイナリの画像データ,RGB順,左から右,上から下へ操作した順)... また,グレースケールを画像を扱うPGM型式のファイルの先頭部分は以下のようになっている.
10 3 画像処理プログラミング P5 256 256 255 ...(以下バイナリの画像データ)... 1行目の“P5”“P6”はマジックナンバーと呼ばれ,画像形式を宣言している.グレースケールのバイナリで あればP5,フルカラーのバイナリであればP6となる.2行目の2つの数字は画像のサイズで,上の例だと 256× 256 pixelのサイズの画像であることを表す.その次の行の255は階調で,各pixelの値を0∼255の範 囲で表現することを示す.0∼255の値を表現するためには8 bit(=1 byte)の情報量が必要なので,この画像 はグレースケールの場合1pixelあたり1byteのデータであることがわかる.カラー画像の場合は,1pixel当 たり,RGBそれぞれ8bitで順番に格納され3byteのデータであることがわかる.
3.3
描画のためのウィジェット
GTKはGDK (GIMP Drawing Kit)のうえに実装されており,GDKは基本的には基礎となるウィンドウ 関数(X Window systemではXLibにあたる)へアクセスする低レベル関数を包むラッパー(Wrapper)であ る.GDKのリファレンスとしては,http://www.gnome.gr.jp/docs/gtk+-1.2.x-refs/gdk/index.htmlを見 ると良い.
スクリーンへの描画の処理の為に使うウィジェットはDrawingArea ウィジェットである.描画領域ウィ ジェットは,空白のキャンバスでそこに好きなものを何でも描ける.描画領域ウィジェットは以下の呼び出し によって作成される.
GtkWidget* gtk_drawing_area_new ( void );
/* ウィジェットのデフォルトサイズはこの呼び出しで指定できる.*/ void gtk_drawing_area_size ( GtkDrawingArea *darea,
gint width, gint height); 描画領域を生成した後,次のシグナルに接続する必要がある場合がある. • ユーザ入力に応答するためのマウスとボタン押下シグナル • ウィジットが生成されて表示されたときに必要な処理を行う“realize” シグナル • ウィジットのサイズが変更されたときに必要な処理を行う “size allocate”シグナル • ウィジットの内容を再描画するための“expose event”シグナル RGB,白黒とカラーのイメージ(画像)をウィンドウのピクセルに変換して通常のウィンドウに表示するた め,GDKにはGdkRgbという呼び出しが用意されている.GdkRgbの機能を使用する前にgdk_rgb_init()
3.3 描画のためのウィジェット 11
関数を呼び出す.もしこの処理に失敗すると,coreダンプする.(GtkPreviewを含む) GdkRgbを使用する 全てのGTK+ウィジットは自分のクラスの初期化メソッド(class_init)の中でgdk_rgb_init()関数を 呼び出す.よって,間接的にのみGdkRgbを使用する場合は,その呼び出しを考慮する必要はない.
void gdk_rgb_init (void);
void gdk_draw_rgb_image ( GdkDrawable *drawable, GdkGC *gc, gint x, gint y, gint width, gint height, GdkRgbDither dith, guchar *rgb_buf, gint rowstride );
void gdk_draw_gray_image ( GdkDrawable *drawable, GdkGC *gc, gint x, gint y, gint width, gint height, GdkRgbDither dith, guchar *buf, gint rowstride );
drawable : 描画する GdkDrawable (通常は GdkWindow)
gc : グラフィックス・コンテキスト(全てのGDK描画関数が必要とするもの 中身は無視される) x : drawable の左上端の X座標 y : drawable の左上端の Y座標 width : 描画する矩形の幅 height : 描画する矩形の高さ dith : GdkRgbDither の値で、お好みのディザ・モードを選択する rgb_buf : ピクセル・データでPacked24ビットデータとして表現される (Packed : ビットの格納方式の一つ.一ピクセルにつきDepthを 並べて格納する) rowstride : rgb_buf である桁の始点から次の桁が開始する点までのバイト数
12 3 画像処理プログラミング
gdk_draw_rgb_image () は Drawable の 中 に RGB 画 像 を 描 画 し ,gdk_draw_gray_image () は
Drawable の中に白黒画像を描画する.引数rowstride はより柔軟に線を配置するためのもので,一般的に,
0 <= i < widthと0 <= j < heightで,pixel(x+i, y+j)は赤(R) = rgb_buf[j*rowstride + i*3], 緑(G) = rgb_buf[j*rowstride + i*3 + 1],青(B) = rgb_buf[j*rowstride + i*3 + 2] となる.
3.4
ウィジェットからの情報の取得
また,GUIによるインタラクティブなプログラムを作るためには,ボタンのような押す/離すの単純な入力 だけでなく,文字列や数値など,より複雑な情報を入力できるようにする必要がある.ここでは,文字列の入 力方法としてentryウィジェット,数値の入力方法としてscaleウィジェットの使い方を学ぶ. entryウィジェット 文字列を入力するためにはentryウィジェットを用いる.entryウィジェットの作成は以下のようなコード を書けば良い. /* ウィジェットの宣言 */ GtkWidget *entry; /* 新しいentry ウィジェットの確保,文字数の設定 */ entry = gtk_entry_new_with_max_length (50); また,entryウィジェットの文字列を取り出すには以下のようにすればよい. gchar *entry_text;entry_text = gtk_entry_get_text (GTK_ENTRY(entry));
scaleウィジェット 数値を入力するためには,entryウィジェットに直接数値を打ち込んでも良いが,よりGUIらしい入力方法 として,scaleウィジェットが用意されている.scaleウィジェットの使用方法はこれまでのウィジェットより 若干複雑になっている.まず,scaleウィジェットの範囲や初期値,増減値などを設定するための,adjustment オブジェクトを作成する.adjustmentオブジェクトを作成するための関数gtk_adjustment_new()の使い 方は以下の通りである.
3.4 ウィジェットからの情報の取得 13
GtkObject* gtk_adjustment_new ( gfloat value, gfloat lower, gfloat upper, gfloat step_increment, gfloat page_increment, gfloat page_size ); value : 初期値 lower : 最小値 upper : 最大値 step_increment : 増減値(小) page_increment : 増減値(大) page_size : スケールを移動するウィジェットの大きさ
upperは表示領域の最大値であり,値の最大値ではないことに注意せよ.値の最大値は upper - page_size
になる. その後,これまでのウィジェットと同様にscaleウィジェットを宣言して確保し,先に作成したadjustment オブジェクトを指定してやればよい.まとめると,以下のようなコードを書けばよい. /* オブジェクト,ウィジェットの宣言 */ GtkObject *adjustment; GtkWidget *scale; /* 新しいadjustment オブジェクトの確保 */ adjustment = gtk_adjustment_new(0, 0, 255, 1, 1, 0); /* コールバック関数の設定(ここではon_slider_moved()という関数をよそで定 義している) */ gtk_signal_connect(GTK_OBJECT(adjustment), "value_changed", GTK_SIGNAL_FUNC(on_slider_moved), adjustment); /* 新しい水平scale ウィジェットの確保,adjustment オブジェクトの指定 */ scale = gtk_vscale_new(GTK_ADJUSTMENT(adjustment)); /* 小数点以下の桁数の設定 */ gtk_scale_set_digits(GTK_SCALE(scale), 0); また,scaleウィジェットの値をコールバック関数から取り出すには以下のようにすればよい.
14 3 画像処理プログラミング
void on_slider_moved(GtkWidget * widget, GtkAdjustment *adj) { int scale_value; scale_value = adj->value; } ◇ 課題4◇
1. sample4.cをコンパイルし,さらにsample4.c内の関数load_pgm()を完成させ,図9のよ うにlena.pgmが表示されるようにせよ.
2. entryウィジェットからファイル名を取得して画像ファイルを開けるようにせよ(図9).