見出し画像

Unit-CamS3で撮影したカメラ映像やSDカードのmjpeg動画をESPNowCam経由で通信した話


はじめに

(AIアシスタントに書かせてみました)
映像や動画の通信手段として注目されているESPNowCamとは何か、Unit-CamS3との連携方法、そしてそのメリットについて解説します。
Unit-CamS3で撮影したカメラ映像やSDカードのmjpeg動画をESPNowCam経由で通信することで、高速なデータ送受信が可能になります。
ESPNowCamを使用することで、ワイヤレスでの通信による利便性が向上し、映像や動画のリアルタイム配信や遠隔監視など、さまざまな用途に応用することができます。
また、ネットワーク接続が不要なため、簡単な設定で迅速に利用開始することができます。
本記事では、ESPNowCamの基本的な機能や設定方法を詳しく紹介し、Unit-CamS3との連携による映像や動画の通信の一例を解説します。
映像や動画の通信手段を求める方や、ESPNowCamの活用方法を知りたい方は必見です。

motivation

現在制作しているIsolation  Sphereでディスプレイ画面に動画を再生したいと考えています.

このIsolation  sphereは,メモリ内にテクスチャマッピングとして画像を保持し,球状に配置されたLEDの各色をテクスチャメモリから抽出することで画像を呈示することができます.
これまではこのテクスチャ画像をSDカードに保存して読み込んでいましたが外殻全面がLEDで覆われているため,回路・電池などを内部に保持しなければならない構造になっています.

球体内部


閉じたところ

電源や操作用のスイッチ類や充電用のコネクタなど,外側に突出するような部品は取り付けることができなくなっています.
そのため,BLEやWiFiなどを使って無線で操作を行なったり,ワイヤレス給電の仕組みを使って外部からワイヤレスでアクセスする仕組みが必要になります.

通信方式の選定

もちろんESPNowもワイヤレス通信方式として有望な候補となっています.
候補であるBLE,WiFi,ESPNowの通信方式の特徴について表にまとめると

性能比較

こんな感じかな,と思います.この中で重要だと考えているのは

  • 消費電力:バッテリー駆動のため

  • 通信速度:速ければ速いほど応答性が上がるので良い

  • 混線耐性:MakerFaireなど混雑した環境下で通信可能か

このあたり.
BLEはとても扱いやすく,なおかつDabbleというアプリがありスマートフォンから簡単に操作可能な手法があります.

UIの操作であれば(通信速度がそれほど速くなくても問題ないし,通信量も少ない)Dabbleは非常に有効で,Isolation Cubeでは採用しています.
しかし,UIを作り直すことができないという問題点があったりと皆どこか足りない....

WiFiは消費電力が大きく,混線耐性が低い(MakerFaireTokyoでは展示時に繋がらず阿鼻叫喚)ため,最初から採用は見送り.
もちろんDabbleでは不可能なUIレイアウトなども自由度が高いんですけれどね.

そして最後の候補ESPNow.
私にとって最初の印象があまり良くなく(WiFi系なので消費電力が...など)しかも1パケットの通信上限があり250byteと低い...
しかし,混線耐性についてはこのような投稿(MakerFaireTokyoでの動作実績)もあり,

おやおや,なかなか良さげじゃないの?となったところに

このように映像をESPnow経由で送っている投稿を見かけました.しかも10Hz出てる.
調べてみると

そして日本でも実際に適用している人も!

これはすごい!
これなら10Hz程度で画像を送信できるので,母艦側にSDカードなどで動画を入れておいて,都度映像を送信することで映像の呈示をIsolation  Sphere
を開けることなく切り替えることができる!

カメラ映像の送信を @UtaAoya さんがやってくださっているので,私は同じ仕組みを活用してSDカードに入っているmjpeg動画を転送できるようにしたことについてお話しします.

構成

基本構成は @UtaAoya さんの構成を踏襲します.
変なところでトラブルに見舞われたくないので.

