SPRESENSEで画像認識1

6 min read読了の目安(約5600字

はじめに

Sonyの汎用マイコン基板のSpresenseを用いて色々やってます。このマイコンの布教活動のため使い方や開発中のものを掲載していこうと思います。

作るもの

今回はSpresenseを用いて3人の息子の顔を自動判別するデバイスを開発していきます。今回はカメラで画像を取得できるところまで進めていきます。

準備

必要な部材は以下です。

  • Spresense
  • Spresense拡張ボード
  • Spresense用カメラ
  • USBケーブル(microB)
  • uSDカード
    *spresense一式はスイッチサイエンスにて購入しました。

カメラの動作確認

一式を組み立てて、いざ動作確認。

ファイル > スケッチ例 > camera を選択し、タイムラプスカメラのサンプルコードをそのまま書き込んだところ、問題なく動作しました。

サンプルのソースコードの理解

簡単にサンプルコードを読み解きます。カメラが画像を取得するとコールバック関数CamCBが呼ばれます。CamCBには色々と書かれていますが、行っているのは取得画像データのフォーマット変換だけです。

void CamCB(CamImage img){
  /* Check the img instance is available or not. */
  if (img.isAvailable()){
      /* If you want RGB565 data, convert image data format to RGB565 */
      img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565);
      /* You can use image data directly by using getImgSize() and getImgBuff().
       * for displaying image to a display, etc. */
      Serial.print("Image data size = ");
      Serial.print(img.getImgSize(), DEC);
      Serial.print(" , ");
      Serial.print("buff addr = ");
      Serial.print((unsigned long)img.getImgBuff(), HEX);
      Serial.println("");
  }else{
      Serial.print("Failed to get video stream image\n");
  }
}

setup関数でシリアルとSDカードとカメラ設定を初期化しています。エラー時の対応のため色々と記載がてんこ盛りですが、初期化は以下だけでOK(なはず)です。

void setup(){
  Serial.begin(BAUDRATE);
  
  theSD.begin();
  
  theCamera.begin();
  theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT);
  theCamera.setStillPictureImageFormat(
     CAM_IMGSIZE_QUADVGA_H,
     CAM_IMGSIZE_QUADVGA_V,
     CAM_IMAGE_PIX_FMT_JPG);
}

loop関数でカメラ画像100枚取得するまで一定時間ごとに撮影しファイル保存しています。
一連の流れは以下の通りです。

  1. 1秒待つ
  2. 取得した画像枚数が100枚以下なら撮影する、101枚以上ならカメラを終了する
  3. 撮影した画像が正常ならファイル名を決定(ファイル名は"PICTXXX.JPG"、XXXは連番)
  4. 同一名称のファイルがSDカードにある場合はファイルを削除する
  5. uSDカードを書き込みモードにし画像データを書き込む
void loop(){
  sleep(1); /* wait for one second to take still picture. */
  if (take_picture_count < 100){
      CamImage img = theCamera.takePicture();
      if (img.isAvailable()){
          char filename[16] = {0};
          sprintf(filename, "PICT%03d.JPG", take_picture_count);    
          theSD.remove(filename);
          File myFile = theSD.open(filename, FILE_WRITE);
          myFile.write(img.getImgBuff(), img.getImgSize());
          myFile.close();
      }
      take_picture_count++;
  }else{
      theCamera.end();
  }
}

ボタン押下時のみタイムラプスカメラが動作するように変更

サンプルの場合は電源投入後に自動でタイムラプスカメラが起動します。これを少し変更して、ボタン押下時のみタイムラプスカメラが起動するようにしてみます。
方法は至って単純で、GPIOピンの状態を検出しLowの場合にLoop関数が実行されるようにします。ついでにタイムラプスカメラ起動中であることを表示するためにspresense上のLEDを点灯させます。

ハードウェアの準備

GPIOピン(D22)と3.3V電源の間にプッシュスイッチを設けます。念のため1kohm抵抗を直列に接続しておきます。プッシュスイッチを押すと3.3VがD22ピンに入力されます。
*正確には、直列に挿入した1kohm抵抗とspresenseのプルダウン抵抗で分圧された電圧がD22ピンに入力されることになります。

