ESP32 CAMを高画質GoogleDrive対応監視カメラにする.サンプルプログラムを修正して高画質対応

未分類

はじめに

 ESP32-CAMというカメラとWifiがついて700円程度の安いマイコンを用いて監視カメラを作る。画像の保存先はGoogleDriveにする。https://github.com/gsampallo/esp32cam-gdriveのGitHubレポジトリの内容を参考にしたが、カメラの性能を最大限に活かすためにプログラムに改良を加えた。SVGA(800×600) ー> esp32-camera FRAMESIZE_UXGA (1600×1200) おおよそ4倍の画素数に対応した。

esp32-camera

画素数一覧

以前

 ESP32 CAM(https://ja.aliexpress.com/item/1005001636562460.html)を購入し、自分のメインマシンのWebサーバーに画像をPOSTするものを1年前に作った。30秒に一回程度の保存だったのだが、1日あたり2GBくらいずつ増える上、自分のパソコンの電源が入っていないといけなかった。もし泥棒が入ってもパソコンを盗まれたら意味がなくなってしまうし、頻繁な書き込みはハードディスクの寿命を縮めそうだったので運用をすぐに辞めてしまった。

なぜGoogleDriveなのか

 私はあまりお金がないので無料で使えるサーバーが良い。また、1ヶ月分の画像すなわち60GBほどのストレージが欲しい。実家サーバーは回線が最大10Mbpsの悪名高きJ:COM回線なので下宿先からの通信に向かず除外した。Xserverの無料サーバーはディスク容量が足りない。GoogleDriveは無料の中では最も多い15GB使えるので妥協してそうしたが、なんと! 大学のアカウントの容量制限がないではないか!(追記:大学全体で100TBまでになったのでやめた)無限のストレージが無料で使える(大学在籍中は)となるとGoogleDriveこそ最適解と分かった。

ハードウェア

ESP32-CAM(https://ja.aliexpress.com/item/1005001636562460.html)
ACアダプター(そこらへんに転がってたもの)
ケーブル(microUSB to 2pin)

マイコンの設定

ソフトウェア

GoogleAppScript(GoogleDriveにファイルをアップロードするWebAPIを作る事ができる)

GASコードではドライブの”監視カメラ”フォルダに1日毎のフォルダを作って送信されてきた画像を保存するようにしている。

esp32-googledrive-cam.ino (画像を送信する)

この他にGitHubからダウンロードしたBase64.cppとBase64.hが必要https://github.com/gsampallo/esp32cam-gdrive ←gsampallo氏が作ったスケッチ

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "Base64.h"

#include "esp_camera.h"

const char* ssid     = "SSID";   //your network SSID
const char* password = "PASS";   //your network password
const char* myDomain = "script.google.com";
String myScript = "/macros............/exec";    //Replace with your own url
String myFilename = "filename=ESP32-CAM.jpg";
String mimeType = "&mimetype=image/jpeg";
String myImage = "&data=";

int waitingTime = 30000; //Wait 30 seconds to google response.
int imageFile_length=0;//base64eddatasize
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22
sensor_t *sensor = NULL;
void setup()
{
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  
  Serial.begin(115200);
  delay(10);
  
  WiFi.mode(WIFI_STA);

  Serial.println("");
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);  

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println("");
  Serial.println("STAIP address: ");
  Serial.println(WiFi.localIP());
  delay(200);
  Serial.println("");

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;  // UXGA|SXGA|XGA|SVGA|VGA|CIF|QVGA|HQVGA|QQVGA
  config.jpeg_quality = 10;
  config.fb_count = 1;
  
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    delay(1000);
    ESP.restart();
  }
  sensor = esp_camera_sensor_get();
  sensor->set_exposure_ctrl(sensor, 1);
  sensor->set_aec2(sensor, 1);
}

boolean enviar = true;

void loop() {
  //if(enviar) {
    saveCapturedImage();
    enviar = false;
    delay(30000);
  //}
}

