まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タイトルどおりの内容のエラーが発生したので、その原因と解決策を探った記録をブログにまとめました。

環境としては以下の図のようなものです。1台の Windows 10 PC の中に Docker Desktop を導入・起動し、MySQL サーバーのイメージからコンテナを作って 3306 番ポートで公開起動しました。この環境に WSL(2) の MySQL CLI を使って、localhost:3306 の MySQL サーバーにログインする、というだけの内容なのですが、ログイン時にコネクションエラーが発生してしまう、という症状が出ていました:
20210612


具体的に紹介します。今回使った MySQL のコンテナイメージはこちらです。厳密には mariadb を使っています。同様の現象が発生すると思うので、他の MySQL 系イメージでも構いません:
https://hub.docker.com/r/linuxserver/mariadb


Docker Desktop を使ってこのイメージからコンテナを作成します。WSL を起動するなどして docker コマンドが使える状態で以下のコマンドを実行します(目的は Docker Desktop 内のコンテナとして起動することなので、このコマンド自体は WSL から実行しなくても構いません):
$ docker run -d --name=mariadb -e PUID=1000 -e PGID=1000 -e MYSQL_ROOT_PASSWORD=root -e TZ=Asia/Tokyo -e MYSQL_DATABASE=mydb -e MYSQL_USER=user -e MYSQL_PASSWORD=P@ssw0rd -p 3306:3306 --restart unless-stopped linuxserver/mariadb

こんな感じで、Docker Desktop 内のコンテナとして MySQL が起動します:
2021061301


指定したオプションはコンテナイメージのドキュメントを参考に指定しています。上の例では root のパスワードは root 、利用ユーザーは user 、利用ユーザーのパスワードは P@ssw0rd 、利用データベース名を mydb、コンテナ名は mariadb、などを指定しています(適当に変更いただいても構いません)。ポート番号に -p 3306:3306 を指定しているので、ローカルホストの 3306 番ポートからも(公開されているので)アクセスできるはずです。事前準備はここまで。

ではこの方法で Docker Desktop 内に起動した MySQL(mariadb) に、WSL から MySQL CLI で接続してみます。上述のオプションであれば以下のコマンドで接続できるはずです:
$ mysql -u user -pP@ssw0rd -h localhost mydb

しかし実際には以下のようなエラーが表示されてしまいます:
$ mysql -u user -pP@ssw0rd -h localhost mydb
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2 "No such file or directory")

ユーザー名やパスワード、データベース名は間違っておらず、接続先も localhost だから間違えようがないはずです。ポート番号もデフォルトの 3306 のままなので特別なオプションも不要のはず・・・ ではなぜエラーになってしまうのでしょうか?


改めてエラーメッセージをよく見ると "Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'" と表示されています。ちなみにこのファイルは存在していません(/var/run/mysqld というディレクトリが存在していません):
$ ls -la /var/run/sqld/mysqld.sock
ls: cannot access '/var/run/sqld/mysqld.sock': No such file or directory

このエラーメッセージでわかる人もいると思うのですが、MySQL コマンドは localhost が対象の場合はデフォルトで「ソケット接続」という方法で接続を試みます。そしてこのソケット接続をする場合の情報を /var/run/sqld/mysqld.sock というファイルで管理しているため、このファイルを読み込もうとしているのでした。

しかし、今回の環境では WSL 内には MySQL サーバーは起動していません。WSL 内からの docker コマンドで起動してはいますが、実体は Windows にインストールされた Docker Desktop のコンテナとして起動しています。要は「WSL と同じマシン上で動いてはいて、ポートも公開されているけど、WSL から直接ソケット接続できる状態で動いていない」のです。これがエラーの原因で、エラーメッセージの理由でもあります。

では、エラーなしに WSL から MySQL に接続するにはどのようなコマンドにすればいいでしょうか? 答はシンプルで「ソケット接続ではなく、TCP 接続するようなオプションを指定」することで解決できます。具体的には以下のように --protocol=TCP オプションを付けて実行します:
$ mysql -u user -pP@ssw0rd --protocol=TCP -h localhost mydb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 11
Server version: 10.4.18-MariaDB-1:10.4.18+maria~bionic-log mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [mydb]>

今度は無事に接続できました。「localhost の罠」にかかった時の話でした。


