MZ Platformとフィジカルコンピューティング

Arduinoカメラの作成

Arduinoを用いてインターバル撮影カメラを作成したので、 その作成方法と利用例について説明します。 動機としては、 数年前から手がけているUSBカメラを用いた映像記録 がインターバル撮影にあたると気付き、 さらにマラソン中にインターバル撮影してみたいと考えたからです。

Arduinoの説明と利用方法については、 こちらをご覧ください。

作成したカメラを車に載せて撮影した例と、マラソンで撮影した例を動画にしてみました。 カメラの概要は構成のところで説明しています。動画については利用例のところで説明します。

作成したArduinoカメラの構成

マラソン中に使うことを想定してなるべく軽くしたいと考えて、下記の構成としました。

  • Arduino FIO
  • リチウムイオンポリマー電池 3.7V 850mAh
  • LinkSpriteシリアル接続JPEGカラーカメラ
  • MicroSD用コネクタ変換基板
  • MicroSDカード 2GB
残念ながらカメラモジュールは赤外線LED付きしかすぐには手に入らなかったので、 そちらを利用しました。日中に外を撮るには不向きだと予想できましたが、 時間がなかったので妥協しました。

下図左が組み立てた状態で、右がArduino FIOと基板を分離した状態です。

基本的な回路は下図のようになります。ご覧の通り、この構成だとほぼ接続するだけです。 上の写真ではこの回路に加えて、電源スイッチとD13に接続するLEDと抵抗が追加されています。

結果として、単独で動作させた場合には約200gの重さとなり、 解像度320x240の画像を1分間に5枚程度(約12秒間隔で)撮影することができ、 フルに充電した状態で4時間以上連続して動作することがわかりました。 1枚の画像は13KB程度で、1時間で300枚として4MB程度となり、 数時間の撮影でもSDカードの容量は問題ありません。

この構成だと、解像度が低くて撮影間隔も長いので、 マラソン中の撮影用としては少々性能不足でしたが、 軽さと動作時間は充分でした。

Arduino用プログラミング

作成したArduinoカメラ用のスケッチ(ソースコード)を公開します。 使用したArduino IDEのバージョンは1.0.5です。 SDカードの扱いはスケッチ例とリファレンスを参照して作成し、 JPEGカメラの扱いは製造元の情報とすでに同様のスケッチを公開している方々の情報を参考に作成しました。

Arduino FIOは電圧が3.3Vで、 使用したJPEGカメラのデフォルトのボーレートでは正常に動作しないようです。 そのため、起動後にボーレートを19200に落としています。 また、撮影間隔の短縮のためにJPEGカメラの解像度をデフォルトから320x240に落としましたが、 解像度の変更は一度実行すると記憶されるようなので、スケッチ中ではコメントアウトしています。

いろいろ試していたら、 SDカードの同一のフォルダ内に作成できるファイル数に上限(512)があることに気付いたので、 ファイル数が500に達したらフォルダを新たに作成するようにしています。


/*
 * Arduino FIO + SD card + JPEG camera
 */
#include <SD.h>
#include <SoftwareSerial.h>

// SD card DI/DO/CLK/CS to pin 11/12/13/10
SoftwareSerial camPort(2,3);  // camera rx/tx to pin 3/2

#define BUF_SIZE 56 // 32
uint8_t KH=0x00,KL=0x38; // KL=0x20;
int readDelay = 5;
byte incomingbyte;
int address=0x0000;

int fileIndex = 0;
int fileMax = 500; // less than 512
int loopContinue = 0;
int sendImage = 0; //1;

void setup() {
  Serial.begin(57600);
  
  serialPrint("Initializing SD card...");
  if (!SD.begin(10)) { // OK for Arduino Pro Mini/FIO
    serialPrintln("failed!");
    return;
  }
  serialPrintln("done.");

  camPort.begin(38400); delay(25); // default baudrate
  
  serialPrint("Resetting camera...");
  cmdReset(); delay(3000); consumeCamPort();

  serialPrint("Changing baudrate...");
  cmdBaudRate19200();
  camPort.end();

  serialPrint("Reconnecting camera...");
  camPort.begin(19200); delay(25); consumeCamPort();
  serialPrintln("done.");
  
  loopContinue = 1;
  /*
  // for the first time (from here)
  cameraResolution();
  loopContinue = -1;
  // for the first time (until here)
   */
}

