Take’s diary

Macとマイコンに関すること--ワクワクの製作日記

スイッチサイエンスの ESP-WROOM-02 開発ボードで、FLIR LEPTON 赤外線カメラを動かしてみる(その1)

今回は,

こんなのを作ってみました。  

   f:id:TAKEsan:20160217151437j:plain:w500
             こんな風につないで(電源はeneloop電池)

        
Macで画像表示させると(赤外線カメラ->ESP-WROOM-02->WiFi(OSC)->Mac(Openframeworksアプリ))


 前回の記事でちょっと紹介しましたが、赤外線カメラ(FLIR LEPTON 80X60 ドット)を手に入れました。これを使った商品はiPhone用にも販売されていて、とってもかっこ良くて性能の割には比較的安いのですが、センサーそのものの方が若干価格が高いというような逆転現象が起きています。
    f:id:TAKEsan:20160216140354j:plain:w200f:id:TAKEsan:20160216140351j:plain:w200
 しかし高い。いつものように「どこから買ってもいいや」でなく、購入先を検討。とても慎重になってしまいます。(スイッチサイエンス、秋月、 Degi-Keyで扱ってます)
どうせならということで、あまり値段が変わらないシャッター付きにしました。表面に付いているシャッター部品は、不要な場合は簡単に取り外せます。普通に使っているデジカメの電源を入れると、レンズカバーが開きますがそれと同じような役割をしています。

          f:id:TAKEsan:20160205115549j:plain:w300    

