ラインスキャンカメラ(TSL1401)の使い方ー値取得から2値化までー

#はじめに
ロボット、特にライントレース競技において赤外線センサーの代わりにカメラを用いた画像認識マシンが存在する。本記事は、画像認識マシンを作成したいと考えている方に向けた入門編である。今回は、値取得から2値化までを扱う。
![20190302_125520.jpg](https://qiita-image-store.s3.amazonaws.com/0/395048/d95952b1-877c-ce55-e0f1-69747ed4e490.jpeg)

#ラインスキャンカメラ（TSL1401）について
通常のカメラでは平面の画像（緑の範囲）を撮影が可能である。対して、ラインスキャンカメラでは1ライン（1x128ピクセル）のみ（赤の範囲）を撮影が可能である。
![hanni.jpg](https://qiita-image-store.s3.amazonaws.com/0/395048/81682e95-8bc4-bfcf-0d14-33b397d7c60e.jpeg)

つまり、通常のカメラには可能なラインの形状（カーブなど）を判断することが出来ない。しかし、ライントレースなどに必要なセンサーとコースとのズレは判断可能である。また、シンプルかつ高速化しやすいメリットも存在する。


#使用するマイコンボードについて
ラインスキャンカメラ（以降、カメラ）を制御するためのマイコンボードとして、Arduino UNO(の互換品）を使用する。また、Arduinoの最低限の使い方を知っているものとして話を進める。
加えて、実際にロボット競技などで使用する場合は別のものを使用することを強くお勧めする。ちなみに、使用したことのあるマイコンとして、RX621,RX62G,R8C38Aなどがある。現在は、AD変換が高速なRXシリーズを使用している。

#マイコンボードとカメラの接続
カメラの端子にVCC,GND,SI,CLK,A0が存在する。なお、購入したカメラによっては端子が異なる場合があるが、この５つは存在するものと思われる。
![20190408_221545.jpg](https://qiita-image-store.s3.amazonaws.com/0/395048/51f8f72b-3d8c-bca4-b457-30ca71f30e2c.jpeg)

Arduinoとカメラの接続端子は以下のようにした。もちろん別の端子を使用しても構わないが、以降紹介するサンプルプログラムはこのように接続されていることを想定している。

|Arduino|カメラ|
|:---:|:---:|:---:|
|5V|VCC|
|GND|GND|
|7|SI|
|8|CLK|
|A0|A0|
![20190408_221516.jpg](https://qiita-image-store.s3.amazonaws.com/0/395048/d8e539b9-b09e-6314-b041-4935ae304e81.jpeg)

#画像の取得
画像取得に必要なプログラム全体は以下リンク。
[サンプル（TSL1401-0）](https://github.com/hirata0517masato/TSL1401-sample/blob/master/TSL1401-0/TSL1401-0.ino)
サンプルプログラムを動作させ、ArduinoIDE内のシリアルモニタで出力を確認すると「,」区切りの128個の数値が横一列に出力され、次の行に2つの数値が出力される。
###解説
以下のプログラムはサンプルの一部である。

```c
省略
void loop() {
  while(1){
    ImageCapture();     //イメージキャプチャー

    cnt++;
    if(cnt>50){
      for(int i = 0; i < 128; i++){
         Serial.print(ImageData[i]);
         Serial.print(",");
      }
      Serial.println(" ");
      Serial.print(Max);
      Serial.print(" ");
      Serial.println(Min);
      
      cnt=0;
    }
  }
}
省略
```

カメラからの値を取得するにはImageCapture()関数を使用する。すると、配列ImageDataの0~127に値が格納されている。ここでの値とは、カメラが取得した明るさである。値が低いほど暗く、高いほど明るいということを示す。
また、サンプルでは50回に1回のペースで値を出力している。これが先ほどシリアルモニタに出力されていた値の一行目である。二行目には値の最大値、最小値を出力している。

加えて、実際の競技などで使用する場合、この出力は行わないでください。高速に撮影が出来なくなります。

また、以下のように撮影した際の出力をグラフ化すると
![20190408_221703.jpg](https://qiita-image-store.s3.amazonaws.com/0/395048/4fbbf539-1934-a4d2-298c-7fef8c10f8c2.jpeg)

![gurahu.png](https://qiita-image-store.s3.amazonaws.com/0/395048/3f7cf835-d68b-7cc1-eea1-a61655d57c71.png)
このようになるはずである。値は部屋の明るさに左右されるため、現状は気にする必要はない。
ここまでの内容がカメラから値を取得する最低限の内容である。

#撮影範囲の変更
先ほどのグラフを確認すると、左端の数値が少し高いことがわかる。
![gurahu-Left.png](https://qiita-image-store.s3.amazonaws.com/0/395048/5ee9b61f-6e9e-67ec-8327-e91399024bc1.png)
この問題の原因として、カメラのレンズが考えられる。レンズは平面ではなく球面である。そのため、左右の端になればなるど画像に歪みが生じ、値が不安定となりやすい。
この問題を解決するため、撮影範囲の変更を行う。簡単に言い換えると、左右の端は使わないということである。

変更後のサンプルプログラム全体は以下リンク。
[サンプル（TSL1401-1）](https://github.com/hirata0517masato/TSL1401-sample/blob/master/TSL1401-1/TSL1401-1.ino)
###解説
以下のプログラムはサンプルの一部である。

```c
#define   LineStart   27    /* カメラで見る範囲*/
#define   LineStop    100
省略
void loop() {
  while(1){
    ImageCapture(LineStart,LineStop);     //イメージキャプチャー
省略
  }
}
void ImageCapture(int linestart, int linestop){   
  unsigned char i;
  Max = 0,Min = 1024;

  digitalWrite( TSL_SI, HIGH );
  digitalWrite( TSL_CLK, HIGH );
  digitalWrite( TSL_SI, LOW );
  digitalWrite( TSL_CLK, LOW );

  for(i = 0; i < linestart; i++) {    
    digitalWrite( TSL_CLK, HIGH ); 
    digitalWrite( TSL_CLK, LOW );
  }

  for(i = linestart; i <= linestop; i++) {        
    digitalWrite( TSL_CLK, HIGH ); 
    ImageData[i] = analogRead(TSL_AD);  
    digitalWrite( TSL_CLK, LOW );

    if(Max < ImageData[i]){
      Max = ImageData[i];
    }     
    if(Min > ImageData[i]){
      Min = ImageData[i];
    }
  }

  for(i = linestop+1; i < 128; i++) {   
    digitalWrite( TSL_CLK, HIGH ); 
    digitalWrite( TSL_CLK, LOW );
  }
}
```

上記のように変更を加えた。
主な変更点として、LineStartとLineStopが追加されたことである。前回までは0~127までが範囲として固定されていた。今回の範囲はLineStart~LineStopが範囲となる。この範囲はカメラの設置位置や角度に合わせて各自調整してほしい。

また、0~LineStart-1,LineStop+1~127に関してはAD変換を行わないため、前回より少し高速な撮影が可能となっている。

#明るさの自動調整
現状の問題点として、部屋の明るさが異なった場合、数値も変化してしまう。実際に暗い部屋で低い値（暗い値）が出力されることはカメラとしては当然のことである。しかし、物体やラインの認識を行う際は明るさは一定である方が望ましい。
また、ロボット競技などにおいてはマシンの移動範囲（コース全体）が一定の明るさではない可能性がある（影や蛍光灯の有無、太陽光など）。
そこで、明るさを一定に保つために自動調整を行う必要がある。

変更後のサンプルプログラム全体は以下リンク。
[サンプル（TSL1401-2）](https://github.com/hirata0517masato/TSL1401-sample/blob/master/TSL1401-2/TSL1401-2.ino)

###解説
以下のプログラムはサンプルの一部である。

```c
省略
#define   Line_Max  450   /* ライン白色MAX値の設定 */
省略
long  EXPOSURE_timer = 10000;      /* 露光時間   */
省略
void loop() {
  while(1){
    expose();                             //露光時間 
    ImageCapture(LineStart,LineStop);     //イメージキャプチャー
省略
  }
}
省略
void expose( void )
{
  long i;
   
  if(Line_Max - Max < 0){
    EXPOSURE_timer -= 100;
  }else{
    EXPOSURE_timer += 100;
  }

  if( EXPOSURE_timer > 100000000) EXPOSURE_timer = 100000000;
  else if( EXPOSURE_timer <= 0 ) EXPOSURE_timer = 0;
  
  for(i=0;i<EXPOSURE_timer;i++)asm("nop");
}
```


変更点として、Line_Max,EXPOSURE_timer,expose()が追加された。
Line_Maxを撮影した値の最大値の基準として扱う。つまり、撮影した値の最大値がLine_Maxを超えていた場合、明るい画像と判断する。反対にLine_Maxを下回っていたら暗い画像と判断する。そして、明るい画像であれば暗くなるように、暗い画像であれば明るくなるように調整を行う。その為の関数がexpose()である。

expose()関数の重要な部分は以下の一行である。

```c
for(i=0;i<EXPOSURE_timer;i++)asm("nop");
```
このプログラムは「何もしない」をEXPOSURE_timer回行うということである。
しかし、実際には「何もしない」をしている間にカメラのレンズに光が取り込まれている。
つまり、一般的なカメラの場合のシャッターを開けてから閉めるまでの時間に相当する。
このシャッターが開いている時間が短い場合、十分に光が取り込めず暗い画像となる。反対に長い場合、より光をレンズに多く取り込むこととなり、明るい画像となる。
このシャッターを開けておく時間（露光時間）を調整することで画像の明るさを変更することができる。

まとめると
最大値がLine_Maxを超えていた場合、明るい画像と判断し、露光時間を短くする。
反対にLine_Maxを下回っていたら暗い画像と判断し、露光時間を長くする。

```c
  if(Line_Max - Max < 0){
    EXPOSURE_timer -= 100;
  }else{
    EXPOSURE_timer += 100;
  }
```

変化量は仮に100としているが、より高速に明るさの変化に対応するには変化量を増加させればよい。ただし、大きすぎた場合、明るさが安定しなくなる可能性があるので注意。
また、基準と最大値の差と比例させるなど、より速く基準値に収束させる工夫をしても面白いだろう。

実際に撮影したデータをグラフ化したものを示す。EXPOSURE_timer（露光時間）は11100であった。最大値がLine_Max（サンプルでは450に設定）と等しいことがわかる。
![gurahu-450.png](https://qiita-image-store.s3.amazonaws.com/0/395048/88152355-3b99-1e65-88ba-0d6e5ecddb1f.png)

また、明るさを暗くするため、被写体（白黒のコース）から遠ざけてみた。
![gurahu-450-kurai.png](https://qiita-image-store.s3.amazonaws.com/0/395048/3e3f30b5-59ca-6355-6933-b4f2606f049d.png)
しかし、明るさの最大値はほぼ450である。違いは、グラフから読み取れないがEXPOSURE_timer（露光時間）が17800に増加していることである。
少し撮影速度が遅くなってしまったが安定した値を取得することができた。

#2値化
カメラから取得した値、生データを2値化する。2値化を簡単に説明すると、白黒画像にすることである。ここでは、黒なら0、白なら1とする。
ちなみに、被写体（コースなど）の灰色を認識したい場合、3値化することも可能である。しかし、初めのうちはお勧めしない。
[サンプル（TSL1401-3）](https://github.com/hirata0517masato/TSL1401-sample/blob/master/TSL1401-3/TSL1401-3.ino)

###解説
以下のプログラムはサンプルの一部である。

```c
省略
int   BinarizationData[128];        /* ２値化          */

int   Max = 0,Min,Ave;  /*カメラ読み取り最大値、最小値、平均値*/

void loop() {
  while(1){
    expose();                             //露光時間 
    ImageCapture(LineStart,LineStop);     //イメージキャプチャー
    binarization(LineStart,LineStop);     //２値化

    cnt++;
    if(cnt>50){
      for(int i = LineStart; i <= LineStop; i++)Serial.print(BinarizationData[i]);

      Serial.println(" ");
      Serial.print(Max);
      Serial.print(" ");
      Serial.print(Min);
      Serial.print(" ");
      Serial.println(EXPOSURE_timer);
      cnt=0;
    }
  }
}
省略

void binarization(int linestart, int linestop)
{
  int i,a;
  /* 最高値と最低値から間の値を求める */
  Ave  = (Max + Min) / 2;

  /* 黒は０　白は１にする */
  if(Max - Min < 100){
    if( Max > Line_Max - 200 ){
      for(i = linestart ; i <= linestop; i++) {
        BinarizationData[i] = 1;
      }
    }else{         
      for(i = linestart ; i <= linestop; i++) {
        BinarizationData[i] = 0;
      }
    }
  }else{
    for(i = linestart ; i <= linestop; i++) {
      if(  ImageData[i] > Ave ){     
        BinarizationData[i] = 1;
      }else{
        BinarizationData[i] = 0;
      } 
    }
  }
  //範囲外は黒に
  for(i = 0; i < linestart; i++){
    BinarizationData[i] = 0;
  }
  for(i = linestop+1; i < 128; i++){
    BinarizationData[i] = 0;
  }
}
```

主な変更点として、イメージキャプチャの後にbinarization(LineStart,LineStop)関数を呼び出し、2値化を行っていることである。2値化された値は、配列BinarizationDataに格納されている。

では、2値化を行うためには、どのような値が白（1）でどのような値が黒（0）なのかを判断する必要がある。最も単純な方法として、最大値と最小値の平均値。つまり、中間の明るさより暗いのか明るいのかで判断する方法である。

平均値(Ave)を求める方法は以下のとおりである。

```c
/* 最高値と最低値から間の値を求める */
Ave  = (Max + Min) / 2;
```

ちなみに、より高速に処理するために以下のように変更してもよい。

```c
/* 最高値と最低値から間の値を求める */
Ave  = (Max + Min) >> 1;
```

そして、この平均値を閾値として以下のプログラムで白黒の判別を行う。

```c

   省略
    for(i = linestart ; i <= linestop; i++) {
      if(  ImageData[i] > Ave ){     
        BinarizationData[i] = 1;
      }else{
        BinarizationData[i] = 0;
      } 
    }
```

また

```c
if(Max - Min < 100){
```
の条件が成り立つ場合は、最大値と最小値の差が小さい場合である（仮で100を使用）。つまり、真っ白or真っ黒である。その場合は先ほどの平均値から判断することができない。
そこで、別の判断方法を利用する。ここで基準とするのはLine_Maxである。
ここまでの内容が正しく動作していた場合、明るさの最大値（白色）はほぼLine_Maxと同じとなっているはずである。
つまり、撮影した値の最大値が基準となるLine_Maxに近ければ真っ白、差が大きければ（仮に200を使用）真っ黒として判断する。

```c
省略
  if(Max - Min < 100){
    if( Max > Line_Max - 200 ){
      for(i = linestart ; i <= linestop; i++) {
        BinarizationData[i] = 1;
      }
    }else{         
      for(i = linestart ; i <= linestop; i++) {
        BinarizationData[i] = 0;
      }
    }
  }else{
    省略
  }
省略
```


2値化した後の出力を確認すると、以下のようになる。
（実際のデータではなく手動で入力しました、すいません）

0000000000000001111111111111100000000000000000

これで、2値化が完了となり、カメラから白黒画像を撮影したこととなる。

#おわりに
今回は、ラインスキャンカメラ（TSL1401）とArduinoを用いて2値化までの解説を行った。
実際にロボット競技で使用するには不十分な内容である。しかし、競技によりルール、コースが異なるため、ここで一区切りとする。
今後、実際に競技で使用するために必要な内容を公開できればと考えています。

もし、内容に間違い、わかりにくい点があれば連絡していただけると助かります。