2. 1 画像ファイルの入出力



 
今週は画像を画像ファイルから入力して,その画像に色々な処理を施します.

プログラム作成のQ&Aは 「2.5 画像の拡大と縮小」の後ろにあります。
クリックして読んでください。

すべての作業はディレクトリ「$HOME/Image2」の下で行いま す.まず、ディレクトリを用意します。
% mkdir $HOME/Image2
% cd $HOME/Image2


入力画像ファイルをディレクトリ「Image2」にコピーします
% cp ../RASLIB-2007/*.p?m .

画像があるかどうか確認
% ls


他の画像 (画像をクリックしてダウンロードしてください。)
                 mandrill-grey   mandrill


2.1.1 画像ファイルから画像を読み込 み、別の画像ファイルに保存する

scene1.pgmという画像ファイルから画像を読み込んで,
scene_copy.pgmという名前の画像ファイルに保存するプログラムを作ります.

1.画像「scene1.pgm」を確認

% gimp scene1.pgm &

2.emacsで「copy.c」というファイルを開く
% emacs copy.c &

copy.cに以下の内容にします。
/* Begin copy.c */
#include "raslib.h"

int main(int argc, char **argv)
{
  Rasimg *img;

  img = pnm_load("scene1.pgm", NULL);
  pnm_save("scene_copy.pgm", img);
  return 0;
}
/* End copy.c */


説明:
  img = pnm_load("scene1.pgm", NULL);
  Rasimg *pnm_load(char *file_name, Rasimg *old_image) 関数は、file_name で指定したpnm形式の画像ファイルから画像を読み込み、読み込んだ画像へのポインタを返します。 pnmは、pbm (白黒)、pgm(濃淡画像) と ppm (カラー画像)の総称です。old_image引数にすでにある画像へのポインタを渡すと、可能の限り、読み込んだ画像をそのポインタの画像に保存しま す。old_imageの引数をNULLにすると、pnm_loadは(内部で)ras_allocを用いて画像を新しく確保してくれます。

3.copy.c をコンパイルし、実行して、scene_copy.pgmの有無とその中身を確認してください。

練習 2.1: hdcard.ppm画像ファイルから 画像を読み込み、その画像を二つの画像ファイル hdcard1.ppm と hdcard2.ppm に保存するプログラムを作りなさい。


2.1.2 横並びの画像を作る

 
cat1 ⇒ cat1cat1

scene1.pgmという画像ファイルから画像を読み込んで,上記の図に示すように、横 に2個並んだ画像をつくり、scene2.pgmという画像ファイルに保存します。

1.emacsで「copy2.c」というファイルを開く
% emacs copy2.c &

copy2.cに以下の内容にします。
/* Begin copy2.c */
#include "raslib.h"

int main(int argc, char **argv)
{
  Rasimg *img, *outimg;
  int w, h;
  unsigned char **ii, **oo;

  img = pnm_load("scene1.pgm", NULL);
  w = ras_width(img);
  h = ras_height(img);
  outimg = ras_alloc(w * 2, h, 8);
  ii = ras_lnidx(img);
  oo = ras_lnidx(outimg);
  int x, y, c;

  for (y = 0; y < h; ++y) {
    for (x = 0; x < w; ++x) {
      c = ii[y][x];
      oo[y][x] = c;
      oo[y][x + w] = c;
    }
  }
  pnm_save("scene2.pgm", outimg);
  ras_display(outimg);
  ras_end();
  return 0;
}
/* End copy2.c */


2.copy2.c をコンパイルし、実行して、scene2.pgmの有無とその中身を確認してください。

練習 2.2:
  次のような画像を作るプログラムを作りなさい。

  cat1cat1
 
 cat1cat1


練習 2.3:
 scene1.pgmという画像ファイルを読み込んで,下記の図に示すように、時計まわり90 度回転した画像を作り、その結果をscene1_90.pgmという 画像ファイルに保存するという処理を行うプログラムを作成しなさい。

    cat1  ⇒ catrw90


2.2 画像の回転

予備知識
 a 画像座標系

image_coor width×height画素の画像の画素の位置は(x,y) 座標で表す。左図に示すように、座標原 点 O は画像の左上の画素に,  X 軸は右向きに、そして Y 軸は下向きに設定されています。
画像の中の画素の座標の範 囲は
      0 ≦ xwidth - 1
      0 ≦ yheight - 1

 b 回転変換
