SPRESENSE で AVI の動画をサポートしてみた! [SPRESENSE]
SPRESENSEは静止画記録しかサポートしていませんが、せっかくなら動画を記録できるようにしたいなと思い、少し調べてみました。
Motion JPEGであれば対応できるだろうと調べていたら、AVI なら JPG を格納できることが判明!
AVI ファイルの話し
http://landhere.jp/blog/a149.html
紹介されているサイトを巡って調べてみると、音声等も格納できるマルチメディアフォーマットとなっており、真面目に実装しようとするとなかなか工数がかかりそう。まぁ当然だよね。(´・ω・`)
かなり古いフォーマットだし、きっとArduinoで実装している人いるだろ?と探してみたところ、やっぱりいました。オープンソース素晴らしい!
ArduCAM_Mini_Video2SD.ino
https://github.com/ArduCAM/Arduino/blob/master/ArduCAM/examples/mini/ArduCAM_Mini_Video2SD/ArduCAM_Mini_Video2SD.ino
で、このコードを参考にSPRESENSE向けに再実装をしてみたのがこちらです。
https://github.com/YoshinoTaro/Spresense-Simple-AVI-codec
SPRESENSEだと、コードが簡潔にすっきりまとまりました。素晴らしい!
このスケッチで記録した動画を、RIFF Tree Viewer で見ると、このファイルは壊れていると言われてしまうので、まだどこかおかしいようですが、Windows 10 ではきちんと再生できました。
SPRESENSEは、MP3を記録できるのでAVIでオーディオも格納できます。これで音声もサポートできるようになれば、立派なカメラになるのでそのうちチャレンジしてみるかなぁ。
( ´ ▽ ` )ノ
Motion JPEGであれば対応できるだろうと調べていたら、AVI なら JPG を格納できることが判明!
AVI ファイルの話し
http://landhere.jp/blog/a149.html
紹介されているサイトを巡って調べてみると、音声等も格納できるマルチメディアフォーマットとなっており、真面目に実装しようとするとなかなか工数がかかりそう。まぁ当然だよね。(´・ω・`)
かなり古いフォーマットだし、きっとArduinoで実装している人いるだろ?と探してみたところ、やっぱりいました。オープンソース素晴らしい!
ArduCAM_Mini_Video2SD.ino
https://github.com/ArduCAM/Arduino/blob/master/ArduCAM/examples/mini/ArduCAM_Mini_Video2SD/ArduCAM_Mini_Video2SD.ino
で、このコードを参考にSPRESENSE向けに再実装をしてみたのがこちらです。
https://github.com/YoshinoTaro/Spresense-Simple-AVI-codec
#include <Camera.h> #include <SDHCI.h> #include <stdio.h> #include <math.h> SDClass theSD; /* WIDTH == 1280 (0x500) */ #define WIDTH_1 0x00 #define WIDTH_2 0x05 /* HEIGHT == 960 (0x3C0) */ #define HEIGHT_1 0xC0 #define HEIGHT_2 0x03 #define TOTAL_FRAMES 300 #define AVIOFFSET 240 unsigned long movi_size = 0; const char avi_header[AVIOFFSET+1] = { 0x52, 0x49, 0x46, 0x46, 0xD8, 0x01, 0x0E, 0x00, 0x41, 0x56, 0x49, 0x20, 0x4C, 0x49, 0x53, 0x54, 0xD0, 0x00, 0x00, 0x00, 0x68, 0x64, 0x72, 0x6C, 0x61, 0x76, 0x69, 0x68, 0x38, 0x00, 0x00, 0x00, 0xA0, 0x86, 0x01, 0x00, 0x80, 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x84, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x6C, 0x73, 0x74, 0x72, 0x68, 0x30, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x73, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x66, 0x28, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, WIDTH_1, WIDTH_2, 0x00, 0x00, HEIGHT_1, HEIGHT_2, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x4D, 0x4A, 0x50, 0x47, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x10, 0x00, 0x00, 0x00, 0x6F, 0x64, 0x6D, 0x6C, 0x64, 0x6D, 0x6C, 0x68, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x4C, 0x49, 0x53, 0x54, 0x00, 0x01, 0x0E, 0x00, 0x6D, 0x6F, 0x76, 0x69, 0x00 }; File aviFile; String filename = "movie.avi"; uint32_t start_ms = 0; static void inline uint32_write_to_aviFile(uint32_t v) { char value = v % 0x100; aviFile.write(value); v = v >> 8; value = v % 0x100; aviFile.write(value); v = v >> 8; value = v % 0x100; aviFile.write(value); v = v >> 8; value = v; aviFile.write(value); } void setup() { Serial.begin(115200); theCamera.begin(); theSD.begin(); theSD.remove(filename); aviFile = theSD.open(filename ,FILE_WRITE); aviFile.write(avi_header, AVIOFFSET); Serial.println("Recording..."); theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG); start_ms = millis(); digitalWrite(LED0 ,HIGH); } int loopCounter = 0; void loop() { CamImage img = theCamera.takePicture(); if (!img.isAvailable()) { Serial.println("faile to take a picture"); return; } aviFile.write("00dc", 4); uint32_t chunk_top = aviFile.position(); uint32_t jpeg_size = img.getImgSize(); uint32_write_to_aviFile(jpeg_size); aviFile.write(img.getImgBuff() ,jpeg_size); movi_size += jpeg_size; /* Spresense's jpg file is assumed to be 16bits aligned * So, there's no padding operation */ if (++loopCounter == TOTAL_FRAMES) { float duration_sec = (millis() - start_ms) / 1000.0f; float fps_in_float = loopCounter / duration_sec; float us_per_frame_in_float = 1000000.0f / fps_in_float; uint32_t fps = round(fps_in_float); uint32_t us_per_frame = round(us_per_frame_in_float); /* overwrite riff file size */ aviFile.seek(0x04); uint32_t total_size = movi_size + 12*loopCounter + 4; uint32_write_to_aviFile(total_size); /* overwrite hdrl */ /* hdrl.avih.us_per_frame */ aviFile.seek(0x20); uint32_write_to_aviFile(us_per_frame); uint32_t max_bytes_per_sec = movi_size * fps / loopCounter; aviFile.seek(0x24); uint32_write_to_aviFile(max_bytes_per_sec); /* hdrl.avih.tot_frames */ aviFile.seek(0x30); uint32_write_to_aviFile(loopCounter); aviFile.seek(0x84); uint32_write_to_aviFile(fps); /* hdrl.strl.list_odml.frames */ aviFile.seek(0xe0); uint32_write_to_aviFile(loopCounter); aviFile.seek(0xe8); uint32_write_to_aviFile(movi_size); aviFile.close(); Serial.println("Movie saved"); Serial.println(" File size (kB): " + String(total_size)); Serial.println(" Captured Frame: " + String(loopCounter)); Serial.println(" Duration (sec): " + String(duration_sec)); Serial.println(" Frame per sec : " + String(fps)); Serial.println(" Max data rate : " + String(max_bytes_per_sec)); digitalWrite(LED0, LOW); while(1); } }
SPRESENSEだと、コードが簡潔にすっきりまとまりました。素晴らしい!
このスケッチで記録した動画を、RIFF Tree Viewer で見ると、このファイルは壊れていると言われてしまうので、まだどこかおかしいようですが、Windows 10 ではきちんと再生できました。
SPRESENSEは、MP3を記録できるのでAVIでオーディオも格納できます。これで音声もサポートできるようになれば、立派なカメラになるのでそのうちチャレンジしてみるかなぁ。
( ´ ▽ ` )ノ
米国出張ふたたび! [徒然日記]
米国へまたもや出張してまいりましたせっかくの2週連続三連休がつぶれてしまったのは悲しいですが、仕事ですので仕方ありません。
今回は、成田→ダラス(テキサス)→デトロイト(ミシガン)→サンノゼ(カリフォルニア)→サンフランシスコ→羽田 とまたもや遠征コースです。
ダラスは今回2回目です。前回はかなり田舎で宿泊することになりましたが、今回はダラスのフォートワースというビジネス街に宿泊しました。
おしゃれな地域で夜にはライトアップしていました。テキサスは景気がかなり良いように感じました。アメリカでも勢いのある都市だと思います。物価も安いし。最近は人口が増えているそうです。
おしゃれスポットが多くて活気があります
馬車もあって観光地としても楽しめそう
翌日にはデトロイトへ。デトロイトの空港は中に電車が走っていて近未来感がすごいです。
派手な音がするなと思ったら電車が通り過ぎてびっくり!
せっかくなので近くから撮影しました。無人車でケーブルでけん引する方式でした。
空港の案内は英語と日本語だけで、珍しく中国語はありませんでした。やはり車の社会では日本の存在感がまだまだ大きいのですねぇ。
デトロイトというよりミシガンの会社を数社まわったのですが、ミシガンの街は郷土愛があふれていて、あらゆるところにシンボルの黄色のMマークがありました。街も花にあふれていて、一度ゆっくりと観光で訪問してみたい街です。
その後、デトロイトの空港からソルトレイク経由でサンノゼへ。ソルトレイクの空港は夜10時だというのにすごい賑やか。さすが観光地です。
サンノゼに着くころには夜12時過ぎ。さすがに飛行機での移動が多いと疲れてきます。
サンノゼはかなりの回数を訪問しているので、米国でのホームタウンのような感じです。しかし、最近は物価が高くて参ります。極度な物価高のせいか、最近は2010年頃のような熱気を感じられれなくなってきました。シリコンバレー神話もそろそろ歴史の1ページになってしまうのかな。
シリコンバレーの朝はいつもさわやか
会社の近くの交差点にて
今回も西海岸、東海岸と駆け足で回ってきたので大変でしたが、米国でビジネスするって飛行機で飛び回るのが当たり前なんだとあらためて感じました。ほんと日本とはスケールが違いますね。
(+_+;
帰りはJAL便。飛行機乗った時から日本という感じでほっとします。
今回は、成田→ダラス(テキサス)→デトロイト(ミシガン)→サンノゼ(カリフォルニア)→サンフランシスコ→羽田 とまたもや遠征コースです。
ダラスは今回2回目です。前回はかなり田舎で宿泊することになりましたが、今回はダラスのフォートワースというビジネス街に宿泊しました。
おしゃれな地域で夜にはライトアップしていました。テキサスは景気がかなり良いように感じました。アメリカでも勢いのある都市だと思います。物価も安いし。最近は人口が増えているそうです。
おしゃれスポットが多くて活気があります
馬車もあって観光地としても楽しめそう
翌日にはデトロイトへ。デトロイトの空港は中に電車が走っていて近未来感がすごいです。
派手な音がするなと思ったら電車が通り過ぎてびっくり!
せっかくなので近くから撮影しました。無人車でケーブルでけん引する方式でした。
空港の案内は英語と日本語だけで、珍しく中国語はありませんでした。やはり車の社会では日本の存在感がまだまだ大きいのですねぇ。
デトロイトというよりミシガンの会社を数社まわったのですが、ミシガンの街は郷土愛があふれていて、あらゆるところにシンボルの黄色のMマークがありました。街も花にあふれていて、一度ゆっくりと観光で訪問してみたい街です。
その後、デトロイトの空港からソルトレイク経由でサンノゼへ。ソルトレイクの空港は夜10時だというのにすごい賑やか。さすが観光地です。
サンノゼに着くころには夜12時過ぎ。さすがに飛行機での移動が多いと疲れてきます。
サンノゼはかなりの回数を訪問しているので、米国でのホームタウンのような感じです。しかし、最近は物価が高くて参ります。極度な物価高のせいか、最近は2010年頃のような熱気を感じられれなくなってきました。シリコンバレー神話もそろそろ歴史の1ページになってしまうのかな。
シリコンバレーの朝はいつもさわやか
会社の近くの交差点にて
今回も西海岸、東海岸と駆け足で回ってきたので大変でしたが、米国でビジネスするって飛行機で飛び回るのが当たり前なんだとあらためて感じました。ほんと日本とはスケールが違いますね。
(+_+;
帰りはJAL便。飛行機乗った時から日本という感じでほっとします。
GO TEXAS テキサスへ行こう―ほしのあつおのスケッチ旅
- 作者: ほしの あつお
- 出版社/メーカー: 風土社
- 発売日: 2002/05
- メディア: ペーパーバック
不確実性のマネジメント―危機を事前に防ぐマインドとシステムを構築する (ミシガン大学ビジネス・スクール)
- 作者: カール・E. ワイク
- 出版社/メーカー: ダイヤモンド社
- 発売日: 2002/07
- メディア: 単行本
またもや米国出張! [徒然日記]
また米国へ出張となりました。今回は前日に旅程が変更になるドタバタがあり、やや不安を抱えての出発です。
成田からダラスの直行便は12時間。長いです。ダラスにつくとまだ朝の8時。飛行機の中ではなかなか寝れないタイプなので辛い辛い。
幸い到着は日曜日だったので、ホテルにアーリーチェックインをさせてもらい、ゆっくり体を休めました。
明日にはリフレッシュしてがんばろ。旅の詳細はまた後日アップデートします!
( ・`д・´)
SPRESENSEカメラのモニターをProcessingで作ってみた! [SPRESENSE]
前回、SPRESENSEとProcessingを使ってタイムラプスカメラを作ってみました。そこに、いつもチェックしているスタックオーバーフローで次のような問いかけが!
カメラのリアルタイムのプレビューをPCの画面に表示する方法
SPRESENSEのカメラで現在映しているプレビューを、PCの画面に表示させることは可能でしょうか。接続方法は、できれば、USB(メインボード側のSerial)が良いです。
ん!?これは、できそうな気がするぞ。SPRESENSEとProcessingを使えば…。
ということでチャレンジしてみました!
接続はリクエスト通りシリアルケーブル一本で。SPRESENSE からプレビュー画像を転送し、PC上の Processing でデータを受けて画像を表示します。
余談ですが、少し前に3Dプリンタで作ったカメラホルダをつけてみました。カメラとUSBケーブルの干渉がなくなり、少しすっきりしました。
今回作成したスケッチです。すごくシンプルなのですが、Processing のシリアルはなかなか使いこなすのが大変で苦労しました。Windows + Java のためか、リアルタイムには程遠く、安定した通信をするために、ところどころ工夫が必要でした。
SPRESENSEのスケッチ
Processingのスケッチ
動かしてみると、パフォーマンスは、おおよそ 0.9-0.8fps 。理論的には、 (2000000/8) bytes / 153,600 bytes / =1.6 fps 位のはずですが、Processing の応答性が思いのほか悪く、あまり良い数字は出ませんでした。
実際の動きはこちらの動画で見ることができます!
パフォーマンスはいまいちですけど、この小さなカメラで画面を見れるのはいいですね。次はプレビューではなくて、もう少し大きな画像を転送してみようかな…。
(^^)/~
<参考記事>
SPRESENSEとProcessingでタイムラプスに挑戦!
https://makers-with-myson.blog.so-net.ne.jp/2019-08-17
カメラのリアルタイムのプレビューをPCの画面に表示する方法
SPRESENSEのカメラで現在映しているプレビューを、PCの画面に表示させることは可能でしょうか。接続方法は、できれば、USB(メインボード側のSerial)が良いです。
ん!?これは、できそうな気がするぞ。SPRESENSEとProcessingを使えば…。
ということでチャレンジしてみました!
接続はリクエスト通りシリアルケーブル一本で。SPRESENSE からプレビュー画像を転送し、PC上の Processing でデータを受けて画像を表示します。
余談ですが、少し前に3Dプリンタで作ったカメラホルダをつけてみました。カメラとUSBケーブルの干渉がなくなり、少しすっきりしました。
今回作成したスケッチです。すごくシンプルなのですが、Processing のシリアルはなかなか使いこなすのが大変で苦労しました。Windows + Java のためか、リアルタイムには程遠く、安定した通信をするために、ところどころ工夫が必要でした。
SPRESENSEのスケッチ
#include <Camera.h> #define BAUDRATE 2000000 void CamCB(CamImage img) { if (img.isAvailable() == false) return; while (Serial.available() <= 0); // taking a picture is started by receiving 'S' if (Serial.read() != 'S') return; delay(1); // wait for stable connection digitalWrite(LED0, HIGH); img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); char *buf = img.getImgBuff(); for (int i = 0; i < img.getImgSize(); ++i, ++buf) { Serial.write(*buf); } digitalWrite(LED0, LOW); } void setup() { Serial.begin(BAUDRATE); while (!Serial) {}; theCamera.begin(); theCamera.startStreaming(true, CamCB); theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); } void loop() { /* do nothing here */ }
Processingのスケッチ
import processing.serial.*; import java.io.*; Serial myPort; PImage img; final static int WIDTH = 320; final static int HEIGHT = 240; boolean started = false; int serialTimer = 0; int total = 0; int x = 0; int y = 0; void setup() { size(320, 240); background(0); img = createImage(WIDTH, HEIGHT, RGB); myPort = new Serial(this, Serial.list()[0], 2000000); myPort.clear(); println("setup finished"); delay(2000); // wait for stable connection } void draw() { if (started == false) { started = true; println("start"); myPort.write('S'); myPort.clear(); total = 0; delay(10); return; } // To get stable connection, please adjsut this interval final int interval = 1; if (millis() - serialTimer > interval) { serialTimer = millis(); if (myPort.available() <= 0) return; while (myPort.available() > 0) { char lbyte = (char)myPort.read(); char ubyte = (char)myPort.read(); x = total % WIDTH; y = total / WIDTH; int value = (ubyte << 8) | (lbyte); // RGB565 format char r = (char)(((value & 0xf800) >> 11) << 3); char g = (char)(((value & 0x07E0) >> 5) << 2); char b = (char)((value & 0x001f) << 3); color c = color(r, g, b); img.set(x, y, c); ++total; if (total >= WIDTH*HEIGHT) { println("end"); myPort.clear(); started = false; total = 0; image(img, 0, 0); break; } } } }
動かしてみると、パフォーマンスは、おおよそ 0.9-0.8fps 。理論的には、 (2000000/8) bytes / 153,600 bytes / =1.6 fps 位のはずですが、Processing の応答性が思いのほか悪く、あまり良い数字は出ませんでした。
実際の動きはこちらの動画で見ることができます!
パフォーマンスはいまいちですけど、この小さなカメラで画面を見れるのはいいですね。次はプレビューではなくて、もう少し大きな画像を転送してみようかな…。
(^^)/~
<参考記事>
SPRESENSEとProcessingでタイムラプスに挑戦!
https://makers-with-myson.blog.so-net.ne.jp/2019-08-17