6/28 プログラミング(2)


前回までの補足


5/31の課題に関して



今回の目標


PPMファイル

今回の課題では,プログラムで画像ファイルを出力するが,テキストファイと して扱えるPPM形式という画像ファイル形式を紹介する.実際の例で見てみる.

P3
4 4
255
  0   0   0    0   0   0    0   0   0  255   0 255
  0   0   0    0 255 127    0   0   0    0   0   0
  0   0   0    0   0   0    0 255 127    0   0   0
255   0 255    0   0   0    0   0   0    0   0   0
上の部分をt.ppmというファイルで保存して,
gimp t.ppm
でみると,幅4ドット,高さ4ドットの米粒のような画像が現れるがこれを zoom in していくと,
4x4.png
のような画像とわかる.このファイルの説明をする.
P3
の部分がマジックナンバーという部分で,このファイルが他の画像ファイルで
はなくPPM形式であることを示す(Windowsの多くのアプリケーションのように,
拡張子が .ppm でああることで判断しているわけではない).PPM形式では空白
(White Space, Spaceコードだけではなく改行,タブも含まれる)はいくついれ
ても良いが,P3の前に入れてはいけない.マジックナンバーの後に 空白
があり,
4 4 255
で,画像の幅,高さ,色の階調数を順に指定する.階調の最大値は,一般には
R(red), G(Green), B(Blue)それぞれ 0(暗い)から255(明るい)までの256階調
あれば人間に自然な画像を表せるといわれているので,ここでは 255 を指定
している.詳しくは, ラスタ画像の形式を参照すること.

その後で,

  0   0   0    0   0   0    0   0   0  255   0 255
  0   0   0    0 255 127    0   0   0    0   0   0
  0   0   0    0   0   0    0 255 127    0   0   0
255   0 255    0   0   0    0   0   0    0   0   0
のように,4x4=16ピクセルの RGB値を順に指定している.ピクセルの指定の順 番は横書きの文章と同じように左上から右下へ横方向に走査していく.1ピク セル内の指定の順番は R G Bの順番になっている.

上の例で現れる色は

  • (0,0,0) 黒
  • (255,0,255) RとBを混ぜた色なので紫(マゼンダ)
  • (0 255 127) Gに少しBを加えた色なので青目の緑
の3色である.

他の形式の画像ファイルからppm形式の画像ファイルを作ることもできる.たとえば,HWBのロゴをファイルにhwb3.pngという名前でセーブして,

convert hwb3.png hwb3raw.ppm
pnmnoraw hwb3raw.ppm > hwb3.ppm
として,logo3.ppm というファイルができる.これを見ると
P3
225 27
255
255 255 255  255 255 255  255 255 255  255 255 255  255 255 255  255 255 255
255 255 255  255 255 255  255 255 255  255 255 255  255 255 255  255 255 255
255 255 255  255 255 255  255 255 255  255 255 255  255 255 255  255 255 255
255
...
のように,1ピクセルのRGB値がテキスト形式で表されていることがわかる. このファイルを編集して(たとえば 255をすべて100に変更して),
convert hwb3.ppm hwb3new.gif
のようにすると,gif形式にできる.

PPMファイルを出力するプログラム(グラデーション)

以下のプログラム
class Grad{
  public static void main(String[] args){
    int width=128, height=128;
    System.out.println("P3");
    System.out.println(width); // 幅
    System.out.println(height); // 高い
    System.out.println(255);
    int i,j;
    for(i=0;i<height;i=i+1){ // 
      for(j=0;j<width;j=j+1){ //
        int r,g,b;

        r=255; //
        g=255; //
        b=j*2; //

        r=Math.min(255,Math.max(0,r)); //
        g=Math.min(255,Math.max(0,g)); //
        b=Math.min(255,Math.max(0,b)); //
        System.out.println(r); // R
        System.out.println(g); // G
        System.out.println(b); // B
      }
    }
  }
}
を ~/jousho05以下にGrad.java という名前で保存して,
javac Grad.java
とコンパイルして,
java Grad > grad.ppm
とプログラムを実行し,出力を grad.ppm というファイルに保存すると,この ファイルは PPM形式(Portable PixMap形式)の画像ファイルになっている.
convert grad.ppm grad.png
を実行して,png形式にしたのが
grad.png
である.

PPMファイルを出力するプログラム(グラデーション) の書き換え

上のプログラムをベースに書き換えてみることを試みる.

まずは,

    int width=128, height=128;
の部分を書き換えると作成する画像の縦横のピクセル数を変更することができる. これをたとえば,
    int width=400, height=200;
に変更すると,幅が400, 高さが200に変更となるはずだが,実際に実行して 作成されたファイルを変換してみると,グラデーションが途中で飽和してしまう. これは,widthが400になると,
        b=j*2; // Bがjの値に応じて0-254 
の行で出力するB(青)の階調の範囲が0から798となり,0-255という制限を 超えてしまうからである.これを
        b=j*256/width;