M5Stack用CamS3 Wi-Fiカメラユニット (OV2640)

こちらで販売されています.

開発中だと頻繁にhungoutしてしまうため、いちいち配線でBOOTやRESETをツンツンしなくていいようにボタンを取り付けました。

BOOTとRESETをボタン化

違いはそのくらい.あとは丸パクリです.
しかし,このGROVE端子からUSBに変換するコネクタは優秀ですね.GROVEは単体で使えるし,開発が終われば不要なUSBコネクタを排除できる.なおかつ柔らかいシリコンコーティングのGROVEケーブルを使えば配線も柔軟.思わず他のでも使おうとポチりました.
全部こうなれば良いのに.

基板から筐体外部に配線する時にはとても良いかも(Isolation Sphereでは関係ないですが・・・)

M5Core2

M5Core2はもう古株と言ってもよいM5Stack製品なので,あまり説明することはありません.

これを選定したのも @UtaAoya から丸パクリです.すみませんw
ディスプレイがついているのが大きな選定理由.家に放置されていたものを発掘して使います.

プログラム

ソフトウェア構成

 @UtaAoya さんが作成した構成をESPNowCam本家がgithubに上げてくれたものがあるので,それを活用.

examples/unitcams3-basic-sender

これを使用してプログラムを組みました.
受け手はまんまこれを使用.

examples/m5core2-espnow-receiver

あっという間にできました.
素晴らしい.
送りたいmjpegの動画サイズが320x160なので改変

void processFrame() {
    if (Camera.get()) {
        uint8_t *out_jpg = NULL;
        size_t out_jpg_len = 0;
        int quality = Camera.config.jpeg_quality;

        Camera.fb->height = 160; 
        Camera.fb->len = 320 * 160 * 2;

        frame2jpg(Camera.fb, quality, &out_jpg, &out_jpg_len);
        radio.sendData(out_jpg, out_jpg_len);
        // Serial.printf("\tJPG lenght: %u  %d \r\n", out_jpg_len, sizeof(Camera.fb));
        printFPS("CAM:");
        free(out_jpg);
        Camera.free();
    }
}
        Camera.fb->height = 160; 
        Camera.fb->len = 320 * 160 * 2

改変箇所はここだけ

mjpeg動画作成

まずはmjpegファイル作成.
ffmpegを使って.

ffmpeg -i input.mp4 -c:v mjpeg -q:v 12 -s 320x160 -an output.mjpeg

ビデオ品質はカメラ画像に合わせて12,解像度は320x160に指定してファイルを出力します.
これをSDカードにimageというフォルダを作ってコピーして元のUnitCamS3に挿します.

mjpegのフォーマット

私もmotion Jpegについてわかっていなかったのでここでちょっと勉強.
wikipediaには

それぞれのフレームJPEG形式で圧縮/伸長し、連続でこれを表示することで動画としている。

wikipedia

mpegなどはフレーム間で圧縮をかけているので,前後のフレームの情報がないと1フレームを元に戻すことができないのですが,mjpegは各フレームを一枚の画像としてjpeg圧縮をして,それを連続で繋いでいる画像フォーマット,ということになります.

理屈はわかります.知ってます.
でも,どうやっていフレームを取り出すの?って書いてないんですよ...
各フレームをjpeg圧縮しているのでフレームごとのファイルサイズは異なり,ファイルサイズがわからないとespnowで転送することができません.

そこで,claude3に聞いてみました.
定義上では1フレームの開始に「FF D8」,終端に「FF D9」があり,ファイルをスキャンしながらこれを区切りにしてフレームを区切るようです.下はとあるmjpegの中身.ハイライトがあるのが終端の「FF D9」.よくみるとその隣に次のフレームの開始の「FF D8」があるのがわかります.

mjpegの中身

SDカードでmjpegを読み込み,1フレームづつ格納する

今回はclaude3さんに実装をお願いしていきましょう.

