画像とベクトル

パターン認識だの画像認識だの画像処理だのの下準備です。
コードは全部processingです。

白黒2値

画像と画像を比較することを考えます。
画像の比較、とは。ある画像が他の画像と似てるだとか似ていないだとかをコンピーターに判別してもらうための一つのやり方を指します。
簡単のため、画像は白黒2値画像とし、そのサイズは5×5ピクセルと考えます。

比較の手法の一つとして、与えられた白黒2値画像を、画素の値を成分とする1次元配列、あるいはN次のベクトルとして考えます。現在の例の場合、画素の総数は5×5の25ピクセルですので、生成されるのはN=25次のベクトルです。ベクトルの25個の成分は各々0か1の値をとります。

成分各々が0か1というのは、プログラミングする際には実際に0と1でもよいし、0とそれ以外でも良いし、0と255でも良いし、bool値でも良いものです。

ベクトルというのは向きと大きさを表す性質を持ちますので、これを利用するとなにやらいろいろできそうな気がするというか、できるので変換します。

画像データが2次元配列ないし、2次元のデータ構造で表されるのであれば、これを1次元配列ないしベクトルとして扱うためには任意の、あるいは暗黙の内の変換の方式を定める必要があります。

プログラム的には2次元配列と1次元配列の変換をどうやってするかという話です。

ここではおそらく最も一般的に用いられるであろう、左上から始まって1行ずつ取得する方式を想定します。

//2次元配列から1次元配列へ
float[] ToArray(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  for(int i = 0; i<m; i++)
  {
    var row = input[i];
    for(int j = 0; j<n; j++)
    {
      var val = row[j];      
      ret[m*i+j]=val;      
    }
  }
  return ret;
}

//同じことだがループの走査に戻り値のベクトルを用いた場合
float[] ToArray2(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  
  for(int i = 0; i<ret.length; i++)
  {    
    int x = (int)(i%n);
    int y = (int)(i/n);
    ret[i]=input[y][x];
  }
  return ret;
}

大きさ、長さ

1次元配列化されたベクトルの大きさ、あるいは長さについて考えます。

ベクトルの長さは成分の2乗の総和のルートをとると求まります。

白黒2値画像を1次元配列化した場合、その成分を0か1として扱うならば、ベクトルの長さは1である成分の個数に依存します。0の成分は長さを求める時に計算に関与しないからです。

つまりどこのどのあたりのピクセルが塗られているとかではなく、いくつのピクセルが塗られているかというパラメータしか、画像を1次元配列化したベクトルの長さには関与しないということです。

例えば5×5画素の画像に関して、
1ピクセルのみ塗られているならば、
その画像の1次元配列化されたベクトルの長さは1です。

2ピクセル塗られた場合、
その画像の1次元配列化されたベクトルの長さは$${\sqrt 2=1.4142135…}$$です。

そのことのテストコード


void setup()
{  
  size(500,500);  
  grid = new float[row_count][col_count];
  cell_width = width/col_count;
  cell_height = height/col_count;
}

void mouseClicked()
{
  var coord = HitGridCoord(grid,grid_ox,grid_oy,cell_width,cell_height,mouseX,mouseY);
  if(coord!=null)
  {
    if(grid[(int)coord.x][(int)coord.y]==0)
    {
      grid[(int)coord.x][(int)coord.y]=1;
    }
    else
    {
      grid[(int)coord.x][(int)coord.y]=0;      
    }
  }
}

float[][] grid;
int row_count = 5;
int col_count = 5;
float grid_ox = 0;
float grid_oy = 0;
float cell_width;
float cell_height;

void draw()
{
  strokeWeight(2);
  
  fill(0);
  DrawGrid(grid, cell_width, cell_height);  
  var len = VectorLength(ToArray(grid));
  fill(0);
  text("Length = "+str(len),0,0,100,20);
}

PVector HitGridCoord(
  float[][] grid, 
  float grid_ox, float grid_oy,
  float cell_w, float cell_h, float x, float y)
{
  //out of range
  int m = grid.length;
  int n = grid[0].length;
  float total_width = n*cell_w;
  float total_height = m*cell_h;
  if(x<grid_ox||grid_ox+total_width<x){return null;}
  if(y<grid_oy||grid_oy+total_height<y){return null;}
  
  float gx = x-grid_ox;
  float gy = y-grid_oy;  
  int i = (int)(gy/cell_h);
  int j = (int)(gx/cell_w);
  return new PVector(i,j);
}