void saveCapturedImage() {
  Serial.println("Connect to " + String(myDomain));
  WiFiClientSecure client;
  client.setInsecure();
  if (client.connect(myDomain, 443) ) {
    Serial.println("Connection successful");
    
    camera_fb_t * fb = NULL;
    fb = esp_camera_fb_get();  
    if(!fb) {
      Serial.println("Camera capture failed");
      delay(1000);
      ESP.restart();
      return;
    }
    imageFile_length=0;
    char *input = (char *)fb->buf;
    char output[base64_enc_len(3)];
    String imageFile = "";
    for (int i=0;i<fb->len;i++) {
      base64_encode(output, (input++), 3);
      if (i%3==0){
        //imageFile += urlencode(String(output));
        imageFile_length+=urlencode(String(output)).length();//calc size only
      }
    }
    Serial.println(imageFile_length);
    String Data = myFilename+mimeType+myImage;
    
    
    
    Serial.println("Send a captured image to Google Drive.");
    
    client.println("POST " + myScript + " HTTP/1.1");
    client.println("Host: " + String(myDomain));
    client.println("Content-Length: " + String(Data.length()+imageFile_length));
    client.println("Content-Type: application/x-www-form-urlencoded");
    client.println();
    
    client.print(Data);
    int Index;

    
    //for (Index = 0; Index < imageFile.length(); Index = Index+1000) {
    //  client.print(imageFile.substring(Index, Index+1000));
    //}
    String tmp_data="";
    input = (char *)fb->buf;
    for (int i=0;i<fb->len;i++) {
      base64_encode(output, (input++), 3);
      if (i%3==0){
        //imageFile += urlencode(String(output));
        tmp_data+=(urlencode(String(output)));
        //imageFile_length+=urlencode(String(output)).length();
      }
      if (tmp_data.length()>=1000){
        client.print(tmp_data);
        tmp_data="";
      }
    }
    client.print(tmp_data);

    esp_camera_fb_return(fb);
    Serial.println("Waiting for response.");
    long int StartTime=millis();
    while (!client.available()) {
      Serial.print(".");
      delay(100);
      if ((StartTime+waitingTime) < millis()) {
        Serial.println();
        Serial.println("No response.");
        //If you have no response, maybe need a greater value of waitingTime
        break;
      }
    }
    Serial.println();   
    while (client.available()) {
      Serial.print(char(client.read()));
    }  
  } else {         
    Serial.println("Connected to " + String(myDomain) + " failed.");
  }
  client.stop();
}

//https://github.com/zenmanenergy/ESP8266-Arduino-Examples/
String urlencode(String str)
{
    String encodedString="";
    char c;
    char code0;
    char code1;
    char code2;
    for (int i =0; i < str.length(); i++){
      c=str.charAt(i);
      if (c == ' '){
        encodedString+= '+';
      } else if (isalnum(c)){
        encodedString+=c;
      } else{
        code1=(c & 0xf)+'0';
        if ((c & 0xf) >9){
            code1=(c & 0xf) - 10 + 'A';
        }
        c=(c>>4)&0xf;
        code0=c+'0';
        if (c > 9){
            code0=c - 10 + 'A';
        }
        code2='\0';
        encodedString+='%';
        encodedString+=code0;
        encodedString+=code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}

高解像度に対応するために工夫した点

コードは上記Githubとほとんど同じだが、いくつか工夫をすることでメモリ消費量を抑え、結果としてカメラの対応しているフル解像度(1600×1200)で送信することが可能になった。画像をPOSTで送信する際にBASE64エンコードに変換して送信する必要があるのだが、元のプログラムではカメラメモリの中身をすべてBase64に変換し変数に格納していたのに対し(コメントアウトした //imageFile += urlencode(String(output));)、カメラのRAMから読み取った中身を小刻みにbase64にして送信することでメモリ消費を抑えている。これにより画像サイズが増えてもメモリ消費は変わらなくなる。ただし、POSTでデータを送信する際はご存じの通り、まずファイルサイズをヘッダーに含めなくてはいけない。そのため、はじめに画像をBase64にした場合ファイルサイズがいくつになるかの計算を行い、ヘッダーのContent-Lengthに指定している。

ESP32に書き込み。動くことを確認できた。電源投入後はじめの1枚はなぜか緑色になるが、それ以降は正常に動作している。Wifi接続時に一瞬電流を多く消費するので電源は余裕をもって2A対応にするか、数千ufのコンデンサを電源部分につけることで安定する。

終わりに

ACアダプターを含めてもかなり安価(1機1000円弱)にクラウド対応の監視カメラシステムを作れて満足している。

コメント

  1. test 管理人 より:

    テスト

タイトルとURLをコピーしました