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

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

2021/06

最初にお断りを。今回紹介するサービスはソースコードを提供していますが、実際にブラウザから使える便利なサービスの形では提供していません(理由は後述します)。実際に試すには環境を準備の上、ソースコードを各自の 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 の実現方法はここで紹介した方法以外にもあると思うので、このサンプルを参考にしつつも色々挑戦してみてください。



IBM Cloud から提供されているユーザーディレクトリ管理サービスである App ID の UI カスタマイズに挑戦しています。自分的にはまだ道半ばではありますが、途中経過という意味でいったん公開・紹介します。


【IBM Cloud AppID とは?】
IBM Cloud App ID サービス(以下「App ID」)はアプリケーションで利用するユーザーディレクトリを管理するサービスです。オンラインサインアップを含めたユーザー管理機能を持ち、アプリケーションにログイン機能を付加させたい場合に非常に簡単にログイン機能を実装できるようになります。IBM Cloud の(無料の)ライトアカウントを持っていれば(2要素認証など、一部機能が使えなかったり、1日の実行回数などに制限もありますが)無料のライトプランで利用することも可能です:
2021052900



【IBM Cloud AppID のカスタマイズに挑戦した背景】
App ID はユーザー管理機能が必要なアプリケーション開発を行う上で非常に便利な一方、UIのカスタマイズに関する情報が少なく(管理メニューに用意されているのは、ログイン画面内にロゴ画像を貼り付けることができる程度)、ほぼあらかじめ用意された(英語メインの)画面を利用する必要がありました。

機能的には満足なのですが、この UI カスタマイズがどの程度厄介なものなのかを調べる意味も含めて、AppID の UI カスタマイズに挑戦してみました。結論として 2021/06/02 のこのブログエントリ公開時点では 100% の実装ができているわけではないのですが、ログイン画面およびパスワードリセット画面のカスタマイズには成功しているため、ここでいったん公開することにしました。


【IBM Cloud AppID のカスタマイズサンプル】
AppID UI カスタマイズを使ったサンプルアプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/appid_fullcustom


サンプルアプリケーションを利用するには IBM Cloud にログインして、App ID サービスのインスタンスを作成し、サービス資格情報を作成・参照する必要があります:
2021060201


そして以下の値を確認します:
{
  "apikey": "*****",
  "appidServiceEndpoint": "https://us-south.appid.cloud.ibm.com",
  "clientId": "*****",
  "secret": "*****",
  "tenantId": "*****",
    :
    :
}

これらの値をソースコード内 settings.js のそれぞれの変数に代入して保存します:
//. IBM App ID
exports.region = 'us-south';
exports.tenantId = '*****';
exports.apiKey = '*****';
exports.secret = '*****';
exports.clientId = '*****';

exports.redirectUri = 'http://localhost:8080/appid/callback';
exports.oauthServerUrl = 'https://' + exports.region + '.appid.cloud.ibm.com/oauth/v4/' + exports.tenantId;


※exports.region の値は appidServiceEndpoint の値の "https://" と ".appid.cloud.ibm.com" の間の文字列です。それ以外はサービス資格情報にかかれている値をそのまま記載します。

また exports.redirectUri の値はアプリケーションの OAuth ログインのリダイレクト先 URL を記載します。このサンプルアプリケーションでは 8080 番ポートで待ち受けて /appid/callback にリダイレクトする想定で作られているのでこのような値になりますが、実際にパブリックなインターネットで利用する場合はこの値を実際の URL 値に書き換えてください。

準備の最後に AppID サービスのリダイレクト URI にここで指定した export.redirectUri の値と同じものを登録します。サービスの「認証の管理」メニューから「認証設定」タブを選択し、「Web リダイレクト URL の登録」欄に export.redirectUri に指定した値と同じものを追加してください。これで準備は完了です:
2021060202


このサンプルアプリケーションを実際に動かしてみましょう。まずは npm install して node app で起動します:
$ cd appid_fullcustom

$ npm install

$ node app

8080 番ポートで待ち受ける形で起動するので、ウェブブラウザで http://localhost:8080/ にアクセスします。正しく動作すると http://localhost:8080/login にリダイレクトされ、以下のようなシンプルなログイン画面になります(この画面はカスタムUIで作った画面です):
2021060203


AppID サービスに登録した ID とパスワードを入力して "Login" します:
2021060204


ログインに成功すると、そのユーザーの名前などが表示される画面になります。この画面で "logout" すると元のログイン画面に戻ります:
2021060205


ログイン画面下部に2つのリンクがあります。「オンラインサインアップ」と書かれたリンクをクリックすると、オンラインサインアップ用の画面に切り替わります(この画面もカスタム UI です):
2021060201


ここでメールアドレスなどを入力してサインアップ可能です:
2021060202


サインアップしてしばらく待つと、メールアドレスの有効性確認を行う必要があるため、指定したメールアドレスにメールが届きます(なお、自分の環境では「迷惑メール」扱いで届きました(苦笑))。メールを開いてリンクをクリックし、有効性確認処理をしてください:
2021060203


メールアドレスの有効性が確認されるとオンラインサインアップが完了したとみなされ、これ以降はメールアドレスとサインアップ時に指定したパスワードでログインできるようになります:
2021060203


ログイン画面下部のもう1つのリンク「パスワードを忘れた場合」はパスワードリセット用のリンクです:
2021060204


こちらをクリックするとパスワードを忘れてしまったメールアドレスを指定する画面(これもカスタムUIです)が表示されるので、ここにパスワードを忘れたアカウントのメールアドレスを指定します:
2021060205


すると指定したメールアドレスにパスワードリセット用の URL が書かれたメールが届きます。このメールを開いて、URL にアクセスします:
2021060206


すると新しいパスワードを入力する画面(この画面だけはカスタムUIではなく、あらかじめ用意されたものです)が表示されるので、新しいパスワードを入力します:
2021060207


これで新しいパスワードを使って再びログインが可能になります:
2021060203


念の為、カスタマイズ無しの場合の AppID の各種画面UIを以下に紹介しておきます。(Your Logo Here) と書かれた箇所にロゴ画像を貼り付けることは可能ですが、それ以外のカスタマイズをしようとすると今回紹介したサンプルのような方法を取る必要があります。その代わり、これらの画面を使う場合は非常に簡単に実装することができるものです。

(ログイン画面)
2021060301

(パスワード忘れ)
2021060302

(オンラインサインアップ)
2021060303



【まとめ】
以上、AppID を独自のカスタム UI で利用する手順を紹介しました。一連の手順の中でパスワードリセット時の新パスワードを指定する画面だけは元の(英語の)UIになってしまっていますが、それ以外はすべて独自の UI で実現できているのがおわかりいただけると思います。この残った箇所の対応は前後関係含めて対応する必要があってちと面倒そうな印象も持っているので、いったんこの状況で公開しました。細かな実装についてはソースコードを参照ください。

便利な AppID を好みの UI で使えるメリットは大きいと思っていますので、興味ある方の参考になれば。


このページのトップヘ