【ラズパイ】【カメラ】WEBから動画を撮影する。

今回はWebのプレビュー画面から動画の撮影を行いたいと思います。

赤丸のボタンを設置し、押すと録画開始、もう一度押すと録画停止という感じです。

まずはサーバ側。

すでに動画を撮影する方法は知っているので、これを使用します。

POSTリクエストを受け付ける処理を書いていきます。

    def do_POST(self):
        global thread
        global aviFilename
        global capture
        global out

        content_len = int(self.headers.get('content-length'))
        requestBody = json.loads(self.rfile.read(content_len).decode('utf-8'))

        if requestBody['contents']['command'] == 1:
            _, img = cap.read()
            dt_now = datetime.datetime.now()
            filename = dt_now.strftime('%Y%m%d_%H%M%S')+".jpg"
            cv2.imwrite(filename, img)

            response = {
                'status' : 200,
                'path': "http://pi4.local:8000/" + filename
            }
        if requestBody['contents']['command'] == 2:
            dt_now = datetime.datetime.now()
            aviFilename = dt_now.strftime('%Y%m%d_%H%M%S')+".avi"
            out = cv2.VideoWriter(aviFilename, fourcc, 20.0, (640,480))
            
            capture = True
            thread = threading.Thread(target=videoCapture)
            thread.start()

            response = {
                'status' : 200,
            }
        if requestBody['contents']['command'] == 3:
            capture = False

            thread.join()

            out.release()
            out = None

            response = {
                'status' : 200,
                'path': "http://pi4.local:8000/" + aviFilename
            }
        else:
            response = {
                'status' : 200
            }

        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        responseBody = json.dumps(response)

        self.wfile.write(responseBody.encode('utf-8'))

録画開始(command=2)を受信した場合、VideoWriterを作成し、動画を保存するスレッドを起動します。

録画停止(command=3)を受信した場合は、このスレッドを停止するように動作します。

実際のスレッド処理はこんな感じです。

def videoCapture():
    while capture:
        _, img = cap.read()
        out.write(img)

captureフラグがTrueのときは延々とカメラの映像を動画ファイルに保存します。

録画停止時にcaptureフラグをFalseに変更し、このスレッドが終了するのをjoinで待ってから、作成された動画ファイルのパスを返信します。

動画のダウンロードですが、たぶん、content-typeを用意しないと行けないので、Dictionaryに追加しています。

CONTENT_TYPE = {'.html': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'text/javascript', '.json': 'application/json',
        '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
        '.css':'text/css', '.avi': 'video/x-msvideo'}

さて、このままだと録画中にブラウザを閉じてしまって、録画を止める手段が無くなってしまいます。

これの対処を次回やります。

(そろそろカメラネタも無くなってきた。)

【テクテクライフ】滝野すずらん丘陵公園

滝野すずらん丘陵公園は、札幌市南部の山の中にある公園で、もちろん徒歩で行けるところではありません。

一般的にバスか自家用車で行くことになります。

バスは地下鉄南北線真駒内駅と東豊線福住駅から出ています。

入るには、入場料がかかりますが、障害者手帳があれば無料になります。

山の中なので、今の時期なら紅葉がきれいに見えます。

今が見ごろだね。

さて、これで札幌名所10選ラリーを攻略しました。

今後の予定は・・・地道に塗っていきますかね。

まだ札幌市13%しか塗れていないんすよ。

雪が降るまでの残りわずか、もう少し頑張ります。

cocos2d-xのコーディングしてて思ったこと。(ガベコレの話)

C#やJavaにはガベージコレクションと言って、使用しなくなったオブジェクトを自動的に解放してくれる仕組みがありまして、

だからnewしたオブジェクトの使用後を意識しなくても使用できるんですが、

C++にはそんな機能は無く、

newしたオブジェクトは明示的にdeleteしないと、そのオブジェクトは消えません。

で、cocos2d-xはspriteなどを作成するときにnewを使用せずにcreate()メソッドを使用しているんです。

そして、サンプルを見る限りではdeleteは行っていない。

これはcocos2d-xのプラットフォームの中で独自にガベージコレクション的な動きをしています。

ただ、cocos2d-xのガベコレとC#やJavaのガベコレとは少しロジックが異なるようです。

詳しい内容はこの記事を見て欲しく

http://furicotech.blogspot.com/2015/03/memory-management-in-cocos2d-x.html

まぁ、この内容をまとめると、

cocos2d-xのspriteなどのオブジェクトは1回使い切り。(その都度createしなければならない。)

これ、自分もコーディングしててハマりました。