と直すと,幅と高さを変更できる.

青から赤へのグラデーションを縦方向におこなう場合には,

        r=255; // Rが255(最も明るい)
        g=255; // Gが255(最も明るい)
        b=j*2; // Bがjの値に応じて0-254 で変化
        r=i*256/height; // R
        g=0; // G
        b=255-r; // B
のようにする.ここで,
        r=i*256/height; // R
        r=i/height*256; // R
のようにしても,同じになりそうだが,(i/height)は整数の除算で, (0<=i <=height-1)の範囲では常に結果が0になってしまう.

また,青から緑に斜めにグラデーションさせるには,同じ部分を

        r=0; // R
        g=(i+j)*256/(width+height); // G
        b=255-g; // B
のようにする.

マンデルブロー集合を表示するプログラム

ある複素数 C を与えた時,
Z0=0
Zn+1=Zn*Zn+C
として, 複素数列{Zn}が定義できる.ここで,n->∞としたとき, |Zn|が発散しないようなCの集合をマンデルブロー集合という.

マンデルブロー集合は,集合の境界が複雑な形をしていて, 拡大をしても元の 図形と同じような複雑度を保っている.このような自己相似のことを総称して フラクタルと呼ぶが,マンデルブロー集合はフラクタルな図形の中ではもっと も有名である.

マンデルブロー集合は定義に無限回の繰り返しを含んでいるため, 計算機上 で正確に描くのは,困難である.そこで, 一般には

  • |Zn|がある値(たとえば 2)を超えたら発散と見なす.
  • 十分大きい回数(たとえば 50)繰り返しても|Zn|がある範囲 に収まっていれば,発散しないと見なす.
という近似を使って計算できる.以下は, この近似を用いてマンデルブロー集合を 計算するプログラムである(マンデルブロー集合は画像の黒い部分で表される).なお,「//」は注釈(コメント)の始まりを表す.

class Mandel{
  // 実数を引数(パラメタ)に持つメソッド mandel の定義
  static int mandel(double x, double y){
    double zr=0, zi=0,new_zr;
    int i;
    for(i=0;i<50;i=i+1){
      if(zr*zr+zi*zi>4.0) { // |Zi|>2.0 の時は発散と判断
        return i;
      }
       // 複素数の計算
       // (zr+zi*i)*(zr+zi*i)+x+y*i=(zr*zr-zi*zi+x)+(2*zr*zi+y)*i
      new_zr=zr*zr-zi*zi+x;
      zi=2*zr*zi+y;
      zr=new_zr;
    }
    return 50;
  }
  public static void main(String[] args){
    int width=256, height=256;
    System.out.println("P3");
    System.out.println(width);
    System.out.println(height);
    System.out.println(255);
    int i,j,count;
    double x, y;
    for(i=0;i<height;i=i+1){
      for(j=0;j<width;j=j+1){
        int r,g,b; 

           // -1.7 <= x < 0.7, -1.2 <= y < 1.2の範囲で計算
           // 整数(int)から実数(double)への変換は「(double)整数式」
           // のようにおこなう
        x= -1.7+2.4*((double)j/(double)width);
        y= -1.2+2.4*((double)i/(double)height);
           // 0 <= count <= 50の値がかえる
        count=mandel(x,y);
           // count=50の時に黒(0,0,0), count=0の時にほぼ白(250,250,250)とする
        r=250-count*5; //
        g=250-count*5; //
        b=250-count*5; //

        r=Math.min(255,Math.max(0,r)); // R の範囲を0から255までに
        g=Math.min(255,Math.max(0,g)); // G の範囲を0から255までに
        b=Math.min(255,Math.max(0,b)); // B の範囲を0から255までに
        System.out.println(r); // R
        System.out.println(g); // G
        System.out.println(b); // B
      }
    }
  }
}
このプログラムを Mandel.java というファイルに保存して,
javac Mandel.java
java Mandel > mandel.ppm
と実行すると,mandel.ppm というファイルが得られる.これを
open mandel.ppm
で見ることもできるし,
convert mandel.ppm mandel.png
として png 形式に変換して,WWWブラウザで見ることもできる. できた mandel.png は以下のようになる.
mandel.png
        r=250-count*5; //
        g=250-count*5; //
        b=250-count*5; //
の行を,たとえば
        r=((50-count)*71)%256;
        g=((50-count)*111)%256;
        b=((50-count)*97)%256;
とすると(「%」は剰余(mod)を表す演算子, 256で割った余りは 0-255になる),
mandel-color.png
のようなカラーの絵が得られる.

マンデルブロー集合を表示するプログラムを書き換え

上のプログラムを書き換えてみる.まず,
    int width=256, height=256;
を書き換えると画像の大きさを変更できるが,widthとheightの比率を1:1以外にする 場合は,
        x= -1.7+2.4*((double)j/(double)width);
        y= -1.2+2.4*((double)i/(double)height);
