📷

M5Cameraで超安価なタイムラプスカメラを作る

2021/10/12に公開

※2019年12月18日に公開された記事の移植です。情報が古い箇所があるかもしれません。

はじめに

みなさん,スマフォでタイムラプスを撮ったことありますか?
撮っているときにポケットに手を突っ込んで「あ,撮ってるんだった...」ってなって寂しくなりますよね

そこで超安価にタイムラプスを撮れるカメラを作ってみました.

使ったもの

M5Camera魚眼レンズ

スイッチサイエンスさんでも売っています.
https://www.switch-science.com/catalog/5691/

438a37d5-0d22-492f-849a-b3be7e8e87e5.jpg

2,150円!!
(私は増税前だったので2,120円でした)

魚眼レンズじゃなくてもいいならば1,815円!!

以上!!!!

使ったその他のもの

100均のモバイルバッテリー
https://japanese.engadget.com/2019/06/07/100-500-arduino/

小さいカメラスタンドとスマフォを挟むやつ
https://amazon.co.jp/dp/B07Y71VF93
今回使ったものは上記のものとは違うものです(適当に家にあったもの)

M5Cameraで遊ぶ

M5Cameraは電源をつなげると「M5FishEyeCam」というwifiが飛ぶので,それに接続
接続したスマフォとかPCのブラウザで「192.168.4.1」へアクセス

とりあえず映像が写っちゃう
もうこれだけでwebカメラですね

これを利用してもいいけども結局撮影している間インターネットに繋げられない

M5Cameraにプログラムを書き込む

自分のwifiルーターに接続

https://www.mgo-tec.com/blog-entry-m5camera-arduino.html
こちらのサイトを参考にしながらサンプルコードを編集していきます

サンプルが表示されない場合
https://qiita.com/n0bisuke/items/354dce451d26cb5e196a
それでも動かない場合,原因
https://qiita.com/tkyko13/items/e375db417e6b6fbde84d
出荷時に戻す(?)
https://docs.m5stack.com/#/en/unit/m5camera_f?id=easyloader

mDNSの利用

このやり方はシリアルコンソールでipアドレスを見て接続しますが,今回はモバイルバッテリーでカメラを放置しておきたいので,ipアドレスではなくmDNSを利用して名前でアクセスできるようにします
「mDNS追加」と書いてあるあたりが自分が追加したものです

#include "esp_camera.h"
#include <WiFi.h>
#include <ESPmDNS.h> //mDNS追加

//
// WARNING!!! Make sure that you have either selected ESP32 Wrover Module,
//            or another board which has PSRAM enabled
//
 
// Select camera model
//#define CAMERA_MODEL_WROVER_KIT //ここを変更
//#define CAMERA_MODEL_ESP_EYE
#define CAMERA_MODEL_M5STACK_PSRAM //ここを変更
//#define CAMERA_MODEL_M5STACK_WIDE
//#define CAMERA_MODEL_AI_THINKER
 
#include "camera_pins.h"
 
const char* ssid = "xxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxx"; //ご自分のルーターのパスワードに書き換えてください
const char* mdnsName = "m5camera_f"; //mDNS追加
 
void startCameraServer();
 
void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  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;
  //init with high specs to pre-allocate larger buffers
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
 
#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif
 
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
 
  sensor_t * s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1);//flip it back
    s->set_brightness(s, 1);//up the blightness just a bit
    s->set_saturation(s, -2);//lower the saturation
  }
  //drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_QVGA);
 
#if defined(CAMERA_MODEL_M5STACK_WIDE)
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
#endif
 
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  // ---ここから---
  if (!MDNS.begin(mdnsName)) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
      Serial.print(".");
    }
  }
  Serial.println("mDNS responder started");
  // ---ここまでmDNS追加---
 
  startCameraServer();
  
  MDNS.addService("http", "tcp", 80); //mDNS追加
 
  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");
}
 
void loop() {
  // put your main code here, to run repeatedly:
  delay(10000);
}

これで同じLAN内にあるPCなどからブラウザで「m5camera_f.local」でアクセスできます!
また,こうすることで複数カメラがあっても一つの端末に集約することも可能ですね!