最初にお断りを。今回紹介するサービスはソースコードを提供していますが、実際にブラウザから使える便利なサービスの形では提供していません(理由は後述します)。実際に試すには環境を準備の上、ソースコードを各自の PC で実際に動かしていただく必要があります。ご了承ください。なおこちらでの動作確認は Windows10 + WSL2 + Ubuntu 18.04(20.04) の Python 及び Node.js で行っています。


今回のブログエントリで紹介するのは「画風変換」サービスです。サービスといってもウェブで公開しているわけではなく、公開しているソースコードを元に自分の PC で動かす必要があるので、実際に変換するまでのハードルは少し高めの内容です。


【画風変換とは?】
耳慣れない言葉だと思います。画風変換そのものは PFN のこのページで紹介されている内容がわかりやすいので、まずはこちらを一度参照ください(今回紹介するサービスも内部的にここで紹介されているツールを使っています):
画風を変換するアルゴリズム


ニューラルネットを使って実装されたアルゴリズムの論文を Python で実装した様子が紹介されています。誤解を恐れずに簡単な説明をすると、2枚の画像を用意して、うち1枚(コンテンツ画像)に写っている物体の形や配置をそのままに、もう1枚(スタイル画像)の色使いやタッチの特徴といった「画風」を適用するように変換して新しい画像を生成するアルゴリズムです:
2021060902


上述のページには同じ猫のコンテンツ画像に様々なスタイル画像を適用して画風変換した例が複数紹介されています:
2021060903


ここで紹介されている画風変換を実現するためのツール : chainer-gogh はソースコードが公開されています:
https://github.com/mattya/chainer-gogh


【「自分のお絵描きで画風変換する」とは?】
今回のブログエントリで紹介するのは「自分のお絵描きをスタイル画像にして画風変換するサービス」です。私が開発&公開しているお絵描きサービス MyDoodles を上述の chainer-gogh と組み合わせて使えるようにしたものです。具体的には画風変換には2枚の画像が必要ですが、コンテンツ画像は自分でアップロードした画像(写真を想定しています)、そしてスタイル画像は MyDoodles を使って描いたお絵描き画像を指定して画風変換する、というものです。画風変換はコンテンツ画像に写真、スタイル画像に絵を指定すると不思議な感じの絵が生成されることが多いのですが、このスタイル画像として自分のお絵描きを適用して画風変換を実現できるようにしたものです。


【ツールをウェブサービスとして公開しない理由】
理想を言えば、このツールをウェブで公開して誰でも気軽に使えるようにしたかったのですが、今回それは諦めました。後述で紹介する方法を実践して、自分の PC で試しに動かしてみるとわかると思うのですが、この画風変換は CPU に非常に大きな負荷をかける処理で、画風変換が完了するまでにかかる時間は、動かす PC のスペックにもよりますが、かなりいいスペックの PC でも1つの処理に対して1時間程度は覚悟※しておく必要があります。

※ GPU を搭載した機種であれば GPU を使って処理すればかなり高速化できると思いますが、それでも数10分間は負荷 100% の状態が続くと思います。

要はそこそこの PC を用意して1つの処理に集中させても1時間程度は負荷 100% が続くような処理なのです。これをウェブで公開して、複数の処理を同時に・・・となった場合の負荷や処理時間が想像できませんが、間違いなくタイムアウトとの戦いになるはずです。 こんな事情もあって、今回はウェブでの公開を諦め、「自分の PC で自分用の画像変換専門に行う」ツールとして公開することにしたのでした。


【ツールのセットアップ】
では実際に画風変換を行うにあたっての、事前セットアップの手順を紹介します。大きく chainer-gogh を動かすためのセットアップと、ウェブのフロントエンドアプリを動かすためのセットアップの2つに分かれていて、手順としては(0)事前準備、(1)フロントエンドアプリのソースコードを用意、(2)chainer-gogh のソースコードを用意、(3)chainer-gogh のセットアップ、(4)フロントエンドアプリのセットアップ、(5)コンテンツ画像とスタイル画像(お絵描き画像)の準備、の順に行う必要があります。では以下で順に紹介します。

(0)事前準備
このサービス・ツールを動作させるために以下のプログラミング言語実行環境ランタイムが必要です:
- Python 3.x
- Node.js 12.x