void loop() {
  char dirName[6], fileName[20];
  if (loopContinue>0) {
    do { // fixing file name
      sprintf(dirName,"cam%03d",fileIndex/fileMax);
      if (SD.exists(dirName)==false) {
        SD.mkdir(dirName); 
        serialPrint("Making directory "); serialPrintln(dirName);
      }
      sprintf(fileName,"cam%03d/img%05d.jpg",fileIndex/fileMax,fileIndex++);
    } while (SD.exists(fileName));
    
    File sdFile = SD.open(fileName, FILE_WRITE);
    if (sdFile) { // suceeded to open file
      serialPrint("Succeeded to open "); serialPrintln(fileName);
    
      serialPrint("Taking a photo...");
      cmdTakePhoto();
      byte returnCode[10];
      int returnCount = readCamPort(returnCode);
      if (returnCount!=5||returnCode[0]!=0x76||returnCode[1]!=0x00||
          returnCode[2]!=0x36||returnCode[3]!=0x00||returnCode[4]!=0x00) {
        serialPrintln(" ERROR(incorrect return code)");
        loopContinue = -1;
        return;
      }
      serialPrintln("done.");

      byte buf[BUF_SIZE];
      serialPrint("Reading and writing data..");
      int i=0, endFlag=0;
      while(!endFlag) {
        cmdReadData();
        if (i%10==0) { serialPrint("."); }
        int j=0, k=0, count=0;
        delay(readDelay); // too small readDelay seems to cause bad jpeg data
        while(camPort.available()>0) {
          incomingbyte=camPort.read();
          k++;
          if((k>5)&&(j<BUF_SIZE)&&(!endFlag)) {
            buf[j]=incomingbyte;
            if(j>1&&(buf[j-1]==0xFF)&&(buf[j]==0xD9)) // check if jpeg data is over
              endFlag=1;
            j++; count++;
          }
        }
        for(j=0;j<count;j++){
          sdFile.write((uint8_t)buf[j]); // write jpeg data
        }
        if (sendImage) Serial.write(buf,count);
        i++;
      }
      sdFile.close();
      serialPrintln("done.");

      serialPrint("Stop taking a photo...");
      cmdStopPhoto();
      returnCount = readCamPort(returnCode);
      if (returnCount!=5||returnCode[0]!=0x76||returnCode[1]!=0x00||
          returnCode[2]!=0x36||returnCode[3]!=0x00||returnCode[4]!=0x00) {
        serialPrintln(" ERROR(incorrect return code)");
        loopContinue = -1;
        return;
      }
      serialPrintln("done.");
    } else { // failed to open file
      serialPrint("Error: failed to open "); serialPrintln(fileName);
      loopContinue = -1;
    }
    
    delay(50);
  }
}

// send byte
void cmd(byte value) {
  camPort.write((uint8_t)value);
}

// reset command
void cmdReset() {
  cmd(0x56); cmd(0x00); cmd(0x26); cmd(0x00);
}

// take picture command
void cmdTakePhoto() {
  address=0x0000;
  cmd(0x56); cmd(0x00); cmd(0x36); cmd(0x01); cmd(0x00);
  delay(25);
}

// read data command
void cmdReadData() {
  uint8_t MH,ML;
  MH=address/0x100; ML=address%0x100;
  cmd(0x56); cmd(0x00); cmd(0x32); cmd(0x0c); cmd(0x00); cmd(0x0a); cmd(0x00); cmd(0x00);
  cmd(MH); cmd(ML); cmd(0x00); cmd(0x00); cmd(KH); cmd(KL); cmd(0x00); cmd(0x0a);
  address+=(BUF_SIZE);
}

// stop taking photo command
void cmdStopPhoto() {
  cmd(0x56); cmd(0x00); cmd(0x36); cmd(0x01); cmd(0x03);
  delay(25);
}

// change resolution command
void cmdImageSize320x240() {
  cmd(0x56); cmd(0x00); cmd(0x31); cmd(0x05); cmd(0x04); cmd(0x01); cmd(0x00);
  cmd(0x19); cmd(0x11);
  delay(25);
}

// change baudrate command
void cmdBaudRate19200() {
  cmd(0x56); cmd(0x00); cmd(0x24); cmd(0x03); cmd(0x01); cmd(0x56); cmd(0xE4);
  delay(25);
}

// change resolution
void cameraResolution() {
  serialPrint("Changing resolution...");
  cmdImageSize320x240();
  while(camPort.available()>0) {
    incomingbyte=camPort.read();
    //if(incomingbyte<0x10) Serial.print("0");
    //Serial.print(incomingbyte,HEX); Serial.print(" ");
  }   
  serialPrintln("done");
}

// read camera port and discard
void consumeCamPort() {
  while(camPort.available()>0) {
    incomingbyte=camPort.read();
  }   
}