point_rotate
座標原点 O を回転中心として、点 P を 反時計回り Q 角度で 回転した後の点を P' とす る。P' の座標は下記の式で計算できます。

          x' = x * cos(Q) - y * sin(Q)

          y' = x * sin(Q) + y * cos(Q)

 c 画像の回転
 画像中心を回転中心として、反時計まわりにQ 角度で回転させるために、まず、画像中心を原点とし、X 軸が右に、Y 軸が上に向くような座標系を設定して、そして各画素の画像座標をその座標系に変換する必要があります。
 画像中心の座標は、
    Cx =  (width - 1.0) * 0.5,
    Cy = (height - 1.0) * 0.5
です。画像座標系のY軸が 下向きであることを考慮すると、画素の画像座標系の座標を(Ix,Iy)とし、画像中心を原点とする直交座標系での座標を(x,y) とすると、下記の座標変換の式が得られ ます。
     x = Ix - Cx
           y = - (Iy - Cy) = Cy - Iy

上の回転変換の式より、回転した後の点の
画像中心を原点とす る直交座標系での座標(x',y')は 次のように計算できます。
 
          x' = x * cos(Q) - y * sin(Q)
            y' = x * sin(Q) + y * cos(Q)

 回転した結果を保存用の画像の中心座標はCx',Cy'とすると、回転した後の画素の画像座 標系での座標(Ix', Iy')は次のように計算 できます。
     Ix' = x'  + Cx'
           Iy' = Cy'y'

d 回転した後の画像の大きさ
 画像が回転すると、その横幅、縦幅が変わります。そのために、画像を回転させるとき、まず結果画像の大きさを計算する必要があります。これを求めるため の方法の一つは、
  1. 画像の四つの角の画素が回転した後の座標を計算します。(c 画像の回転で紹介した式を使って)
    この際、回転した後の画像の中心座標(Cx',Cy')はまだ分からないために、一時的に0とします。

  2.回転した4つの点のX,Y座標の最大値、最小値をそれぞれ、Xm, Xn, Ym, Ynとすると、結果画像の横幅と縦幅が下記の式で計算します。
     width' = Xm - Xn + 1
          height' = Ym - Yn + 1

これで、結果画像を保存するための画像の大きさが計算できて、結果画像が用意できます。そして、Cx', Cy'も計算できるようになります。

e 各画素について処理する
 (c 画像の回転で紹介した式を使って)回転後の各画素の座標値を計算して、結果画像のその位置の画素に回転前の画素の画素値を代入すれば回転変換を行うことが できます。


練習2.4:
 scene1.pgm という画像ファイルを読み込んで,時計まわり30度回転した画像を作り、その結果を scene1_30.pgmという画像ファイルに保存するという処理を行うプログラムを作成しなさい。

考察:
 2. の問題を解くとき、結果画像に「穴が開く」という現象が現れることがあります。その原因を分析して、解決方法を提案しなさい。

2.3 画像の2値化

予備知識
 「2値化」とは入力画像の各画素の値を操作して、結果の画素値は規定した2つの値のどちらになるという処理です。一般的に、事前にある基準値を決め、入 力画素の値がその基準値を超えれば、結果を1に、さもなければ0にします。この基準値は「閾値」といいます。


1.emacsで「threshold.c」というファイルを開く

% emacs threshold.c &

threshold.cに以下の内容にします。
/* Begin threshold.c */
#include "raslib.h"

int main()
{
  Rasimg *img, *outimg;
  int w, h, t, x, y;
  unsigned char **ii, **oo;

  img = pnm_load("scene1.pgm", NULL);
  w = ras_width(img);
  h = ras_height(img);
  outimg = ras_alloc(w, h, 8);
  ii = ras_lnidx(img);
  oo = ras_lnidx(outimg);
 
  t = 128;
  for (y = 0; y < h; ++y) {
    for (x = 0; x < w; ++x) {
      c = ii[y][x];
      if (c > t) oo[y][x] = 255;
      else oo[y][x] = 0;
    }
  }
  pnm_save("scene_bw.pgm", outimg);
  ras_display(outimg);
  ras_end();
  return 0;
}
/* End threshold.c */

 説明
  上記のプログラムでは、閾値が変数tで表し、その値が128です。入力画素の値が閾値を超えれば、結果は255にします。さもなければ0にします。 ”1”ではなく、"255"にする理由は、結果画像を表示するとき、画素値が1の画素と0の画素がどちらも黒に みえるために、区別がつかないことを避けるためです。