自分の PC 環境でこれらのツールが使えるよう、事前にインストールしておいてください。

(1)フロントエンドアプリのソースコードを用意
今回のツールは以下のようなフォルダ構成となります:
|- chainer-gogh-webapp/
    |- chainer-gogh/ (chainer-gogh のソースコード)
    |
    |- webapp/       (フロントエンドアプリのソースコード)
    |

chainer-gogh のソースコードは手順(2)で用意することになるので、まずは chainer-gogh 以外のソースコード構成を用意します。

git clone または zip ダウンロードで、以下のソースコードを手元に用意してください:
https://github.com/dotnsf/chainer-gogh-webapp

正しく展開できると、以下のようなフォルダ構成が再現できます:
|- chainer-gogh-webapp/
    |- webapp/       (フロントエンドアプリのソースコード)
    |

次に残った chainer-gogh のソースコードを用意します。

(2)chainer-gogh のソースコードを用意

(1)で用意した chainer-gogh-webapp/ フォルダに移動してから以下を git clone するなどして、chainer-gogh-webapp/chainer-gogh/ を用意します:
https://github.com/mattya/chainer-gogh


これで以下のようなフォルダ構成になっているはずです:
|- chainer-gogh-webapp/
    |- chainer-gogh/ (chainer-gogh のソースコード)
    |
    |- webapp/       (フロントエンドアプリのソースコード)
    |

(3)chainer-gogh のセットアップ

では早速 chainer-gogh を動かして・・・と言いたい所ですが、実は chainer-gogh の公開ソースコードは少し古い所があり、最新の Python3 環境で動かすには少し変更を加える必要があります。具体的には以下2ファイルで5箇所を書き換えてください( - で始まる行を + で始まる行の内容に変更してください):
(chainer-gogh.py)

(13行目)
   13 : - from chainer.functions import caffe
   13 : + from chainer.links import caffe

(100, 101 行目)
  100 : - mid_orig = nn.forward(Variable(img_orig, volatile=True))
  101 : - style_mats = [get_matrix(y) for y in nn.forward(Variable(img_style, volatile=True))]
  100 : + with chainer.using_config("enable_backprop", False):
  101 : +     mid_orig = nn.forward(Variable(img_orig))
  102 : + with chainer.using_config("enable_backprop", False):
  103 : +     style_mats = [get_matrix(y) for y in nn.forward(Variable(img_style))]

(models.py) (5 行目) 5 : - from chainer.functions import caffe 5 : + from chainer.links import caffe (22 行目) 22 : - x3 = F.relu(getattr(self.model,"conv4-1024")(F.dropout(F.average_pooling_2d(F.relu(y3), 3, stride=2), train=False))) 22 : + x3 = F.relu(getattr(self.model,"conv4-1024")(F.dropout(F.average_pooling_2d(F.relu(y3), 3, stride=2)))) (74 行目) 74 : - y6 = self.model.conv6_4(F.relu(F.dropout(self.model.conv6_3(F.relu(self.model.conv6_2(F.relu(self.model.conv6_1(x5))))), train=False))) 74 : + y6 = self.model.conv6_4(F.relu(F.dropout(self.model.conv6_3(F.relu(self.model.conv6_2(F.relu(self.model.conv6_1(x5))))))))

ソースコードの変更が完了したら Python3 のライブラリを pip でインストールします(Python3 のインストール方法によっては "pip" の部分を "pip3" にする必要があるかもしれません):
$ cd chainer-gogh-webapp/chainer-gogh

$ pip install typing_extensions

$ pip install pillow

$ pip install chainer

chainer-gogh の準備の最後に学習済みモデルを用意します。(ダウンロードサイズと実行時の負荷が比較的小さいという意味で)気軽に使える nin(Network in Network) Imagenet Model をダウンロードします:
https://gist.github.com/mavenlin/d802a5849de39225bcc6

2021060901

上記 URL から↑のリンクをクリックして nin_imagenet.caffemodel というファイルをダウンロードして(30MB ほど)、chainer-gogh/ フォルダに保存します。


(4)フロントエンドアプリのセットアップ

続けて Node.js 側のセットアップを行います。
$ cd chainer-gogh-webapp/webapp