あると思っていたspriteのポインタが無効なポインタになっていた、と言うことが多々ありました。

C#と同じ感覚で使用することができないんですね。

なので、クラス設計をどうするか、少し悩んでいます。

spriteなどの部品はシーンクラスの中で全部処理させた方が良いかもしれない。

【cocos2d-x】タッチした場所にエフェクトを入れる

よくスマホゲームって、タッチした場所にアニメーションを入れてエフェクトかけてるじゃないですか。

あれを実装して見たいと思います。

今回もフリー素材のぴぽや倉庫さんから素材を拝借しました。

すでに、タッチ処理のやり方もアニメーションのやり方も知っているので、これを組み合わせます。

コードはこうなりました。

bool HelloWorld::init()
{

  中略

    auto listener1 = EventListenerTouchOneByOne::create();
    listener1->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this);

    return true;
}

bool HelloWorld::onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event)
{
    auto anime = Sprite::create("pipo-btleffect007.png");
    if (anime == nullptr)
    {
        problemLoading("'pipo-btleffect007.png'");
    }
    else
    {
        this->addChild(anime, 0);
    }
    float frameHeight = anime->getContentSize().height;
    float frameWidth = anime->getContentSize().width / 14.0;
    Vector<SpriteFrame*> animFrames;
    animFrames.reserve(14);
    for(int i = 0; i < 14; i++) {
        animFrames.pushBack(SpriteFrame::create("pipo-btleffect007.png", Rect(frameWidth * i,0,frameHeight,frameWidth)));
    }
    Animation* animation = Animation::createWithSpriteFrames(animFrames, 0.02f);
    Animate* animate = Animate::create(animation);

    anime->setPosition(touch->getLocation().x, touch->getLocation().y);
    anime->runAction(animate);

    return true;
}

前回のタッチ処理でラムダ式を使いましたが、

C++のラムダ式は思った以上に使いづらいので

普通にコールバックとして関数を定義し、マクロで登録する形にしました。

アニメーション画像は14枚横に並んでいるので、この画像を横に14等分割にクリッピングして使用しています。

あとはアニメーション画像の位置をタップ位置に設定してアニメーションを実行します。

前回はanimateをforever(無限ループ)していましたが、今回は1回きりのアニメーションなので、animateをそのまま使用しています。

どちらも親クラスがActionクラスなので、問題ありません。

【テクテクライフ】モエレ沼公園

モエレ沼公園は札幌市の北部にある、中島公園よりも広い公園です。

中心部から歩いて行くのは無謀で、一般的にバスか自家用車を使用します。

バスで行く場合は、地下鉄東西線バスセンター駅からバスセンターでモエレ沼公園行き(期間限定)に乗るか、地下鉄南北線麻生駅または、地下鉄東豊線栄町駅からモエレ沼公園行きのバスに乗ると入り口まで行けます。

入り口には自転車レンタルのお店があります。

はい、自転車移動推奨のとてつもなく広い公園なんです。

だいぶ葉っぱが色づいてきたね。

公園の中央には噴水広場がありまして、時間になると噴水ショーが見られます。

もちろん無料。

15分のショートバージョンと40分のロングバージョンがあります。

そして、モエレ沼の象徴と言えばこれでは無かろうか。

高いね。

昔1回登ったことあるけど、スゴイ大変だった記憶がある。

キノコ。

晴れている日はピクニックしている人達が多いね。

みんなテント張ってランチ食べてる。(公園内は火気厳禁であるのでバーベキューはできない)

そういう優雅な休日も過ごしてみたいなぁ。

【北海道大戦】戦闘結果をメインシーンへ反映

たぶん、これが最後の仕上げ。