// read camera port and store
int readCamPort(byte * returnCode) {
  int returnIdx=0;
  while(camPort.available()>0) {
    incomingbyte=camPort.read();
    //if(incomingbyte<0x10) Serial.print("0");
    //Serial.print(incomingbyte,HEX); Serial.print(" ");
    returnCode[returnIdx++]=incomingbyte;
  }
  return returnIdx;
}

void serialPrint(String text) {
  if (!sendImage)
    Serial.print(text);
}

void serialPrintln (String text) {
  if (!sendImage)
    Serial.println(text);
}


スケッチを書き込むときには、USB-シリアル変換アダプタを使うのが簡単です。 これがあれば、書き込んだあとにシリアル接続経由でメッセージを出力させて動作を確認できます。

MZアプリの作成

ここで紹介しているArduinoカメラは、MZアプリと関係なく単体で動作します。 ですが、 カメラが出力する動作確認用のメッセージや画像をシリアル接続経由で受信するアプリがあれば、 詳細な動作確認ができる上に新たな使い道につながるかもしれないので、作成してみました。

シリアル接続は、前述のUSB-シリアル変換アダプタ経由の有線のほか、 FIO標準のソケットにXBeeを挿して無線でも実現できます。 XBeeを用いたシリアル通信の無線化については、 こちらをご覧ください。

前掲のスケッチでは、下図のようなメッセージがシリアル接続で得られます。 これはシリアル通信コンポーネントの標準機能で実現できます。

下図はXBeeによる無線シリアル通信で画像を取得して表示している様子です。 こちらは配布版のMZ標準機能では実現できなかったので、 シリアル通信コンポーネントと画像ファイル入力に機能追加をして実現しました。 処理内容としては、 シリアル通信で受信したJPEGデータのバイト列をまとめて配列に変換し、 バイト配列から画像を作成しています。

前掲のスケッチでは、sendImage=1とするとこのMZアプリ用の画像データを送信します。 MZアプリ側は画像受信のチェックを入れてから接続すると、 バイト配列から画像を作成して表示します。

画像を表示する機能の使い道としては、 SDカードを取り出さなくても撮影範囲を確認できることが挙げられます。 ただし、1枚撮影するのに12秒程度かかるため、 カメラを動かしてから実際に確認するまで最長24秒程度かかります。 なので、一応確認できるという感じで、微調整は難しかったです。 また、詳細には確認していませんが、XBeeを使うと電池の減りは速くなるはずです。

利用例

運転中の撮影

作成したArduinoカメラで、自動車を運転中に撮影した例を示します。 下図が設置状況です。

下図の左列がArduinoカメラで撮影した画像で、 右列が参考として近い場所で市販のデジタルカメラで撮影した画像です。 カメラの画質に加えて赤外線の影響もあると思いますが、かなり色が異なります。

前掲の動画は、40分程度で撮影した画像を5fpsで映像として結合しています。 USBカメラを用いた映像記録 で使用したMZアプリを利用しました。 おおよそ1分が1秒程度になっています。 1枚の撮影に12秒程度かかるので、速度が出ているときは風景の連続性がありません。 そのため、あまりフレームレートを上げることができませんでした。 それでも、車だと姿勢が安定しているのでまだマシです。

マラソン中の撮影

続いて、作成したArduinoカメラでマラソン中に撮影した例を示します。 下図がマラソン用の帽子に取り付けた様子です。防滴・防塵のために透明な袋で包みました。 カメラ側は帽子のつばに針金で固定し、 Arduino側は帽子に安全ピンで取り付けた巾着袋に入れて口を締めます。 取り付けた後で電源を入れて動作を確認できるように、 スイッチとLEDが巾着袋の口から覗くようにしました。

下図は両方ともArduinoカメラで撮影した画像です。 比較用の画像がありませんが、運転中の例に比べて色が良くなっている印象です。 カメラモジュールの側で自動的に調整が行われるので、原因についてはよくわかりません。 赤外線の影響としては、紅葉していない植物の葉が白く見えるのと、 黒いウェアが紫に見えるのがわかります。

前掲の動画は、2013年11月24日に開催されたつくばマラソンで撮影した画像を、 2fpsで映像として結合しています。これも USBカメラを用いた映像記録 で使用したMZアプリを利用しました。 1枚の撮影に12秒程度かかるので、画像間の連続性はかなり低いです。 しかも頭に付けているので、カメラの姿勢も状況によって変わってしまいます。 このため、車の例よりもさらにフレームレートを落とさないと、観ていて酔ってしまう感じでした。 撮影は成功したのでとりあえず満足ですが、いろいろ改善したい感じがします。

作成日 2013-11-26

最終更新日 2013-11-26