ソースの修正

LED0をアウトプット、D22ピンをプルダウンに設定とします。

int swPin = 22;
void setup() {
  pinMode(LED0, OUTPUT);
  pinMode(swPin, INPUT_PULLDOWN);
  //省略
}

LED0を点灯・消灯させるコードは以下となります。

//LED0を点灯
digitalWrite(LED0, HIGH);
//LED0を消灯
digitalWrite(LED0, LOW);

D22ピンを読み取り、High/Lowで処理内容を分岐させるコードは以下となります。

if(digitalRead(swPin)){
    //D22がHighの場合の処理
}else{
    //D22がLowの場合の処理
}

これらを組み合わせて、ボタン押下時のみタイムラプスカメラを起動させるコードを作成します。D22がHighの場合に、LED0を点灯させてカメラのキャプチャー処理を走らせれば良いだけです。簡単ですね。

void loop(){
    if(digitalRead(swPin)){
        //D22がHighの場合の処理
        digitalWrite(LED0, HIGH);
	
	//元々あったloop関数内の処理
	sleep(1);
	if (take_picture_count < 100) {
	//省略
          theCamera.end();
        }
    }else{
        //D22がLowの場合の処理
        digitalWrite(LED0, LOW);
    }
}

ボタン押下で1枚撮影されるように変更

次はさらに変更を加えて、ボタン押下で画像が1枚撮影されるようにしてみます。プッシュボタンの押下をトリガーとして割り込未処理をしてみます。Arduino IDEでは割り込みも簡単に実装できます。ついでに撮影したことを表示するためにLEDを一定時間点灯させます。

割り込みの実装

Arduino IDEでは割り込みは非常に簡単で、setup関数内に以下を記入すればOKです。

attachInterrupt(pin, void (*isr)(void), mode);

ここで各引数は以下の通りです。
pin: 割り込みのトリガーとなるピン
isr:割り込み発生時に実行する関数
mode:以下からトリガーを選択

  • LOW: ピンの状態がLOWのとき
  • CHANGE: ピンの状態が変化したとき
  • RISING: ピンの状態がLOWからHIGHに変わったとき
  • FALLING: ピンの状態がHIGHからLOWに変わったとき
  • HIGH: ピンの状態がHIGHのとき

今回はプッシュスイッチを押下してD22ピンがLOWからHIGHになった場合にcaptureImage()関数を実行するようします。
従って、下記の記述になっていれば割り込みが実行されます。

void setup(){
    //省略
    attachInterrupt(swPin, captureImage, RISING);
    //省略
}

あとはcaptureImage内に処理を書けばよいのですが、割り込み関数内では使用可能な関数に制約があるようでそのままではキャプチャができませんでした。今回は手抜きではありますが、グローバル変数capEnを設けておき、割り込み時にcapEnをHIGHとし、capEnの状態に応じてloop関数内で処理内容を変更することで対応しました。

void loop() {
  if (capEn) {
    digitalWrite(LED0, HIGH);
    Serial.println("call takePicture()");
    CamImage img = theCamera.takePicture();

    if (img.isAvailable())
    {
      char filename[16] = {0};
      sprintf(filename, "PICT%03d.JPG", take_picture_count);

      Serial.print("Save taken picture as ");
      Serial.print(filename);
      Serial.println("");

      theSD.remove(filename);
      File myFile = theSD.open(filename, FILE_WRITE);
      myFile.write(img.getImgBuff(), img.getImgSize());
      myFile.close();
      take_picture_count ++;
    }
    
    sleep(0.2);
    digitalWrite(LED0, LOW);
    capEn = LOW;
  }
}

void captureImage() {
  capEn = HIGH;
}

動作は以下の通りです。

  1. プッシュボタンを押下するとcapEnがHIGHとなりloop内の処理が走る
  2. LED0が点灯する
  3. 撮像・画像ファイル保存
  4. 0.2秒後にLED0が消灯する
  5. capEnをLOWとなる

結果的に割り込みを使う意味が無くなってしまいましたが、割り込みの使い方の勉強にはなりました。ヨカッタヨカッタ。

今回は以上です。次回はSonyのNNC(Neural Network Console)を用いて機械学習に挑戦します。