の行の拡大率(2.4)をxとyとで変更した方が良い.

元のプログラムでは,-1.7 <= x < 0.7, -1.2 <= y < 1.2の範囲で計算していたが, たとえば,-0.12 <= x <=-0.02, -0.7 <= y <= -0.6 の 範囲を指定するには,

        x= -0.12+0.1*((double)j/(double)width);
        y= -0.7+0.1*((double)i/(double)height);
のようにする.

色を変更するには,

        r=250-count*5; //
        g=250-count*5; //
        b=250-count*5; //
の行を変更する.たとえば,白の背景に青で出したい場合は,
        r=count*255/50; //R
        g=count*255/50; //G
        b=255; // B
のようにすれば良い.

円や長方形を描くプログラム

class Draw{
    /**
     *
     * Math.sqrtを呼び出して平方根を計算できる
     */
    static double distance(int x0,int y0, int x1, int y1){
	int dx=x0-x1;
	int dy=y0-y1;
	return Math.sqrt(dx*dx+dy*dy);
    }
    /**
     * circleX 円の中心のX座標 
     * circleY 円の中心のY座標 
     * circleR 円の半径
     */
    static boolean inCircle(int x, int y, int circleX, int circleY, int circleR){
	if(distance(x,y,circleX,circleY) <= circleR) {
	    return true;
	}
	else {
	    return false;
	}
    }
    /**
     * rectangleX 矩形の左上X座標
     * rectangleY 矩形の左上Y座標
     * rectangleW 矩形の幅
     * rectangleH 矩形の高さ
     */
    static boolean inRectangle(int x, int y, 
			    int rectangleX, int rectangleY, 
			    int rectangleW, int rectangleH){
	if(rectangleX <= x &&
	   x <= rectangleX+rectangleW &&
	   rectangleY <= y &&
	   y <= rectangleY+rectangleH){
	    return true;
	}
	else {
	    return false;
	}
    }
    public static void main(String args[]){
	int width=256;
	int height=256;
	int x,y;
	System.out.println("P3");
        System.out.println(width);
        System.out.println(height);
        System.out.println(255);
	for(y=0;y < height;y++){
	    for(x=0;x < width;x++){
		/**
		 * 赤 r, 緑 g, 青 b をすべて0にすると黒
		 */
		int r=0, g=0, b=0;
		if(inRectangle(x,y,0,50,150,100)){
		    // 白みがかった暗い黄色
		    r=200; g=200; b=100;
		}
		if(inCircle(x,y,200,150,50)){
		    // 暗いマゼンダに
		    r=200; g=0; b=200;
		}
		if(inCircle(x,y,150,200,70)){
		    // 白を重ねる
		    r=r+100;
		    g=g+100;
		    b=b+100;
		    // 255までしか色を表現できないので,
		    // 255と小さい方の値を取ることにより,切り捨てる
		    // Math.minを呼び出して小さい方の値を取る
		    r=Math.min(255,r);
		    g=Math.min(255,g);
		    b=Math.min(255,b);
		}
		{
		    // (200,50)を中心にした緑のフレア効果
		    double len=distance(x,y,200,50);
		    g+=200*25/(len+20);
		    g=Math.min(255,g);
		}
		System.out.println(r);
                System.out.println(g);
                System.out.println(b);
	    }
	}
    }
}
このプログラムを実行した結果は,
となる.

sin カーブを描くプログラム

class Sin{
    public static void main(String args[]){
	int width=256;
	int height=256;
	int x,y;
	System.out.println("P3");
        System.out.println(width);
        System.out.println(height);
        System.out.println(255);
	for(y=0;y < height;y++){
	    for(x=0;x < width;x++){
		int r,g,b;

		    // Math.sinを呼び出してsinを計算する
		double sinY=-Math.sin(x*0.03)*100.0+128.0;
		    // Math.absを呼び出して絶対値をとる
		    // (int)で整数型に変換する
		int dl=(int)Math.abs(y-sinY);
		r=200*25/(dl+20);
		g=200*25/(dl+20);
		b=200*25/(dl+20);

                r=Math.min(255,Math.max(0,r)); // R の範囲を0から255までに
                g=Math.min(255,Math.max(0,g)); // G の範囲を0から255までに
                b=Math.min(255,Math.max(0,b)); // B の範囲を0から255までに
		System.out.println(r);
                System.out.println(g);
                System.out.println(b);
	    }
	}
    }
}
このプログラムを実行した結果は,
となる.

今日の課題

上のURLは,今日(6/28)の15:10 までは
Forbidden

You don't have permission to access /~ktanaka/jousho05/report628/index.html on this server.
とうメッセージが出てアクセ スできないはずである.15:10以降にも同様のエラーが出る時は,Shiftキーを 押しながら,再読み込み(Reload)を押してみること.

なお課題の締切りは7/19(火)の21:00である.