void DrawGrid(float[][] input, float w, float h)
{
  DrawGrid(input, 0, 0, w, h);
}

void DrawGrid(float[][] input, float ox, float oy, float w, float h)
{
  int m = input.length;
  int n = input[0].length;
  
  var g = getGraphics();
  var current_color = g.fillColor;
  var current_stroke_color = g.strokeColor;
  
  stroke(0);
  
  for(int i = 0; i<m; i++)
  {
    for(int j = 0; j<n; j++)
    {
      //描画条件
      if(input[i][j]>0)
      {
        fill(current_color);        
      }
      else
      {
        fill(255);
      }
      rect(ox+j*w,oy+i*h,w,h);
    }
  }  
  fill(current_color);
  stroke(current_stroke_color);
}

float VectorLength(float[] vector)
{
  float sum = 0;  
  for(var val : vector)
  {
    sum += val*val;
  }
  return sqrt(sum);
}
//2次元配列から1次元配列へ
float[] ToArray(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  for(int i = 0; i<m; i++)
  {
    var row = input[i];
    for(int j = 0; j<n; j++)
    {
      var val = row[j];      
      ret[m*i+j]=val;      
    }
  }
  return ret;
}

//2次元配列から1次元配列へ
float[] ToArray2(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  
  for(int i = 0; i<ret.length; i++)
  {    
    int x = (int)(i%n);
    int y = (int)(i/n);
    ret[i]=input[y][x];
  }
  return ret;
}


ピクセルの1つが黒である画像のベクトルとしての大きさは1
ベクトルの大きさを見ると黒ピクセルの位置は判別できない
黒ピクセルの数の多い少ないは判別できる



向き、角度

ベクトルは2つ揃うと角度を測ることができます。

内積

$$
Dot(\bold{v}1,\bold{v}2)=|\bold{v}1||\bold{v}2|cos\theta
$$

より

$$
cos\theta=\frac{Dot(\bold{v}1,\bold{v}2)}{|\bold{v}1||\bold{v}2|}
$$

2つのベクトルに関して
$${cos\theta=1}$$の時、2つのベクトルは向きが一致します。
$${cos\theta=-1}$$の時、2つのベクトルは向きが真反対です。
$${cos\theta=0}$$の時、2つのベクトルは直交します。なす角90度です。

とりあえずお試し用コードをみます。


void setup()
{  
  size(500,500);  
  grid1 = new float[row_count][col_count];
  grid2 = new float[row_count][col_count];
  cell_width = width/col_count;
  cell_height = height/col_count;
}

void mouseClicked()
{
    var coord = HitGridCoord(grid1,grid_ox,grid_oy,cell_width,cell_height,mouseX,mouseY);
    if(coord!=null)
    {
      if(mouseButton==LEFT)
      {      
        if(grid1[(int)coord.x][(int)coord.y]==0){grid1[(int)coord.x][(int)coord.y]=1;}
        else{grid1[(int)coord.x][(int)coord.y]=0;}
      }
      if(mouseButton==RIGHT)
      {
        if(grid2[(int)coord.x][(int)coord.y]==0){grid2[(int)coord.x][(int)coord.y]=1;}
        else{grid2[(int)coord.x][(int)coord.y]=0;}        
      }
    }
}

float[][] grid1;
float[][] grid2;
int row_count = 5;
int col_count = 5;
float grid_ox = 0;
float grid_oy = 0;
float cell_width;
float cell_height;

void draw()
{
  background(255);
  strokeWeight(2);
  
  DrawGrid2(grid1, grid2, 0, 0, cell_width, cell_height);  

  var len = VectorLength(ToArray(grid1));
  fill(0);
  text("V1 Length = "+str(len),0,0,100,20);
  var theta = VectorCos(ToArray(grid1),ToArray(grid2));
  fill(0);
  text("Theta = "+str(theta),0,20,100,20);
  
  
}

PVector HitGridCoord(
  float[][] grid, 
  float grid_ox, float grid_oy,
  float cell_w, float cell_h, float x, float y)
{
  //out of range
  int m = grid.length;
  int n = grid[0].length;
  float total_width = n*cell_w;
  float total_height = m*cell_h;
  if(x<grid_ox||grid_ox+total_width<x){return null;}
  if(y<grid_oy||grid_oy+total_height<y){return null;}
  
  float gx = x-grid_ox;
  float gy = y-grid_oy;  
  int i = (int)(gy/cell_h);
  int j = (int)(gx/cell_w);
  return new PVector(i,j);
}