$ npm install

(5)コンテンツ画像とスタイル画像(お絵描き画像)の準備

最後に画風変換を行う際のコンテンツ画像とスタイル画像を準備します。コンテンツ画像は画像ファイルであればなんでもいいのですが、絵よりは写真の画像を用意することをおすすめします。今回は「フリー素材アイドル」である MIKA☆RIKA さんの、以下の画像を使わせていただくことにします:
mikarika


次にスタイル画像となるお絵描き画像を用意します。一応サンプル的なものは用意されているので自分でお絵描きをする必要はないのですが、自分のお絵描きがスタイル画像となって画風変換することが楽しいと思っているので、是非挑戦してみてください(笑)。


PC ブラウザかスマホブラウザを使って(スマホがおすすめ)以下の URL にアクセスしてください:
https://mydoodles.mybluemix.net/
2021060904


指でお絵描きができるサービスです。色や線の太さ、背景色を指定して描いたり、アンドゥ・リドゥ・リセット程度のリタッチが可能です。PC ブラウザから利用している場合は指の代わりにマウスで描いてください。最後に「保存」ボタンで保存します。なお画風変換のスタイル画像として利用する場合は背景色の指定をしておいたほうが変換後のイメージがスタイルに近づくと思っています(背景が不要の場合でも背景色に白を明示的に指定して描いてください。透明なままだとスタイル変換時に黒くなってしまうようなので):
2021060906


自分が描いた画像はトップページの「履歴」ボタンから参照可能です:
2021060904


スタイルとして使いたい画像を1つ選んで表示します:
2021060905


この時の URL を参照します。URL の最後に /doodle/xxxxxxxxxx という部分があるはずなので、この /doodle/ に続く最後のパラメータの値(xxxxxxxxxx)が後で必要になるのでメモするなどしておいてください:
2021060907


これで画風変換を行うための準備が整いました。


【画風変換】
では実際に画風変換してみましょう。まずは webapp フォルダでフロントエンドアプリケーションを起動します:
$ cd chainer-gogh-webapp/webapp

$ node app

これがデフォルトの起動方法ですが、この方法だと 'python' というコマンドで Python3 が(内部的に)実行されます。セットアップ手順によっては Python3 コマンドが 'python3' というコマンド名になっていることもありますが、その場合は node 実行時の環境変数 python で Python3 が実行されるコマンド(この例だと python3)を指定しながら起動してください:
$ cd chainer-gogh-webapp/webapp

$ python=python3 node app

フロントエンドアプリケーションの起動に成功すると 8080 番ポートで待ち受けるので、PC のウェブブラウザで http://localhost:8080/ にアクセスします:
2021060901


この画面中央に3列からなる表示エリアがあります。一番左に画風変換を行うコンテンツ画像、真ん中がスタイル画像、そして右に画風変換の結果が表示されることになります。デフォルトでスタイル画像が表示されていますが、これは僕が描いたもので、これをそのままスタイル画像として使うこともできます。が、せっかくなので自分で描いた画像に変えてから画風変換を実行していただくことを想定しています&おすすめします:
https://mydoodles.mybluemix.net/doodle/0175dcf0-c045-11eb-8c4e-8de1dabb1b17
2021060902


スタイル画像を変更にするには、MyDoodles で描いたお絵描きの URL 最後のパラメータの値(上述例だと 0175dcf0-c045-11eb-8c4e-8de1dabb1b17)を sub と書かれた下のフィールドにコピー&ペーストしてください。

例えば、このお絵描きをスタイル画像に指定する場合であれば、パラメータは 63e083b0-c345-11eb-8c4e-8de1dabb1b17 なので、sub の下のフィールドに 63e083b0-c345-11eb-8c4e-8de1dabb1b17 と入力します:
2021060903


するとこのようにフロントエンドのスタイル画像が指定したお絵描きに切り替わります:
2021060904


次にコンテンツ画像を指定します。こちらは直接アップロードする必要があるので、main と書かれた下のボタンをクリックして、自分の PC からコンテンツ画像として使いたい画像ファイルを指定します。今回は上述の MIKA☆RIKA さんの画像を使うので、指定するとこのような画面になります:
2021060905


