WallStudio

技術ブログや創作ブログに届かない雑記です

PythonとWebRTCで画処理のウェブアプリを作る

f:id:yukawallstudio:20181020031909j:plain

ウェブアプリって気軽に使えて便利ですよね(・ω・)

動機

夏コミで「かみしば」という同人誌をボイロが呼んでくれるアプリAndroid)をリリースしました。

つづいでiOSへの移植を…なんて思ったのですが、開発環境が貧弱すぎるせいなのか必須ライブラリが使えなくて…ってことがありました。

wallstudio.hateblo.jp

それで「どうしようかなー」と考えていると、Android内でも機種による差異によって映らないという障害報告がありました。

wallstudio.hateblo.jp

カメラやコンパスなどのハードセンサーを使うアプリの難しさを改めて感じさせられます。だからこそ独自性が得られやすいのでしょうけど。

どの道、iOSでは完成の兆しが全く見えないのでもういっそのことサーバーで処理しちゃおう!ってことで所謂クラウドの開発を始めました。

アプローチ

  1. スマートフォンブラウザ上でビデオカメラを起動

    WebRTCのAPIを利用。と言ってもgetUserMediaを呼ぶと、カメラの視界をビデオとして再生してくれるという程度のものでプログラマーの負担は殆どありません。ここまでシンプルになるまで抽象化してくれてるので、よっぽどはぐれモノの端末でない限り互換性が期待できます。iOSももちろんiOS11から対応しているそうです。(が、私のiPodtouchG5はiOS9(´;ω;`))

    今回は以前からずっと気になっていたTypeScriptを使ってみました。

  2. 撮影したフレームをそっくりそのままサーバーに送信

    画像をPOSTで送って、インデックスのjsonが返ってくるREST風 APIです。Advanced REST clientというChrome拡張を見つけて使ってみたのですが、かなり便利、オヌヌメです!

    画像処理と言えば、Python。せっかくなのでPythonのWebフレームワークDjangoを勉強がてら使ってみました。

  3. 結果(何ページ目の画像か)をレスポンスとして送信

    レスポンス内のインデックス情報を元にmp3をSeek&Play&Pauseです。

サーバーサイド開発

Djangoはそれ自体がPythonで書かれたPython用のWEBフレームワークです。PythonのWEBフレームワークとしては最もメジャーなもののひとつらしいですが、Django自体はNodejsやRuby on railsほど人気でもなさそうな印象を受けました。(Pythonは人気なのにね)

MVCで言うところのControlっぽいところがViewだったりViewっぽいところがTemplateだったり、言葉で惑わしにかかってくる部分はありましたが、任意の処理をさせるまでには結構素早く達っせました。公式のチュートリアルが結構わかりやすいです。

はじめての Django アプリ作成、その 1 | Django documentation | Django

ざっくりとした流れは

# djangoのインストール
> pip install django 
# django-adminコマンドはdjangoと一緒に入ってくる
> django-admin startproject project_name 
# プロジェクト内にいくつか小さいアプリをポコポコ作っていくスタイル
> django-admin startapp app_name 

ビュー(コントロール)を一つ作るごとに以下の2か所の編集が必要です。関数ベースにする方式とクラスベースにする方式があります。クラスが推奨されていますが、APIとかならテンプレートもいらないし、関数ベースの方が読みやすいかもしれません。

# app_name/views.py

def Index(request: HttpRequest) -> HttpResponse:
    # GETパラメータ(ディレクトリ表現を推奨)は引数でもとれる

    post_data = request.POST['key']
    form_file = request.FILE['key']
    # requestを元にHTMLやjsonを作る
    return HttpRequest(html)
    # return JsonRsponce({hoge: ’huga’, piyo: 5})

# Template(拡張htmlみたいなの)がクライアントに返る
class IndexView(TemplateView):
    template_name = 'narrator/app.html'
# app_name/urls.py

urlpatterns = [
    path('/index', views.index, name='index'),
    path('/index-view', views.Index.as_views(),name='index_view'),
]

このビュー関数の中で自由自在にOpenCVを呼べるのがいいですよね!

# 送信された画像をOpenCV用の形式(ndarray)に変換
img = np.array(Image.open(request.FILES['image'])) 

ただやっぱり型がゆるゆるなので疲れます。

クライアントサイド開発

TypeScript環境

Nodejsは使いませんが、パッケージマネージャとしてNodejsのもの、npmを使うのが一般的みたいです。VisualStudioInstallerの方がいいかなとも思ったのですが、脱Windowsを迫られている身なのでクロプラなのを使っていこうとしています。

nodejs.org

# TypeScriptのコンパイラtscだけではブラウザが
# 名前解決できるところまでやってくれないので
# webpackとの合わせ技です(なんでだよ💢)
> npm install typescript ts-loader webpack webpack-cli webpack-dev-server --save-dev

qiita.com

TypeScriptを書く

class PgdetRetval{
    public id:string;
    public index: number;

    private static prevPackage: string = "";
    private static prevIndex: number = -1;

    public constructor(jsonObj: any){
        if("id" in jsonObj && "index" in jsonObj && "score" in jsonObj && "cross" in jsonObj && "timing" in jsonObj){
            this.id = jsonObj.id;
            this.index = jsonObj.index;
        }else{
            throw `Server internal error! ${jsonObj.toString()}`;
        }
    }

    public past(pack:string, id:HTMLSpanElement, index:HTMLSpanElement,
        score:HTMLSpanElement, cross:HTMLSpanElement, image:HTMLImageElement, audio:HTMLAudioElement){
        id.innerHTML = this.id;
        index.innerHTML = this.index.toString();
        
        let imageUrl:string;
        if(this.cross >= 5){
            imageUrl = image.src = `/static/narrator/loading.gif`;
        }else{    
            imageUrl = `/static/narrator/packages/${pack}/${("000" + this.index).slice(-3)}.jpg`;
        }
        if(image.src != imageUrl)
            image.src = imageUrl;

        if(this.cross >= 5){
            // 404を表示する
        }else{    
            PgdetRetval.prevPackage = pack;
            PgdetRetval.prevIndex = this.index;
        }
    }
}

う~ん、C#流石MS製ですね。中核のアーキテクトもC#と同じらしいです。いいですね!

ただし、いくらC#の皮をかぶっても所詮はJavaScriptです。次のようにしたとき、コールバックの方はthisがコールバック呼び出し元になってしまうのでメンバを解決できません。

class Takahashi{
    private id: number;
    public constructor(id:number){
        this.id = id;
        # getIdが評価される時にはthisが書き変わっている!
        setInterval(getId, 100);
    }
    public getId(): number{
        return this.id;
    }
}

JavaScriptのthisはそのコンテクストでの関数なのでどうしようもなかったのかもしれませんね。ただ圧倒的にJavaScriptより書きやすいのは間違いないです。(型定義ファイルとか、準備が面倒くさいけど;要勉強)

WebRTC

驚くほどなんてことないです。video.srcObject = stream; した瞬間には全てが終わっていて後はvideoに対する色々をするだけです。ただ一つだけ残念なのは、毎フレームのコールバックが無く、setIntervalで定期的に見るしかないという点ですかね。

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;

// ここでBindせずに this.frameCallBack を渡してしまうとthisがイベントの発火元になってしまう
let handle:CallPgdet = this;
navigator.getUserMedia({video: {width: 360, height: 480, frameRate: 2}, audio: false}, 
    this.frameCallBack.bind(handle),
    console.log
);
...
private frameCallBack(stream: MediaStream){
    this.video.srcObject = stream;
    this.video.play();

    this.task = setInterval(()=>{
        console.log(this.frameCount);
        // カメラが慣れるまでスキップ
        if(this.frameCount == 5)
            console.log("Camera maybe became stabled.");
        if(this.frameCount++ >= 5 && this.isPlay){
            this.callPgdet(this.video, this.canvas);
        }
    }, 500);
}

コンパイル

> npm run build

本番サーバーに展開

環境

  • Conoha VPS 512MB 1vCPU
  • CentOS 7
  • nignx
  • let's encrypt

Pythonの問題

PythonコマンドがPython2.xにaliasされていたので時々ハマりました…また、pipがyumで入りません。泣く泣くwgetで入れました。

> wget https://bootstrap.pypa.io/get-pip.py
> sudo python3.6 get-pip.py

stackoverflow.com

環境変数

DJANGO_SETTINGS_MODULE と PYTHONPATH の設定が必要です。Windowsでは普通に名前解決してくれたんですが…そもそもこんなの二つ以上プロジェクトがあったら詰むのでは?

export DJANGO_SETTINGS_MODULE=project_name.settings
export PYTHONPATH=project_name

HTTPサーバーとのローカル通信

nginx<->uwsgi<->django が推奨されているが、uwsgiの設定が面倒くさすぎてdjangoの中に入っているwsgiでNポートで待ち受け、nginxからリバースプロキシ(https->http)してます。

> python manage.py runserver 0.0.0.0:5050
# nginx.conf

server {
        listen 443;
        server_name kamishiba2.wallstudio.work;

        client_max_body_size 100M; # 画像なので大きめに
        # Let's Encrypt
        ssl_certificate      xxxxxxxxxxxxxxxxxxxxxxxx;
        ssl_certificate_key  xxxxxxxxxxxxxxxxxxxx;

        location /static {
            # 本当は開発用のstaticディレクトから別の
            # ところに実行時にエクスポするのが推奨
            # らしいけど面倒くさい
            alias {開発中に使ってたstaticディレクトリ};
            #root /static;
        }

        location / {

            proxy_pass http://localhost:5050;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Connection keep-alive;
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;
            #auth_basic "Makimaki";
            #auth_basic_user_file /etc/nginx/.htpasswd;
        }
    }

nshiba.hatenablog.com

完成品

精度も良好ですね!かなり思った通りに動いてくれています。

f:id:yukawallstudio:20181020031909j:plain

全然関係ないけど、マキマキのおなかえっちぃ

自分で言うのもなんだけど!

はぁマキマキ可愛いなぁ (*´Д`)

タカハシ

魔が差した