void DrawGrid2(float[][] input1, float[][] input2, float ox, float oy, float w, float h)
{
  int m = input1.length;
  int n = input1[0].length;
  
  var g = getGraphics();
  var current_color = g.fillColor;
  var current_stroke_color = g.strokeColor;
  
  stroke(0);
  
  for(int i = 0; i<m; i++)
  {
    for(int j = 0; j<n; j++)
    {
      //描画条件
      if(input1[i][j]>0&&input2[i][j]>0)
      {
        fill(0,255,0);        
      }
      else if(input1[i][j]>0&&input2[i][j]==0)
      {
        fill(255,0,0);  
      }
      else if(input1[i][j]==0&&input2[i][j]>0)
      {
        fill(0,0,255);  
      }   
      else
      {
        fill(255);
      }
      
      rect(ox+j*w,oy+i*h,w,h);
    }
  }  
  fill(current_color);
  stroke(current_stroke_color);
}

//2次元配列から1次元配列へ
float[] ToArray(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  for(int i = 0; i<m; i++)
  {
    var row = input[i];
    for(int j = 0; j<n; j++)
    {
      var val = row[j];      
      ret[m*i+j]=val;      
    }
  }
  return ret;
}

//2次元配列から1次元配列へ
float[] ToArray2(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  
  for(int i = 0; i<ret.length; i++)
  {    
    int x = (int)(i%n);
    int y = (int)(i/n);
    ret[i]=input[y][x];
  }
  return ret;
}


float VectorLength(float[] vector)
{
  float sum = 0;  
  for(var val : vector)
  {
    sum += val*val;
  }
  return sqrt(sum);
}

float DotProduct(float[] v1, float[] v2)
{
  float ret = 0;
  for(int i = 0; i<v1.length; i++)
  {
    ret += v1[i]*v2[i];
  }
  return ret;
}

float VectorCos(float[] v1, float [] v2)
{
  return DotProduct(v1,v2)/(VectorLength(v1)*VectorLength(v2));
}

上記のコードは2つの画像を意味するグリッド、それを1次元配列化した時の角度を比較して左上の方に計算して出します。

セルを左クリックするとグリッド1の対応セルが黒画素を意味する赤画素になります。
セルを右クリックするとグリッド2の対応セルが黒画素を意味する青画素になります。
両方のグリッドが黒画素を配置しているなら表示色は緑になります。

図a
図b

互いに一致する画素を持たないなら成分同士の積の総和である内積は常に0。コサイン0。角度90度。ベクトル同士は直交します。

対して画素がすべて一致するならコサイン1。ベクトル同士は同じ方向を向きます。

図c
図d


図e:互いに黒画素を2つ持ち、1つの黒画素が共通。
図f:互いに3つの黒画素のうち、2つの黒画素が共通。
図g:互いに3つの黒画素のうち、1つの黒画素が共通。


図h:1黒画素と2黒画素で1黒画素共通。
図i:1黒画素と3黒画素で1黒画素共通。
図j:2黒画素と3黒画素で2黒画素共通。
図fと比較すると、共通でない成分が伸びるくらいなら0成分であった方が
角度が小さいと分かる。
2黒画素と3黒画素で1黒画素共通。

つまりある画像を1次元配列、ないしベクトルと考え、かつ2枚の画像をベクトルと考えてそれらの角度を求めてみた場合、だいたいどれくらいの画素が重複しているかがほのめかされる。

グレースケールの場合

テスト用コード


void setup()
{  
  size(500,500);  
  grid1 = new float[row_count][col_count];

  cell_width = width/col_count;
  cell_height = height/col_count;
}

int _clickCount = 0;
void mouseClicked(MouseEvent m)
{
    var coord = HitGridCoord(grid1,grid_ox,grid_oy,cell_width,cell_height,mouseX,mouseY);
    if(coord!=null)
    {
      if(mouseButton==LEFT)
      {             
        grid1[(int)coord.x][(int)coord.y]=rclamp255(grid1[(int)coord.x][(int)coord.y]+10);
      }
      if(mouseButton==RIGHT)
      {
        int count = m.getCount();
        if(count>=2)
        {
          grid1[(int)coord.x][(int)coord.y]=0;
        }
        else
        {
          grid1[(int)coord.x][(int)coord.y]=255;
        }
      }
    }
}