コンテンツ画像とスタイル画像の両方がプレビューされている状態になれば画風変換を実行できます。result と書かれた下にある「RESULT」ボタンをクリックして画風変換がスタートします。

PCのスペックにもよりますが、画風変換には非常に長い時間がかかります。result 欄にはその画風変換の途中経過が少しずつ表示されていきます:
2021060901
  ↓
2021060902
  ↓
2021060903


またこの途中経過画像は chainer-gogh-webapp/chainer-gogh/output_dir/(指定したパラメータ名)/ というフォルダの中に随時作成されていきます。このフォルダの中を見ていると、元のコンテンツ画像にスタイル画像の画風が少しずつ適用されて、輪郭がはっきりしていく様子が確認できます。これはこれで見ていて面白いです:
2021060904



画像は im_00000.png, im_00050.png, im_00100.png, im_00150.png, .. と "50" 刻みで増えていき、im_04950.png まで 100 段階 100 枚が作成された段階で(このアプリケーションでの)画風変換は終了となります:
2021060905


今回のコンテンツ画像とスタイル画像での画風変換は最終的にこのような結果(右)になりました。適当にお絵描きした○ールおじさんの画風(?)が MIKA☆RIKA さんの写真に適用できていますよね。元の画像やお絵描きを指定している点で人手は入っていますが、これは「人工知能が描いた世界に一枚の絵」という表現ができるものです:
20210609_mikarika_0.5


同じコンテンツ画像で、スタイル画像を替えて実行した結果はこんな感じでした。スタイル画像の違いが結果の違いにあらわれている様子が確認できると思います:
20210608_mikarika_0.5


ちなみにこの画風変換にかかる処理時間(最後の画像が作られるまでの時間)ですが、Intel i7-7500 2.7GHz の4コア CPU でメモリを 16GB 搭載した Windows 機で他の処理を何もせずに実行した場合でおよそ1時間といったところでした。環境によってかかる時間は前後すると思いますが、1つの参考になれば。

この処理時間を短縮するため、GPU 搭載機種であれば GPU を使って高速に処理できるよう改良中です。Windows から直接実行する場合だとうまくいくかもしれないのですが、WSL を挟んで実行しようとするとなかなかうまくいかず・・・また GPU を使うのであれば利用モデルも別のものにしたほうがいいと思われる、など、こちらは現時点ではまだまだ検討段階含めて未実装ということで。


ともあれ、いちおう動く状態のフロントエンド付きサービスにはなっていると思います。環境構築の手間がちと大変かもしれませんが、ぜひ自分(やご家族)でお絵描きした結果を、自分で撮った写真のスタイルに適用するなどして楽しんでいただければと思っています。


スマホのカメラを HTML ページから起動するには、以下のようなタグを用意する方法があります:
<input type="file" accept="image/*" capture="camera"/>


この方法でボタンからカメラを起動すると、カメラで撮影した画像をこのタグの value として引き渡すことができます。「カメラで画像を撮影してアップロード」する方法としては非常に簡単です。

ただこの方法には1つ大きな難点があります。この方法で起動したカメラは HTML による装飾ができず、常にスマホの全画面を使って写真撮影することになります。つまり「撮影中は決まった UI しか使えない」のです。

具体的には以下のようになります(iOS 14 の場合)。<input> タグ自体は CSS で見た目を変えることができるため、例えばこのような画面を用意して、カメラアイコンをタップするとカメラを起動、させるように作ることができます。この「カメラを起動する前」までの UI は自由にデザイン可能です:
2021060801


しかし実際にカメラを起動するとこのような画面になります。全画面でカメラが有効になり、撮影ボタンを押すことで、その瞬間のカメラ映像を記録できます。ただ「撮影ボタンを押す」というアクションを避けることはできません(画面内に QR コードが認識したら自動的に・・・といったことはできません):
2021060802


撮影すると一度この確認画面になり、「写真を使用」を選択することで処理を続けることができるようになります(再撮影した場合は再度撮影画面を経てこの画面になります):
2021060803


カメラ画面を終了すると元の画面に戻ります。ここからはまた自由に見た目を調整することができ、また既に撮影済みの画像のデータを取得することもできるので(File API などと組み合わせることで)プレビュー表示したり、ここから QR コードを読み取って・・・といったこともできるようになります:
2021060804