FLIR LEPTON 赤外線センサーについて

 i2cとSPIを使っていて、i2cは制御用、SPIは画像配信用に使っているようです。とりあえずのお試し用ならSPIだけでいいみたいですが、私の持っているIoT制御機器で実行できるのかどうかが疑問ではありますが、ざっとわかった範囲で処理の流れを書くと、

  1. センサー(カメラ)に組み込まれている温度センサーを基準にして、センサーで読み取れる範囲の最高最低温度を計算。
  2. 最高最低温度を基準にして、各60X80=4800ピクセルで読み取った値を加味した濃淡データを計算(結果が14bit=8192階調なのでかなり精密)。
  3. 2+80word(=164byte 最初の2wordは行番号など)のデータを1フレームあたり60回SPIで流す。
  4. 出力されたデータを8192階調から256階調にホスト側で修正。(i2cの制御で8bitに変換して出力できるようです。今回は無視)

 センサー周囲又はセンサーの発熱で温度が変わっても、あくまでも基準ですからデータの信憑性は変わりません。画像配信だけならセンサー温度を考慮しなくてもOK。
 じゃー読み取ったデータの正確な温度を知るには? i2cで現在のセンサー温度を読み取って、逆算してやれば4800ピクセルのどの位置でも温度が数値で出てきます。
 このセンサと、Webカメラから赤外線フィルタを取っただけのものと大きな違いは、赤外線ライトが必要ない事。4800ドットのすべてのピクセルで温度が測定できる点です。
 他の非接触赤外線センサーと1ピクセルあたりの金額を比較すると、LEPTON=7円、OMURON≒400円、MXL90621 ≒150円 と、このセンサーは圧倒的に安いけれども個体ではやっぱり高い。 
       f:id:TAKEsan:20150809111402j:plain:w100      [f:id:TAKEsan:20160111134338j:plain:w200[
     OMURON非接触赤外線センサー           MLX90621

Pi2でさっそく実験!!

 Sparkfunにチュートリアルがあったので、https://learn.sparkfun.com/tutorials/flir-lepton-hookup-guideで試してみたら簡単に実行できちゃいました。でも...Piは、Iotとして使うには大きすぎる。

        
         Pi2用のExample結構早い。このくらいのスピードが理想的

Edisonでは?

 では、充分ちっちゃいEDISONでは? 同じサイトにEDISONのソースも入っていたので、Spiもi2cも考えられるすべてのことを試して見てもまともにつながりません。特にi2cは、Edison内部のソフトPullupを最大限活用してもダメでした。 調べてみるとEdisonとLeputonは、i2c接続が不可能の模様です。つまり制御ができない。
 このセンサーのEdison接続に関して2015年の1年間Webの書き込みがないので、これは絶対無理の模様。前の記事で紹介したセンサーもそうでしたが(かえって結構すごいことができた)Edisonのi2cは、癖があるようです。一般的なセンサーは問題ないけど、特殊なやつだとちょっと.....。
 実験しただけでも2日かかったので、Edisonでは断念。SPI接続に関しては今後挑戦して行こうと思ってます。

ところで、本題のESP-WROOM-02開発ボード

         f:id:TAKEsan:20160216224046j:plain:w300
これは本格的にいい!!理想的です。
 今まで私が考えてたこの種のボードの理想的最小限構成。つまりSPI、i2c、最低限のデジタルピン、それにArduino Ideで動く。そしてWiFi。そこそこのスピードと容量。ちっちゃい。安い。全部網羅しています。さらに付け加えるなら安定してる。下手に私の生理的に嫌いなBluetoothまで手を出してないところがさらに良い!!。あの手強いMLX90621でさえ、試してみたらサクサク動く(これが動けば感覚的に他のi2cセンサーは全く問題なし)、Arduino Ideの利用方法はスイッチサイエンスさんが紹介してますので、このまま設定するだけ。とても簡単です。
 コンパイルと転送に少し時間がかかりますが、許容範囲。その昔興味本位で試したCPM のBDS C(なんと30年前の代物。その頃確かコンパイルスピードが早いともてはやされていた)の体感したスピードより早い。.....比べる時代がおかしいか。
 ダメ元と思ってこの「ESP-WROOM-02開発ボード」でFLIR LEPTON に挑戦してみました。メーカーのソースはこのページ-->https://github.com/groupgets/LeptonModule
 まず今回のセンサーのArduino成功例を探して実行。i2cは問題なしで繋がるが、案の定肝心のSPI画像配信信号の読み取りは全くダメ。
 Edisonと同じです。SS信号(hight,low)の高速化とクロックスピードが鍵となるようですけど、ArduinoのSystem変数を使ってるので、Edisonと同じようにESP-WROOM-02でもそのままコンパイルできません。仕方がないのでSSピンを強制的にOn Offすることにしましたが、これでも全然ダメ。でも確かに読み込んでいる挙動があります。
 Arduino標準SPIライブラリでこのセンサーを操作するのは無理と判断したので、SPIに関する外部ライブラリーを探してみました。で、発見。おまけにESP-WROOM-02Arduinoライブラリまで入っていました。
https://github.com/MetalPhreak/ESP8266_SPI_Driver
 最初はこれを使っても全く動かない。このライブラリのSPIの選択では、HSPI,SPIの指定があるので、ESP-WROOMのマニュアルに書いてあるHSPIにしてみると、やったー。それらしいのを表示。SPI読み込みはArduinoのSPIライブラリと違って1行で済むので、ソースの見た目も納得。
 ただFLIR LEPTONから出力された最後の画像の12ラインをどうしても読み取れない。これはスピードとタイミングの問題という勘が働いて、ライブラリのコマンドを調べてみたら、speedコマンドがあることを発見。多分クロックスピードの調整用と思い、試しに spi_clock(HSPI,1.7,2) にすると なんと全てのラインを読み込んでしまいました。
 ただし、要所に入れたDerayの長さによって通信が不安定になることもわかりました。このライブラリと今回のセンサーを使う限りスピードの調整が必要です。SPIに関して全く無知なので、反復学習するしかありません。学習結果のテストプログラムは次のようになりました。温度の計算方法は、この方の 0.ht - Simple thermal camera using a FLIR Lepton modulehttp://0.ht/thermal
プログラムを使わせていただきました。
 今回の記事を応用して、最終的にこうなりました。自分的にはかなり満足です。     takesan.hatenablog.com

FLIR LEPTON から読み取ったデータを行単位でコンソールへ16進表示
extern "C"{            //<------spi.h,spi.c,spi_register.hは Arduino/liblaly の中にフォルダを作って入れておく
  #include <spi.h>
  #include <spi_register.h>
}
#include <Wire.h>

byte x = 0;
#define ADDRESS  (0x2A)
#define AGC (0x01)
#define SYS (0x02)
#define VID (0x03)
#define OEM (0x08)

#define GET (0x00)
#define SET (0x01)
#define RUN (0x02)

#define VOSPI_FRAME_SIZE (164)
#define COMMANDID_REG (0x04)
#define DATALEN_REG (0x06)
#define DATA0 (0x08)
#define IMAGE_SIZE (800)
byte image[IMAGE_SIZE];
int image_index;
uint16_t lepton_frame_packet[VOSPI_FRAME_SIZE];
  unsigned int min = 65536;
  unsigned int max = 0;
  unsigned int pixel;
  float diff;

void setup()
{
 //pinMode(15, OUTPUT);
  Wire.begin();
  Serial.begin(115200);
  spi_init(HSPI);         // <-----------------FSPI or SPI
  spi_clock(HSPI,1.7,2);  // <-----------------FSPI or SPI 1~2,2
  Serial.println("setup complete");
}

static uint16_t lepton_image[63][82]; 

void read_lepton_frame(void)
{
  int i;
  uint16_t data = 0x000f;
  delay(50);             // <-----------------重要!! 
  while (data & 0x000f == 0x000f)
  {
    data = spi_rx16(HSPI);
    lepton_image[0][0] =data;
     for (i = 0; i < ((VOSPI_FRAME_SIZE - 2) / 2); i++)
    {
      lepton_image[0][i+1]=spi_rx16(HSPI);  
    }  
  }

  for (int frame_number = 1; frame_number < 60; frame_number++){ 
    for (int i = 0; i < 82; i++)
    {
        lepton_image[frame_number][i] = spi_rx16(HSPI); //<----SP_I read
     }
   }
  }
  
void lepton_sync(void)
{
  int i;
  uint16_t data = 0x000f;
  uint16_t aaa;
delay(10);
  while (data & 0x000f == 0x000f)
{
    data = spi_rx8(HSPI)<< 8;
    data |= spi_rx8(HSPI);
     for (i = 0; i < ((VOSPI_FRAME_SIZE - 2) / 2); i++)
    {
      spi_rx8(HSPI);
      spi_rx8(HSPI);
    }  
  }

}

void print_lepton_frame(void)
{
  int i;

  for (int frame_number = 0; frame_number < 60; frame_number++){ 
    for (i = 0; i < (VOSPI_FRAME_SIZE / 2); i++)
    {
        Serial.print(lepton_image[frame_number][i] ,HEX);
        Serial.print(",");
    }
        Serial.println(" ");
  }
        Serial.println(" "); 
}

void lepton_command(unsigned int moduleID, unsigned int commandID, unsigned int command)
{
  byte error;
  Wire.beginTransmission(ADDRESS);

  // Command Register is a 16-bit register located at Register Address 0x0004
  Wire.write(0x00);
  Wire.write(0x04);

  if (moduleID == 0x08) //OEM module ID
  {
    Wire.write(0x48);
  }
  else
  {
    Wire.write(moduleID & 0x0f);
  }
  Wire.write( ((commandID << 2 ) & 0xfc) | (command & 0x3));

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

void set_reg(unsigned int reg)
{
  byte error;
  Wire.beginTransmission(ADDRESS); // transmit to device #4
  Wire.write(reg >> 8 & 0xff);
  Wire.write(reg & 0xff);            // sends one byte

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

int read_reg(unsigned int reg)
{
  int reading = 0;
  set_reg(reg);
  Wire.requestFrom(ADDRESS, 2);
  reading = Wire.read();  // receive high byte (overwrites previous reading)
  //Serial.println(reading);
  reading = reading << 8;    // shift high byte to be high 8 bits
  reading |= Wire.read(); // receive low byte as lower 8 bits
  return reading;
}

int read_data()
{
  int i;
  int data;
  int payload_length;

  while (read_reg(0x2) & 0x01)
  {
    Serial.println("busy");
  }

  payload_length = read_reg(0x6);

  Wire.requestFrom(ADDRESS, payload_length);
  //set_reg(0x08);
  for (i = 0; i < (payload_length / 2); i++)
  {
    data = Wire.read() << 8;
    data |= Wire.read();
  }
  return data;
}

void loop()
{
  int i,p;
  int reading = 0;
  String debugString;
  unsigned int col;
  float value_min,value_max;
  
  Serial.println("Start!!");
  lepton_command(SYS, 0x19>>2 ,GET);
  read_data();
    while(1){
      min = 65536;
      max = 0;
      
     read_lepton_frame();
     
     for (int frame_number = 0; frame_number < 60; frame_number++){     
       for (i = 0; i < 82; i++)
       {
          p=lepton_image[frame_number][i] ;
          if(i >= 2){ 
              if(p < min) min = p;
              if(p > max) max = p;
          }
          if(i==0) lepton_image[frame_number][i]=(p & 0x00ff);
       }
     }     
     diff = max - min;
     diff = diff / 256.0f;
     if(diff < 0.56f) diff = 0.56f; // 0.66

        lepton_command(SYS, 0x10 >> 2 , GET); //センサー温度i2cから取得
        float aux=read_data() ;

        float fpatemp = aux/ 100.0f;
        float fpatemp_f = fpatemp * 1.8f - 459.67f;

       Serial.println(fpatemp-273.15);
        value_min = ((0.05872 * (float)min - 472.22999f + fpatemp_f));
        value_min= (value_min - 32.0f) / 1.8f;    
        value_max = ((0.05872 * (float)max - 472.22999f + fpatemp_f));
        value_max = (value_max - 32.0f) / 1.8f;

       Serial.println(value_min);
       Serial.println(value_max);
       
    for (int frame_number = 0; frame_number < 60; frame_number++){ 
       for (int i = 2; i < (VOSPI_FRAME_SIZE / 2); i++)
       {
            col=lepton_image[frame_number][i] ;
            col = col - min;
            col = col / diff;
            if(col <= 0) col = 0;
            if(col > 255) col = 255;
            lepton_image[frame_number][i] =col;
       }
    }
      print_lepton_frame();
      delay(100);           // <-----------------重要 100以上
   }  
}

製造元で公開しているArduinoソースコードの内容とは肝心のSPI信号読み取り部分(read_lepton_frame)がだいぶ違っています。当方勘と実験だけでプログラムを動かしてますので、この筋の専門家の方々はもっとスマートにできるんでしょうね。あーうらやましい。あと30歳若かったらなんて思ったりしますが、私の性格では遡ったって多分同じでしょうね。
 で、これを実行させると、ちゃんとデータをすべて受け取っているみたいです。だって文字表示だけなのに、赤外線をで読み取った画像の移動がわかります。
 ここまで来たら、もうこっちのもの。まだメモリを半分ぐらいしか使ってないみたいなので、本格的な赤外線画像をWEB配信するなり、手持ちの小型ディスプレイに表示するなり面白そうな実験ができそうです。
 そこでまずMacESP-WROOM-02の通信方法を検討。 https://www.mgo-tec.com
 この方は非常に魅力的なブログを書いています。よく読んでみると、どうやら今回のような比較的容量の大きなデータ送受信は難しい模様。さらにiPhoneであまり良い結果が出てないようなので今回は使わないことに。ただしこの方のアイディア・表現力・手軽さはピカイチです。今後の進展に期待してしまいます。
 じゃーどうする? せめてOSCが使えたらなーなんて探したらこんなのありました(前に別のArduino版OSCライブラリがまともに動かなかった)。しかもESP8266用のも入っています。
   OSC for ARDUINO-->https://github.com/CNMAT/OSC
 問題なくコンパイルできたので、MacのOpenframeworksのOSCサンプルをちょっと修正して通信を試したら、Good!。 これをiPhoneに載せ替えたら 超小型遠隔操作赤外線カメラができそう。
 OSCでは画像ファイルが送られるようなのですが、Openframeworksとこのライブラリでは仕様が違うので送れない。そもそも256色だけのデータをjpeg等にエンコードするだけでもメモリと実行時間が無駄にかかりそう。しょうがないので文字配列として1行80文字分のデータ60行を1画面として送ることにしました。(いろいろ試してみるとStringの場合最大で450文字ぐらいは送れるようなので5ライン分くらいはまとめて送信できる)最終的にでき上がったものは1秒あたり3フレーム程度です。このセンサーの利用方法を考えたら十分許容範囲だと思います。

さらに補間して詳細表示

 一応Macで作ったアプリとESP-WROOM-02からWiFi経由で送ってみたのが、冒頭の画像です。改めて感激!!。しかもeneloop電池で動いてるんですよ。満充電以降2ヶ月以上ほったらかしにしていた電源でも連続で4時間以上稼働します。
 気を良くしたので、画像を倍に補間してみます。今までの赤外線センサーとは違い、画素が多いので、ラグランジェの曲線補間などしなくても単純な前後左右の平均だけで充分と思い、Openframeworks側で倍に上げてみました。比べてみるとこんなに違います。
   f:id:TAKEsan:20160216214109p:plain
センサーのピクセル数のまま80X60ドット表示。Openframeworksでは1ピクセル5X5ドットの四角形として描画。表示がギザギザ。PIのExampleと同じです。Piと画像の雰囲気が違うのは、カラーマップを変えたから。
   f:id:TAKEsan:20160216214110p:plain
152X119に補間して半径1.5ドットの円として表示。全然違うでしょ。キュッと引き締まります。しかもMac側で処理しているのでスピードはほとんど変わらない!!
 256階調でこんな感じなのですから、このセンサーの本来の性能である8192階調で表示したらすんごいことになりそうです。ただ、どのようにして色で表現するのか......が、問題。         
        
                 倍に補間した画像。
※電源をコンピューターから取ると、かなり画像が乱れるので、バッテリーがオススメです。

次回は、実際のプログラムを紹介します。 iPhoneでも実行できそうなので、できたら一緒に紹介します。
takesan.hatenablog.com
Pi zeroでLEPTONの動画配信できました。スピードは倍以上!!
takesan.hatenablog.com

質問への対応

インストールに成功したライブラリは以下からアップできます。(SPI及びOSC)
id:TAKEsan の Driver.zip
ライブラリの保存場所は
f:id:TAKEsan:20170112234157p:plain
です。Macの場合  書類/Arduino/libraries に入れます。

  • Macでテストする場合のOpenFrameworksのソースをダウンロード可能にしておきました。解凍後apps/myApps へフォルダごと移動させビルドしてください。

id:TAKEsan の oscReceive-LEPTON.zip
WROOM02側のソースは次のページに書いてあるソースを参考にしてください。無線LANIPアドレスとパスワード、MacIPアドレスを自分の環境に合わせて修正することをお忘れなく。