2.画像処理基礎


2−1.エッジ処理

画像のフォーマットに関しては理解できたでしょうか?次は画像処理の基礎について説明していきます.まずは画像のエッジを抽出するプログラムを作りましょう.

比較的簡単なエッジ抽出方法が Sobel オペレータです.Sobel オペレータは局所積和演算でグラジエントの強度(微分値)を求める方法で,

    

に対して, で強度が求まります.また, でエッジの方向を求めることができます.

さて,いきなり難しい話になったので,分かりやすく1次元に一桁の数字が並んでいる場合で考えましょう.エッジとは急に色の変わるところです.例えば,111111999999 というデータの並びがあったとします,1 と 9 の境界がエッジです.それは分かりますね.

それではエッジを機械的に見つける方法を考えてみましょう.分かりましたか?そうです,自分の両隣の数字を見比べればいいわけです.両端には隣がないので無視して 0 にするとして,さきほどのデータの右隣と左隣の差をとっていきましょう.

はい,000008800000 になりますね.見事に値の大きいところがエッジになってます.では,123213897969 ならどうでしょうか?これも両端は 0 にして両隣の差を取っていくと,020217610100 となります.値の大きなところがエッジです.でも先ほどのデータのように綺麗な数字ではないのでこのままではどこがエッジか分かりにくいです.

そこでこれ以上の数字をエッジにするという値を決めましょう.この場合なら 5 以上をエッジと決めれば良さそうです.エッジを分かりやすくするには,処理したデータの 5 以上の値を 9 にして,5 以下のデータを 0 にしてみましょう.000009900000 となります.この 5 のことを閾値(しきいち)と呼び,最後の処理を閾値処理といいます.良く出てくる言葉なので覚えておきましょう.

ここで,両隣の差を取るというのは次の局所演算を行うことに相当します.

つまり,赤い画素に対して両隣の差を取るというのは,左の画素をマイナス 1 倍したものと右の画素を 1 倍したものを足し合わせることと同じです.さて,話を簡単にするために 1 次元で説明しました.これを 2 次元平面に拡張したのが,Sobel オペレータです.ここまでが理解できたら,もう一度上から読み直してみて下さい.

下のプログラムは肝心の Sobel オペレータの部分が抜けています.

#include <stdio.h>
#include <stdlib.h>

//ppm ファイルを読み込む関数(画像サイズは640×480のみ対応)
int ppm_read(char *filename, unsigned char *pimage){
  FILE *fp;
  if((fp=fopen(filename,"rb"))==NULL){
     printf("ファイル%sが開けません\n",filename);
     exit(-1);
  }
  fscanf(fp,"P6\n640 480\n255\n"); //ヘッダを読み飛ばす
  fread(pimage,sizeof(char),640*480*3,fp);
  fclose(fp);
  return 0;
}

//pgm ファイルを書き込む関数(画像サイズは640×480のみ対応)
int pgm_write(char *filename, unsigned char *pimage){
  FILE *fp;
  fp=fopen(filename,"wb");
  fprintf(fp,"P5\n640 480\n255\n");
  fwrite(pimage,sizeof(char),640*480,fp);
  fclose(fp);
  return 0;
}

main(){
  unsigned char *image; //取り込む画像
  unsigned char *edge; //エッジ画像
  int x,y;
  int fx,fy; //オペレータグラジエントの強度
  FILE *fp;

  image = malloc(sizeof(char)*640*480*3); //メモリの確保
  edge = malloc(sizeof(char)*640*480);
  ppm_read("aist.ppm", image); //ファイルの読み込み

  for(y=1;y<480-1;y++){ //Sobel オペレータ
    for(x=1;x<640-1;x++){
      fx = ;// ここを考える
      fy = ;// ここを考える
      if(fx*fx+fy*fy>500) *(edge+640*y+x) = 0x00; //閾値より大きいところは黒
      else *(edge+640*y+x) = 0xff; //それ以外は白
    }
  }

  pgm_write("edge.pgm", edge); //ファイルの書き込み
  free(image); //メモリの開放
  free(edge);
}

まず, pgm について説明しておきます.unsigned char *edge; は edge = malloc(sizeof(char)*640*480); で 640×480 分の配列しか用意していません.これは,edge をグレースケールの画像で表現しているためです.グレースケールの場合はヘッダが P5 となり,拡張子は通常 pgm (portable graymap) となりますが,ppm のままでも特に問題はありません.

次に,ファイルからポインタに画像を読み込む関数と画像を書き込む関数を main の外に出しました.これらの関数に関しては後のプログラムでも使います.ただ,プログラムの簡単化のために画像サイズが 640×480 で固定だとか,# のコメントに対応できていないなどの問題があることは覚えておいて下さい.

それでは,まず aist.ppm をダウンロードして,絵の確認をしてから演習に取り掛かってください.

演習2−1

  1. 上のプログラムを完成させよう.
  2. 閾値を色々と変えて処理してみよう.
  3. 最適な閾値を自動で探すプログラムを考えてみよう.(余力のある人向き)


aist.ppm

edge1.pgm