まずはじゃんけんの結果を判定します。

    class BattleScene : asd.Scene
    {
        private void onClickMouseShowActionResult(asd.Vector2DF pos)
        {
            var result = janken(selectedAttack, selectedDeffece);
            if(result == BattleResult.win)
            {
                _deffencePower -= _attackPower;
                if(_deffencePower <= 0)
                {
                    Singleton.GameData.BattleResultUpdate(BattleResult.win);
                    var scene = new MainScene();
                    asd.Engine.ChangeScene(scene);
                }
                _deffenceParam.Text = "戦闘力:" + _deffencePower;
            }
            else if(result == BattleResult.lose)
            {
                _attackPower -= _deffencePower;
                if (_attackPower <= 0)
                {
                    Singleton.GameData.BattleResultUpdate(BattleResult.lose);
                    var scene = new MainScene();
                    asd.Engine.ChangeScene(scene);
                }
                _attackParam.Text = "戦闘力:" + _attackPower;
            }
            _attackResult.Hide();
            _deffenceResult.Hide();
            _status = GameStatus.SelectDeffenceAction;
        }

        private BattleResult janken(BattleIcon.Icon attack, BattleIcon.Icon deffence)
        {
            switch (attack)
            {
                case BattleIcon.Icon.Gu:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.draw;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.win;
                        case BattleIcon.Icon.Par:
                            return BattleResult.lose;
                        default:
                            return BattleResult.draw;
                    }
                case BattleIcon.Icon.Choki:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.lose;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.draw;
                        case BattleIcon.Icon.Par:
                            return BattleResult.win;
                        default:
                            return BattleResult.draw;
                    }
                case BattleIcon.Icon.Par:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.win;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.lose;
                        case BattleIcon.Icon.Par:
                            return BattleResult.draw;
                        default:
                            return BattleResult.draw;
                    }
                default:
                    return BattleResult.draw;
            }
        }

今回のじゃんけん判定はベタな方法で実装しました。

上手い方法が思い浮かばなかった。