2.thread.c をコンパイルし、実行して、scene_bw.pgmの有無とその中身を確認してください。

3.練習:
  閾値を変えて、さまざまな2値化結果を求めて、確認してください。

閾値以上の画素を赤で表示するようにします。

emacsで「threshold_red.c」というファイルを開く
% emacs threshold_red.c &

thread.cに以下の内容にします。
/* Begin threshold_red.c */
#include "raslib.h"

int main()
{
  Rasimg *img = pnm_load("scene1.pgm", NULL);
  int w = ras_width(img);
  int h = ras_height(img);
  Rasimg *outimg = ras_alloc(w, h, 32);
  unsigned char **ii = ras_lnidx(img);
  unsigned int **oo = (unsigned int **)ras_lnidx(outimg);
  int x, y, c, t;

  t = 128;
  for (y = 0; y < h; ++y) {
    for (x = 0; x < w; ++x) {
      c = ii[y][x];
      if (c > t) oo[y][x] = ras_rgb2i(255, 0, 0);
      else oo[y][x] = ras_rgb2i(c, c, c);
    }
  }
  pnm_save("scene_red.ppm", outimg);
  ras_display(outimg);
  ras_end();
  return 0;
}
/* End threshold_red.c */


thread_red.c をコンパイルし、実行して、scene_red.ppmの有無とその中身を確認してください。

練習:
  0-64の画素を青い色に、65-164の画素をそのままで、165-255の画素を黄色にして表示するプログラムを作りなさい。


2.4 ネガ写真を作ろう

cat ⇒ catn

 「ネガ」とは「負」の写真のことです。画素値は負の値を表現できないために、ネガ写真では、赤、緑、青の3原色の補色で「負」の画素値を表します。画像 の原色と補色との間の関係は下記の式で表すことができます。
   補色のr = 255 - 原色のr
   補色のg = 255 - 原色のg
   補色のb = 255 - 原色のb

カラー画像の使い方:

/* 仮に、img は 32ビットフルカラー画像とします。*/
 Rasimg *img = ras_alloc(100, 100, 32);

/* 次に、ras_lnidx() を用いて、画像の各行の先頭アドレスを記述している配
* 列を取り出します。
*/
  unsigned int **ii = (unsigned int **)ras_lnidx(img);

/* 画像の7行目の11列目の画素のR,G,Bの値を取り出します。*/
unsigned int rgb;
int r, g, b;
rgb = ii[6][10];
r = ras_i2r(rgb);
g = ras_i2g(rgb);
b = ras_i2b(rgb);

  /* 画像の20行目の23列目の画素をR=10,G=20,B=30の色に設定します。*/
ii[19][22] = ras_rgb2i(10, 20, 30);

 練習問題
  scene1.ppmという画像を読み込んで,ネガ写真をつくり、scene1_nega.ppm という画像ファイルに保存しなさい。
2.5 画像の拡大と縮小
 画像を拡大・縮小すると、画像の大きさが変わります。画像を拡大すると、元の1画素が複数画素からなる小領域になり、縮小すると、元の画像にある複数画 素からなる小領域が1画素になります。例えば、2倍拡大すると、元の1画素が4画素となり、1/2に縮小すると、元の画像の4画素分が1画素となります。
 元画像の大きさがWxH画素とし、結果画像の大きさがW'xH'とすると、元画像の画素と結果画像の画素の位置関係は下記の式で表現することができま す。

 拡大の場合、元画像の(x, y)位置にある画素が、
     x * W' /W ≦ x' < (x + 1) * W' / W
     y * H' /H ≦ y' < (y + 1) * H' / H
 の範囲内の小領域になり、

 縮小の場合、結果画像の(x', y')位置にある画素は、
     x' * W /W' ≦ x < (x' + 1) * W / W'
     y' * H /H' ≦ y < (y' + 1) * H / H'
 の範囲内の小領域と対応し ます。

  単純拡大: 元画像の画素をそのまま結果画像の対応する小領域にコピーします。
  単純縮小: 結果画像の画素に対応する元画像の小領域の中心にある画素を結果画素の画素値とします。

 練習問題:
  1.下記の画像を3倍拡大した画像を”big3.pgm"という画像ファイルに保存し、同じ画像を1/4に縮小したものを画像 ファイル"small4.pgm"に保存して、その結果を確認しなさい。
    point_rotate
  2. 上記の画像を640x480画素の画像に変換しなさい。

プログラム作成のQ&A