edge2.pgm

edge3.pgm

edge4.pgm

edge5.pgm

edge6.pgm

edge7.pgm


2−2.背景差分処理

画像処理では背景と前景を分離したいことが多いですが,これは非常に難しい問題です.でももし,前もって前景が映っていない背景のみの画像を撮ることができれば,問題は比較的簡単になります.

このように背景画像がある時に,得られた画像から背景画像を引くことによって前景を切り出す作業を背景差分といいます.次のプログラムは2枚の画像を取り込んで背景差分を取るプログラムです.

#include <stdio.h>
#include <stdlib.h>

int ppm_read(char *filename, unsigned char *pimage); //上と同じ
int pgm_write(char *filename, unsigned char *pimage); //上と同じ

//intを二乗する
int i_square(int i){
  return i*i;
}

main(){
  unsigned char *back; //背景
  unsigned char *image; //取り込む画像
  unsigned char *sabun; //領域
  int x,y,diff,no;
  char filename[64];
  FILE *fp;
  
  back  = malloc(sizeof(char)*640*480*3); //メモリの確保
  image = malloc(sizeof(char)*640*480*3);
  sabun = malloc(sizeof(char)*640*480);
  
  ppm_read("image\back_00.ppm", back); //ファイルの読み込み
  
  for(no=0;no<16;no++){
    sprintf(filename,"image\image_%02d.ppm",no);
    ppm_read(filename, image); //ファイルの読み込み
    
    for(y=0;y<480;y++){ //単純な差分
      for(x=0;x<640;x++){
        diff = i_square(*(image+3*(640*y+x)+0)-*(back+3*(640*y+x)+0)) +
               i_square(*(image+3*(640*y+x)+1)-*(back+3*(640*y+x)+1)) +
               i_square(*(image+3*(640*y+x)+2)-*(back+3*(640*y+x)+2));
      
        if(diff>400) *(sabun+640*y+x) = 0x00;
        else *(sabun+640*y+x) = 0xff;
      }
    }
    
    sprintf(filename,"sabun_%02d.pgm",no);
    pgm_write(filename, sabun); //ファイルの書き込み
  }
  free(image); //メモリの開放
  free(sabun);
  free(back);
}

画像は image.zip を使います.ダウンロードして画像の確認をしましょう.

back_00.ppm〜back_09.ppm が背景の画像です.そして image_00.ppm 〜 image_15.ppm が連続した画像となっています.なぜ背景がたくさんあるのかはひとまず置いておいて,back_00.ppm を背景として,image_??.ppm から back_00.ppm を引いて閾値処理するプログラムになっています.

このプログラムを実行してみて下さい.sabun_??.ppm というファイルができます.背景との引き算をしているので,新しいものが入ってきたらそれが抽出されるはずですが,結果を見ると幾つかの問題があることに気がつくはずです. 計算結果の画像を見てよく考えてみましょう.


image_01.ppm

image_03.pgm

image_05.pgm

image_11.pgm

sabun_01.pgm

sabun_03.pgm

sabun_05.pgm

sabun_11.pgm

考えましたか?それでは問題点をまとめます.

【問題点】
(1) スパイク状のノイズが抽出されてしまう.
(2) 影が抽出されてしまう.
(3) 時によってノイズが多くなる.
(4) 消えたものも抽出されてしまう.

前景は大きな領域で抽出されるはずですが,画面全体に小さな点が抽出されています.これをスパイクノイズといいます.また,色の RGB 値の差を取っているので影も前景とし抽出されてしまいます.また蛍光灯のチラつきや,カメラの特性などによって時々ノイズが多くなったりすることもあります.そして最後に差分からは,消えたのか出たのかが分からないという問題があります.

それでは,問題の解決方法を考えていきます.まず,(1) は比較簡単です.孤立点の除去をすればいいのです.具体的には注目する画素の周りにあまり前景がなければ背景にするという処理をすれば完成です.

(2) に関しては RGB 値の差で前景か背景かを区別しているのが問題です.RGB の差を取るのではなく,HSICIE などの表色系がありますので,それらを使って明るさの変動に強い特徴量に変換して差を取る必要があります.

(3) を解決するために背景のみの画像が複数あるのです.複数の背景を調べてあらかじめ色の変動具合を調べておきます.(4) に関しては残念ながら非常に難しい問題です.ここは基礎を勉強する場なので,残念ながら諦めましょう.

演習2−2

  1. 問題点(1)を解決するプログラムを作ろう.
  2. さらに問題点(2)を解決するプログラムに改良しよう.
  3. さらに問題点(3)を解決するプログラムに改良しよう.
  4. 問題点(4)を解決する方法を考えてみよう.


sabun_01.pgm

sabun_03.pgm

sabun_05.pgm

sabun_11.pgm

ひとまずはこれくらいを目標にしてみましょう.完全に切り出すというのは難しいです.画像の研究をやっていると人間の目がいかにすごいかを実感できます.


| トップ | 0章へ | 1章へ | 2章へ | 3章へ | 4章へ | 5章へ |
Copyright(c) Masaki Onishi All rights reserved.