といった具合です。撮影後は元の画面に戻るので撮影の前後ではデザイン装飾されたページを参照できるのですが、撮影時だけは(撮影し終わるか、キャンセルするまでは)どうにもできません。シャッターを押す、というプロセスを変えることができない UI なので、シャッターを押すことなく画面内に QR コードを確認したら内容を読み取って次の処理へ・・・といったカスタマイズはできないことを意味しています。簡単に実装できる一方で、この撮影前後でのインターフェースにかなり大きな制約を受けることになります。

今回、なんとかして HTML 画面内で上述のようなこと(シャッターを押すことなく画面内に QR コードを確認したら内容を読み取って次の処理)を実現できないかと考え、一応見た目的にはできそうな目処がたったのでサンプルと合わせて紹介します。


まず、サンプルアプリケーションはこちらで試すことができます。ウェブカメラ付きの PC ブラウザか、カメラ付きスマホのブラウザでこちらにアクセスしてみてください(以下の画像は iOS 14 の Safari ブラウザを使った場合の例です):
https://dotnsf.github.io/html_camera_inside/


まず以下のようなダイアログが表示されてカメラへのアクセス許可を求められる(今回の方法だとこの確認ダイアログを回避することはできないと思います)ので「許可」してください:
2021060805


(詳しくは後述しますが)PC ブラウザの場合はウェブカメラ(つまりフロントカメラ)、スマホの場合は背面カメラが有効になり、カメラが映すストリーム映像がウェブ画面上部に表示され続けます。この画面は HTML および CSS で装飾されているもので、自由にカスタマイズできます(上述の「決まったUIしか使えない」ものとは異なります)。今回の例では HTML 画面内に背面カメラの映像を映し続けるようにしています(この画面は HTML で作られています):
2021060806



実際のアプリケーションではここに「撮影」ボタンなどを用意するなどして、ある瞬間の映像を切り取って画像化したり、その画像をサーバーにアップロードすることも可能です。今回のサンプルではボタンで撮影するわけではなく、映像を常に監視しており、映像内に QR コードが認識されたタイミングでその QR コードデータの内容を表示するようにしています。適当な QR コードを探して、スマホや PC ブラウザの映像に映るようにして、動作を確認してみてください:
2021060807



以上がサンプルアプリケーションの紹介です。以下はソースコードの解説です。


このサンプルアプリケーションのソースコードをこちらで公開しています:
https://github.com/dotnsf/html_camera_inside


実質的なコードは index.html 1つだけです。このコードの中に以下のような記載があります:
  :
  :
  //. video
  video = document.createElement( 'video' );
  video.id = 'video';
  video.width = cameraSize.w;
  video.height = cameraSize.h;
  video.setAttribute( 'autoplay', true ); 
  video.setAttribute( 'muted', '' ); 
  video.setAttribute( 'playsinline', '' );
  document.getElementById( 'videoPreview' ).appendChild( video );

  //. media
  var data = {
    audio: false,
    video: {
      facingMode: 'user', //. front 
      width: { ideal: resolution.w },
      height: { ideal: resolution.h }
    }
  };
  if( isMobile() ){
    data.video.facingMode = 'environment'; //. back
  }
  media = navigator.mediaDevices.getUserMedia( data ).then( function( stream ){
    video.srcObject = stream;
  }).then( function( err ){
  });
  :
  :
<body>
  <!-- video(visible) -->
  <div class="container" id="videoPreview" style="text-align: center;">
    <h4>Video Preview</h4>
  </div>
  :
  :


この部分で「カメラを有効にして <body> 内に <video> 要素を生成して撮影した様子を表示」しています。以下2つに分けて説明します。

まずは青字部分、ここで <video> 要素を動的に生成して、<div id="videoPreview"></div> 内に追加しています。ここまではそんなに難しくないと思ってます。

そして赤字部分です。ここで MediaDevices.getUserMedia() メソッドを使ってカメラを有効にしています。その際に PC ブラウザではフロントカメラを、スマホでは背面カメラを有効にする必要があるのですが、getUserMedia() メソッドの引数となるオブジェクトの video.facingMode 属性を 'user'(フロント)にするか、'environment'(背面)にするかで切り替えています。getUserMedia() メソッドが成功したら、その結果を上述の <video> 属性の srcObject に指定することでカメラで撮影し続ける結果を <video> 要素内に表示することができるようになります。