janken()でじゃんけんの結果を判定し、その結果を反映、勝負が決まったら、その情報をGameData.BattleResultUpdate()に反映し、MainSceneに遷移します。

    class GameData
    {
        public void BattleResultUpdate(BattleResult result)
        {
            if (gameStatus == GameStatus.ActionEnemy)
            {
                Battle.EnemyTurnEnd(result);
            }
            else if (gameStatus == GameStatus.ActionPlayer)
            {
                Battle.MyTurnEnd(result);
                gameStatus = GameStatus.ActionEnemy;
            }
        }

状態が敵のターンか、自分のターンかで分岐させます。

    class Battle
    {
        public void EnemyTurnEnd(BattleResult result)
        {
            if(result == BattleResult.win)
            {
                lastAttack.CombinationCity(lastDeffece);
                lastDeffece.Lose();
                aliveCities.Remove(lastDeffece);
                lastDeffece = null;
            }
        }

        public void MyTurnEnd(BattleResult result)
        {
            if (result == BattleResult.win)
            {
                lastAttack.CombinationCity(lastDeffece);
                lastDeffece.Lose();
                aliveCities.Remove(lastDeffece);
                lastDeffece.ClearPaint();
                lastDeffece = null;
            }

            lastAttack.ClearPaint();
            lastAttack = null;

            cityCnt++;
            if (cityCnt >= _cities.Count)
            {
                _cities = cityRandomReplace(aliveCities);
                aliveCities = copyCity(_cities);
                cityCnt = 0;
                turn++;
            }
        }

これでデータの反映もできたはず。

あとはもう少しブラッシュアップさせて行きます。

【ぼっち】七輪焼肉 安安 札幌南3条店

障害年金が入ったんだ。少しぐらい贅沢しても良いじゃ無いか。

今回は安安コース2980円+アルコール飲み放題980円を選択。

七輪で焼く焼肉は美味しい。

炭水化物を先に食べることで、食べ過ぎを防ぐという高度なテクニック。

食べ放題には向いていないけど。

しかし、美味しければ問題無い。

さて、今日、コレステロール値が高いと言われたので、少しお肉を控えて、運動するように心がけたいと思う。

はい。

ビジモネットからビッグローブへ乗り換え

俺の夜のマッタリ時間を返して欲しい。

ビジモネット回線設置から半年が過ぎたので、今の回線より安い値段で他のプロバイダに乗り換えできますよ、

という連絡を、夜8時に受けました。

焼肉から帰ってきて、良い感じに酔っ払っているこのタイミングに、である。

酔っ払っているときにそんな重要な話やめて。

どうやら話を聞くと、このままビジモネットを使い続けていても良いけど、高いっすよ。このタイミングで他のプロバイダに乗り換えた方が安くなりますよ、ということである。

ちなみに、これを言っているのは、引越しの時に回線引越しの手引きをしてくれた業者である。どうやらこの人はビジモネットの人ではないらしい。

で、そのあとにやったこと。

1.引越し前の回線解約料分のキャッシュバックを受けられるので、料金明細をFAXして欲しい。

2.NTT回線の転用手続きをして欲しい。今使用している回線は切り替え先プロバイダに引き継ぐための、その手続き。

完了したら案内番号を連絡。これは電話を受けたその場でやった。(もちろん酔っ払ってる状態)

ちなみに、回線工事料金は全て業者持ちでこちらの負担は無しとのこと。

3.ビッグローブから回線切り替え手続きの連絡が来るので、対応して欲しい。

これは今日やった。手続き完了。

4.ビッグローブからIDが発行されて、書類が自宅に届いたら、すでにビッグローブの回線が使えるので、モデムの設定を済ませて、繋がることを確認したら、ビジモネットに解約の連絡を入れる。

ネットが繋がらなくなる期間はないらしい。良き良き。

5.ビジモネットの解約手続きが完了したら、それを業者に連絡。

なお、ビジモネットの解約手数料も業者が負担してくれるらしい。

ちなみに、この回線切り替えでどれくらいコストが安くなるかというと、

今まで:NTT4925円+ビジモネット878円=5803円

切り替え後:ビッグローブ3980円(初回3000円、3年更新、解約手数料2万円)

1900円ぐらい安くなるみたいです。

ちなみに、ビッグローブを選択した理由は、特にありません。

@niftyが切り替え後の選択肢になかったので、適当に決めました。

いや、だから、酔っ払ってるときに重要な話しないでって言ったでしょ。

まぁ、また何か進展があったらご報告します。

【プロジェクトセカイ】しばらくプレイし続けて思ったこと。

どこか行きたい。

なんとかmasterも普通にクリアできるレベルになりました。

EXもフルコンできた曲増えてきたし。

いきなりこんな譜面出てきたときは、確実に初心者を殺しに来てると思いました。

難しそうで、慣れちゃえば、意外と簡単に行けるもんすよ。

指をクロスしなくちゃいけないけど。

親指じゃ厳しいかもしれない。

そして、とにかく最初はデッキの強化に努めるしかない。

スコアランクがA以上になればレアアイテム獲得できる確率が高くなる。

それまでは、ありったけのリソースを使ってデッキを強化するしかない。

ベテランルームに入れることができれば、また変わってくるかもしれない(←まだ入れない)

ライブボーナスの使用量を調整できるのは良いですね。

ライブボーナス残っていても、消費しないでプレイすることも可能。

練習し放題。

メルト選曲すると切断されがち。

【ラズパイ】【カメラ】Webからカメラの画像を保存する。

前回はカメラのプレビュー画面をWebに表示させましたが、

今回はWebからシャッターボタンを設置して、カメラの画像を保存させます。

すでにPOSTリクエストを処理する方法も知っていますし、カメラの画像を保存する方法も知っているので、これらを組み合わせればできるはずです。

まずはサーバ(ラズパイ)の処理。


    def do_POST(self):
        content_len = int(self.headers.get('content-length'))
        requestBody = json.loads(self.rfile.read(content_len).decode('utf-8'))

        if requestBody['contents']['command'] == 1:
            _, img = cap.read()
            dt_now = datetime.datetime.now()
            filename = dt_now.strftime('%Y%m%d_%H%M%S')+".jpg"
            cv2.imwrite(filename, img)

            response = {
                'status' : 200,
                'path': "http://pi4.local:8000/" + filename
            }
        else:
            response = {
                'status' : 200
            }

        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        responseBody = json.dumps(response)

        self.wfile.write(responseBody.encode('utf-8'))

POST処理を追加しました。

カメラの画像をファイルに保存し、そのファイルパスをレスポンスで返すようなイメージです。

次はWeb側。

<!DOCTYPE html>
<html>
<head>
  <title>My first Vue app</title>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="vue.min.js"></script>
  <script src="jquery-3.5.1.slim.min.js"></script>
</head>
<body>
  <div id="app">
      <image id="camera" src="" /><br />
      <button @click="onShutter">Shutter</button><br />
      <a id="picture" href="" target="_blank">{{ path }}</a>
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        timer: null,
        param: {},
        contents: {
          command: 1,
        },
        path: "",
      },
      created: function() {
        self = this;
        this.timer = setInterval(function() {self.onLoad()}, 50)
      },
      methods: {
        onLoad: function() {
            axios.get('http://pi4.local:8000/Streaming').then(function(response){
                $("#camera").attr('src', response.data.image);
            }).catch(function(error){
            });
        },
        onShutter: function() {
          self = this;
          this.param.contents = this.contents;
          axios.post('http://pi4.local:8000/', this.param).then(function(response){
                $("#picture").attr('href', response.data.path);
                self.path = response.data.path;
            }).catch(function(error){
            });
        }
      }
    })
  </script>
</body>
</html>

ボタンとリンクを追加しました。

ボタンをクリックすると、onShutter処理が実行され、ラズパイ側にPOSTリクエストを送信します。

そのレスポンスから画像のファイルパスを取得し、リンクに反映させます。

同じ仕組みで動画の撮影もできそう。

次回やります。