float[][] grid1;
int row_count = 5;
int col_count = 5;
float grid_ox = 0;
float grid_oy = 0;
float cell_width;
float cell_height;

void draw()
{
  background(255);
  strokeWeight(2);
  
  fill(0);
  DrawGridGray(grid1, 0, 0, cell_width, cell_height);  

  var len = VectorLength(ToArray(grid1));
  
  fill(255);
  rect(0,0,500,20);
  fill(0);  
  text("V1 Length = "+str(len),0,0,500,20); 
}

void set(float[][] grid, float val)
{
  int m = grid.length;
  int n = grid[0].length; 
  for(int i = 0; i<m; i++)
  {
    for(int j = 0; j<n; j++)
    {
      grid[i][j]=val;
    }
  }
}

PVector HitGridCoord(
  float[][] grid, 
  float grid_ox, float grid_oy,
  float cell_w, float cell_h, float x, float y)
{
  //out of range
  int m = grid.length;
  int n = grid[0].length;
  float total_width = n*cell_w;
  float total_height = m*cell_h;
  if(x<grid_ox||grid_ox+total_width<x){return null;}
  if(y<grid_oy||grid_oy+total_height<y){return null;}
  
  float gx = x-grid_ox;
  float gy = y-grid_oy;  
  int i = (int)(gy/cell_h);
  int j = (int)(gx/cell_w);
  return new PVector(i,j);
}

float clamp255(float val)
{  
  if(val<0){val=0;}
  if(255<val){val=255;}
  return val;
}

void DrawGridGray(float[][] input, float ox, float oy, float w, float h)
{
  int m = input.length;
  int n = input[0].length;
  
  var g = getGraphics();
  var current_color = g.fillColor;
  var current_stroke_color = g.strokeColor;
  
  stroke(0);
  
  for(int i = 0; i<m; i++)
  {
    for(int j = 0; j<n; j++)
    {    
      fill(255*clamp255(input[i][j])/255);
      rect(ox+j*w,oy+i*h,w,h);
    }
  }  
  fill(current_color);
  stroke(current_stroke_color);
}

//2次元配列から1次元配列へ
float[] ToArray(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  for(int i = 0; i<m; i++)
  {
    var row = input[i];
    for(int j = 0; j<n; j++)
    {
      var val = row[j];      
      ret[m*i+j]=val;      
    }
  }
  return ret;
}

//2次元配列から1次元配列へ
float[] ToArray2(float[][] input)
{
  int m = input.length;
  int n = input[0].length;  
  var ret = new float[m*n];  
  
  for(int i = 0; i<ret.length; i++)
  {    
    int x = (int)(i%n);
    int y = (int)(i/n);
    ret[i]=input[y][x];
  }
  return ret;
}

float VectorLength(float[] vector)
{
  float sum = 0;  
  for(var val : vector)
  {
    sum += val*val;
  }
  return sqrt(sum);
}

float DotProduct(float[] v1, float[] v2)
{
  float ret = 0;
  for(int i = 0; i<v1.length; i++)
  {
    ret += v1[i]*v2[i];
  }
  return ret;
}

float VectorCos(float[] v1, float [] v2)
{
  return DotProduct(v1,v2)/(VectorLength(v1)*VectorLength(v2));
}

float rclamp255(float val)
{
  if(val<0){return 255+val;}
  if(255<val){return val-255;}
  return val;
}

白黒2値の時は0が白、1が黒と判定しましたが、
グレースケールは0が黒、255が白としています。

すべてのセルが0

テストコードではセルを右クリックで255(白)、右ダブルクリックで0(黒)。左クリックで+10としています。

セルを2つ左クリック。25成分のベクトルのうち、2つの成分が10であるベクトルの長さ=14.142136…


9このセルを適当にクリックした長さ255くらいのベクトル
1つのセルのみ255とした長さ255のベクトル

白黒2値の場合、成分の値がすべて固定なので、ベクトルの長さの違いは黒のセルの個数に関係しましたが、グレースケールは成分の取りえる値に幅があります。
ゆえにベクトルの長さが同じであるなら、成分が0でないセルが少ない画像の方が輝度が大きい、すなわち画像がなんか明るい感じある、みたいな解釈ができます。


この記事が気に入ったらサポートをしてみませんか?