PCに画像保存するぞ

PCにnodeは入ってますか?
入っていないならばこちらから
https://dotstud.io/blog/nodejs-install-use-nodebrew/

今回のバージョンはv12.11.0で行いました

ではnpmで必要パッケージをインストールします

npm i axios

そしてソースコードは以下

"use strict";

const axios = require("axios");
const fs = require("fs");

function main() {
  console.log("capture start");
  axios
    .get("http://m5camera_f.local/capture", {
      responseType: "arraybuffer",
      headers: {
        "Content-Type": "image/jpeg"
      }
    })
    .then(res => {
      fs.writeFileSync("output.png", res.data, "binary");
      console.log("save");
    });
}

main(0);

これだけ

実行して画像が保存されるはず

連番で保存して動画化

動画にする際,ffmpegを使います
さらにそれをnodeで使えるようなライブラリもいれます

以下を参考に
https://qiita.com/nasbi_suganuma/items/222cd894e09b7c5e9652

Macならば

brew install ffmpeg

んでnodeのライブラリを入れる

npm i fluent-ffmpeg

そして以下を参考にして色々便利要素をつけたソースコードはこちらです

注意点は「.outputOptions("-pix_fmt", "yuv420p")」がないと動画化できなかったでしたね


"use strict";

const axios = require("axios");
const fs = require("fs");
const ffmpeg = require("fluent-ffmpeg");

const mDNS_NAME = "m5camera_f";
const baseURL = "http://" + mDNS_NAME + ".local";

// キャプチャ録画数
// 通信がだいたい5秒なので1枚5秒で
const num = 60;
// 保存先ディレクトリ
const dir = "./img3";

const resolusison = 8; //8:1024*768 10(max):1600*1200 0(min):160*120
const vflip = 1; //上下反転 USBが下の設定
const hmirror = 1; //左右ミラー USBが下の設定

async function main() {
  console.log("setup");
  // カメラ設定の通信
  await axios(baseURL + "/control?var=framesize&val=" + resolusison);
  await axios(baseURL + "/control?var=vflip&val=" + vflip);
  await axios(baseURL + "/control?var=hmirror&val=" + hmirror);
  // 保存先のディレクトリを作成
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
  }

  async function loop(_count) {
    console.log(_count + " capture start");
    let res = await axios.get(baseURL + "/capture", {
      responseType: "arraybuffer",
      headers: {
        "Content-Type": "image/jpeg"
      }
    });
    console.log(_count + " save start");
    fs.writeFileSync(
      dir + "/" + ("00000" + _count).slice(-6) + ".jpg",
      res.data,
      "binary"
    );
    console.log(_count + " finish");

    if (_count < num) {
      setTimeout(loop, 100, _count + 1);
    } else {
      console.log("start creating movie");
      ffmpeg(dir + "/%06d.jpg")
        .inputFPS(30)
        .outputOptions("-pix_fmt", "yuv420p")
        .outputFPS(60)
        .on("end", () => {
          console.log("file has been converted succesfully");
        })
        .on("error", e => {
          console.log(e);
        })
        .save(dir + "/output.mp4");
    }
  }

  loop(1);
}

main();

カメラセッティング

自分はこんな感じでセッティングしました

IMG_1696.jpg

IMG_1698.jpg

できた動画はこちらです!
https://youtu.be/DyyCbks2pWY

撮影時間は大体17時前後で,約5秒に1回シャッターで30fpsの動画(動画自体は60fpsで2フレーム同じ画像)
240枚撮ったので 240/30=8秒動画

結果

タイムラプス撮影の間にスマフォをいじれるようになった!
あと,安いカメラなので画素数は多いけど感度が弱いのかなんか暗い感じ
でも逆にいい感じかも

注意

スイッチサイエンスのページでは

※カメラモジュールの長時間使用は、オーバーヒートしがちなため推奨しません。短時間での撮影をお勧めします。

だそうです
自分は30分程度でしたが全然熱くなっていませんでした
もしかしたらストリーミングの長時間撮影かもしれません
であれば,5秒に1フレームと固定されてしまいますが逆に需要があるかもですね

Discussion