なお、getUserMedia() メソッドは https でアクセスしている場合のみ利用することができます。したがって同様のアプリケーションを作る場合も https で利用できるサイトにページを設置する必要がある点に注意してください。

これだけでカメラで撮影しつつ、その映像を HTML ページ内に表示する、という所までは実現できます。ただ <video> タグのままだとこの先のデータ取り出しなどが必要になった場合に不便です。そこで <video> タグ内に表示される映像を(コマ送りで)<canvas> に表示するように処理を追加して、<canvas> の画像データを監視したり取り出して処理できるように改良しています(加えて <canvas> 自体は非表示の <div> 内に生成することで画面的には変化がないようにしています):
  :
  :
  //. canvas
  canvas = document.createElement( 'canvas' );
  canvas.id = 'canvas';
  canvas.width = canvasSize.w;
  canvas.height = canvasSize.h;
  document.getElementById( 'canvasPreview' ).appendChild( canvas );

  //. context
  ctx = canvas.getContext( '2d' );

  //. render video stream into canvas
  _canvasUpdate();
  :
  :

//. render video stream into canvas
function _canvasUpdate(){
  //. video to canvas(animation)
  ctx.drawImage( video, 0, 0, canvas.width, canvas.height );

  //. check QR code
  var img = ctx.getImageData( 0, 0, canvas.width, canvas.height );
  var result = jsQR( img.data, img.width, img.height );
  if( result && result.data ){ 
    alert( result.data ); 
  }else{
    requestAnimationFrame( _canvasUpdate );
  }
};

  :
  :

  <!-- canvas(invisible) -->
  <div  style="text-align: center; display: none;" id="canvasPreview" class="container">
    <h4>Canvas Preview</h4>
  </div>


上述の _canvasUpdate() メソッドでは <video> の映像を(コマ送りになるよう)切り出して ctx.drawImage() メソッドで <canvas> に転写しています。転写後に <canvas> から画像データを取り出し、 QR コードライブラリ : jsQR を使ってデコードします。デコードが成功していたら(画面内に QR コードが映っていたと判断して)そのデコード結果を alert() 表示します。デコードが成功しなかった場合は QR コードは映っていなかったことになるので、requestAnimationFrame メソッドを使って再度 _canvasUpdate() メソッドを呼び出します※。これを繰り返すことで <canvas> にも(不自然なコマ送りにならずに)自然な映像を表示することが実現できています。

※<video> と <canvas> を連動させるこの技術は他でも応用できそうな印象です。


なお上述の青字部分の、特に以下の部分(青太字の3行)についてコメントを加えておきます:
  video.id = 'video';
  video.width = cameraSize.w;
  video.height = cameraSize.h;
  video.setAttribute( 'autoplay', true ); 
  video.setAttribute( 'muted', '' ); 
  video.setAttribute( 'playsinline', '' );
  document.getElementById( 'videoPreview' ).appendChild( video );
  :
  :


ここは処理的には video.autoplay = true; という属性が設定されていれば正しく動くはずの部分です。ところが原因はよくわからないのですが、iOS ではこれ以外に muted と playsinline という2つの属性についてもなんらかの設定がされている必要があり、しかもこれらを setAttribute() メソッドを使って設定しないと正しく動かない、という現象が発生している模様です(実際に setAttribute() を使わずに直接設定して動かすと、最初だけ <video> 内に映像は描画されますが、それ以降は映像が更新されなくなってしまいました。そのため <canvas> への映像更新も止まってしまいました)。その対応のため現状のようなコードになっています。詳しくはこちらも参照ください:
Navigator.mediaDevices.getUserMedia not working on iOS 12 Safari



というわけで、色々苦労しつつもなんとか HTML & CSS で装飾可能なカメラ撮影画面をウェブページでも実現することができそうな目処が立ちました。「カメラを起動する」ための便利なタグを使うのではなく、どちらかというと動画撮影の機能を有効にした上で、ある瞬間を切り取って撮影したことにする、というアプローチで実現しています。具体的な UI の実現方法はここで紹介した方法以外にもあると思うので、このサンプルを参考にしつつも色々挑戦してみてください。



このページのトップヘ