SDカードからmotion JPEGのファイルを読み込んで1フレームづつ取り出してm5canvasに描画しようと思ってます。 また、最後のフレームに到達したら最初のフレームに戻ってループ再生する機能が欲しいです。

claude3

とまあこんなプロンプト.
m5canvasに書き込むのは最終的にLEDに書き込む際にm5displayなどに入れることはできないためです.

出てきたソースをコンパイルすると,エラーが出ます.
そのエラーを再度claude3にぶち込むと,claude3がしれっと

申し訳ありません。コードにいくつか問題がありました。M5Unifiedライブラリを正しくインクルードし、SDカードアクセスライブラリをロードする必要があります。以下のように修正してみてください。

claude3

新しく修正したコードを吐き出します.
これをもう一度コピペしてコンパイル→エラー→claude3が修正を繰り返し,ソースコードを作りました.
update部分だけ抜粋.

uint32_t update() {
            static uint32_t start = 0;
            uint32_t pos = 0;
            bool newFrame = false;

            while (movieFile.available()) {
                frameBuf[pos++] = movieFile.read();
                if (pos >= 4 && frameBuf[pos - 2] == 0xFF && frameBuf[pos - 1] == 0xD9) {
                    newFrame = true;
                    break;
                }
            }

            if (newFrame) {
                // M5.Lcd.drawJpg(frameBuf, pos, 0, 0);
                if (pos == frameSize) {
                    movieFile.seek(start);
                    start = 0;
                    frameCount = 0;
                    Serial.println("Reached end of file");
                }
                else {
                    start += pos;
                    frameCount++;
                    // Serial.println(frameCount);
                    // Serial.printf("Frame: %d  %d %d  %d\n", frameCount, start, pos, frameSize);
                }
                return pos;
            }
            return 0;
        }

while文の中でposをスキャンして0xFFと0xD9を見つけています.
戻り値はファイルサイズとなっており,別の関数で読み込んだframeBufを取得する関数getFrameBufを用意して,ファイルの中身とサイズを取得します.

        uint8_t *getFrameBuf() {
            return frameBuf;
        }

main.cppのloop関数で

void loop() {
    if(fileFlg){
        // Serial.println("File");
        uint32_t framesize = sdFile.update();
        uint8_t* buf = sdFile.getFrameBuf();

        if (framesize != 0){
            // 新しいフレームが描画された場合の処理
        } else {
            // フレームが描画されなかった場合の処理
            // 待機などの処理を行う
            if(sdFile.available() == 0) {
                Serial.println("Reached end of file");
                sdFile.seek(0);
            }
        } 
        radio.sendData(buf, framesize);
        printFPS("FILE:");
    }else{
        processFrame();
    }
}

このようにframeSizeとbufを取得した後sendDataすることで,カメラ版で送ったものと同じようにjpegファイルを送信できます.

これは炎のmjpegをカメラ画像の代わりに送信した結果です.
カメラ画像より圧縮率が高いのかコンスタントに10Hz ほどでています,

あとはUIでファイルかカメラか,さらにファイルのを切り替えもでき,好きな動画を送信できます.

さいごに

私の開発で必要な機能であるSDカードから読み込んだmjpeg動画をESPnow通信でフレームごとに送信する,というプログラムを作りました.

前からずっと動画を扱う方法について考えていたので,ここでまとめて実装まで持っていくことができて良かったです.
animationGifとか考えていたんですが,思ったより圧縮されずjpegの方がファイルサイズ小さかったり,コプロによってはjpegの処理が別処理でできたりもするのでjpegがFAなのかなあ.


あと,生成AIにchatGPTではなくclaude3を使いました.
いや本当はchatGPTも使ったのですが,claude3の方が圧倒的に生成するコードの精度が良かった.
これは色々な人も指摘していましたが,やっぱり本当なんだな,と.

この記事が気に入ったらサポートをしてみませんか?