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

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

タグ:image

最初にお断りを。今回紹介するサービスはソースコードを提供していますが、実際にブラウザから使える便利なサービスの形では提供していません(理由は後述します)。実際に試すには環境を準備の上、ソースコードを各自の 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 を使うのであれば利用モデルも別のものにしたほうがいいと思われる、など、こちらは現時点ではまだまだ検討段階含めて未実装ということで。


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


タイトルそのままです。なんらかの画像をコピーしてクリップボードに格納された状態から、ブラウザ画面内の <canvas> 要素内に画像データをペーストして表示する、という処理が実現できないか試してみました。

結論としてはなんとなく実現できていると思います。サンプルを公開しているのでまずは挙動を試してみてください。


PC でもスマホでも、まず対象の画像をコピーします。今回は「いらすとや」さまのこの画像を使って試してみることにします:
https://www.irasutoya.com/2021/04/blog-post_12.html

2021041402


まずは画像をコピーします。PC の場合は右クリックから「コピー」、スマホの場合は対象画像を長押しするとコピーできると思います:
2021041403


その後、こちらのページを開きます:
https://dotnsf.github.io/web_image_paste/

2021041401



表示された画面内の「ペースト」と書かれた箇所にカーソルを移動した上でペースト(貼り付け)してください(スマホの場合は「ペースト」と書かれた箇所を長押ししてペーストを選択してください):
2021041404


うまくいくと最初にコピーした画像がブラウザ画面内の矩形部分(<canvas>)のサイズに合わせてペーストされます:
2021041405


このサンプルのソースコードはこちらで公開しています:
https://github.com/dotnsf/web_image_paste

このソースコードの中で特に該当の機能を実現しているファイルが index.html です。以下、解説を加えながらこのファイルの内容を紹介します。

まず該当部分の HTML は以下のようになっています:
<div class="container">
  <div id="canvas_div">
    <div id="cdiv">
      <div id="box" contenteditable="true">
        ペースト
      </div>
      <canvas width="80%" height="60%" id="mycanvas"></canvas>
    </div>
  </div>
</div>

画面内の「ペースト」と書かれている部分の <div id="box"> 要素に contenteditable="true" という属性がついています。これによって、この要素部分はペースト可能(Ctrl+V や右クリックメニューでペーストできる)として扱うことができるようになります。単にこの部分に画像をペーストして表示できるようにするだけであれば、この HTML だけで実現できます。

問題はここでペースト処理された画像を、この <div id="box"> 内に表示するのではなく(そのままだと <div id="box"> 内に画像がペーストされて表示されてしまうので、表示しないような処理を加えた上で)代わりにすぐ下の <canvas> 要素内に表示したい、という点です。

今回のサンプルではそういった処理は JavaScript で実現しています。まず「ペースト」と書かれた <div id="box"> 要素にペースト処理が実行されたイベントをフックして、imagePaste() 関数(後述)を実行するように指示しています。加えて false を返すことでフックしたイベントをキャンセルし、通常処理(この場合は <div id="box"> へのペースト処理)が実行されないようにしています(false を返さないと、<canvas> に画像をペーストした後でも <div id="box"> 内にも画像が残ってしまうので、それを避けるための処理です):
$(function(){
  $('#box').on( "paste", function( e ){
    imagePaste( e );
    return false;
  });

  :
  :

そして imagePaste() 関数の実装がこちらです:
function imagePaste( event ){
  var blobimg = null;
  var items = ( event.clipboardData || event.originalEvent.clipboardData ).items;
  for( var i = 0; i < items.length; i ++ ){
    if( items[i].type.indexOf( "image" ) == 0 ){
      blobimg = items[i].getAsFile();
    }
  }

  if( blobimg != null ){
    var bloburl = URL.createObjectURL( blobimg );

    var canvas = document.getElementById( "mycanvas" );
    var ctx = canvas.getContext( '2d' );

    var img = new Image();
    img.src = bloburl;
    img.onload = function(){
      var sw = img.naturalWidth;
      var sh = img.naturalHeight;
      var dw = canvas.width;
      var dh = canvas.height;
      ctx.drawImage( img, 0, 0, sw, sh, 0, 0, dw, dh );
    };
  }
}

まずクリップボード内に登録されているデータを配列で取得し、その中に画像("image")が含まれているかどうかを調べます。存在している場合はそのバイナリデータを取得します。

このバイナリデータが見つかった場合は URL.createObjectURL で画像データの URL を生成して画像化し、<canvas> 内に drawImage() 関数を使って描画します。その際に画像全体の高さや幅を <canvas> 全体の高さや幅に調整して表示するので、それらの情報を取得した上でパラメータ指定しています。

これによって「ペースト」のエリアにペーストした画像データを <canvas> 内に描画し、もとの「ペースト」のエリアには描画しない、という処理が実現できました。


本当は「ペースト」のためのエリアを使わずに <canvas> だけでここに画像を直接ペーストできるようになるのが理想なんですが、実現の可否含めてその方法がわかっていません。実現方法のヒントがありましたら教えていただけると嬉しいです。

 

いま自分の空き時間を使って、過去に Node.js + Cloudant を使って(普通のウェブアプリケーションとして)作っていたウェブサービスの Node-RED 環境への移植に挑戦しています。要はサーバーサイド JavaScript 実行環境である Node.js を使って過去に開発したウェブアプリケーション(画面や REST API)を、Node-RED の HTTP リクエスト/レスポンスノードや、HTML テンプレートノードを使っても動くように移植することに挑戦している、ということです。それなりに実績のある Node.js アプリケーションを Node-RED 上でも動かすことができれば、プラットフォームとしての Node-RED のポテンシャルを証明することができるのではないか、と考えています。

これを具体的に進めようとすると、まずウェブ画面は(i18n とかを考慮しなければ)HTML テンプレートノードを使えば一通りのことはできると思っています。要するに HTML テンプレートの中に HTML や CSS, フロントエンド JavaScript を含めてしまえば、見た目や挙動含めて一通りの画面を作ることはできると思っています。

問題は REST API 部分です。例えばデータベース(今回は Cloudant)のデータを読み書きするインターフェースを REST API で用意しておき、フロントエンドの画面から REST API を呼び出すことでデータの読み書き更新削除を行うことができるようになります(理論上は)。この REST API を Node-RED の HTTP リクエストノードと、HTTP レスポンスノードと、function などのノードを駆使して必要な機能を実装することができるかどうかが移植の可否になりそうだと思っています。まあ普通にデータを読み書き更新削除検索・・・する程度であれば標準の Cloudant ノードの機能範囲内でできそうな感触を持っています。


さて、今とある Node.js + Cloudant 製ウェブアプリケーションの Node-RED プラットフォームへの移植を設計している中で1つの壁に当たってしまいました。上述のように「普通の」データの読み書きの REST API 化はさほど問題にならないのですが、Cloudant が持つ特徴を使った部分が普通のデータのように扱うことができず、一筋縄ではいかない内容でした。結論としてはなんとなく解決の目処はたったと思っているのですが、その内容と解決までの経緯を含めて以下にまとめてみたので、興味ある方はご覧いただきたいです。


さて、問題となった Cloudant が持つ特徴を使った部分です。Cloudant は NoSQL 型(JSON 型)データベースですが、特殊な JSON フォーマットで格納することでバイナリデータを格納することができます。またその格納されたバイナリデータを(Content-Type 含めて)出力することもできます。この機能を使うことで、例えば画像データを Cloudant に格納して、画像データとして出力する、といったことも可能です。この機能は Cloudant の各種 SDK からも便利に使えるよう関数化されていたりします。

実は Cloudant のこのバイナリデータ格納機能を使っている場合が Node-RED 移植をする上でのネックとなります。Node.js などのプログラミング言語で Cloudant を利用する場合(特に上述の機能を使ってバイナリデータを Cloudant に格納する要件が含まれる場合)、Cloudant の REST API や各種 SDK を使ってデータの読み書きを実装することになります。上述のバイナリデータの読み書きも同様です。バイナリデータを書き込んだり、バイナリデータを Content-Type 含めて(つまり画像データであれば画像データとして)取り出して出力したりできます。実際にアプリケーション開発の中でこの機能を使って実装していることは(個人的にはバイナリデータの格納先に Cloudant を使うことが多いので)珍しくありません。

しかし、この部分を Node-RED に移植できるか? となると話は変わってきます。まず Node-RED からは Node.js 向けの Cloudant SDK を利用することはできません。function ノードの中でがんばって  Cloudant の REST API を呼び出すような JavaScript を書けば Node-RED でできるかもしれません(認証情報をどのように管理するかの問題は残ります。またどうせ JavaScript でゴリゴリ書くというのであれば、そもそも Node-RED をプラットフォームに選択しない方が正しいような気もします)。 この問題を標準の Cloudant in/out ノードだけでバイナリデータの読み書きを扱うことはできないか? と読み替えて考えることにしました。


【方法1 正攻法】
そもそも何が正攻法なのか、という問題もありますが、実は標準の Cloudant in ノード(Cloudant にデータを格納するノード)はバイナリデータを格納することもできます。上述の Cloudant のバイナリデータ格納機能は単に JSON データフォーマットをうまい具合に指定することで実現しているので、データを格納する点までは少しの工夫で実現できるのでした。

ただし、この方法の問題点は格納時ではなく取り出し時にあります。標準の Cloudant out ノード(Cloudant からデータを取り出すノード)は _id 値を指定してデータを取り出すことはできるのですが、肝心のこの部分がバイナリデータ格納を意識することなく、普通に JSON データとして取り出してしまうことしかできないのでした。特殊なフォーマットで格納することでバイナリデータ格納を実現しているのですが、この特殊なフォーマットに合わせた取り出しができないため、書き込むことはできても読み出せない、という問題が残ってしまうのでした。。


【方法2 BASE64 エンコードを利用して独自実装】
なんとなく解決の目処が立っているのがこちらの方法です。データの読み書きそのものは Cloudant の標準ノードを使うのですが、扱うバイナリデータは格納前に BASE64 でエンコードして(標準 Cloudant in ノードで)格納します。そして取り出す際も普通に標準 Cloudant out ノードで取り出した後に該当部分を BASE64 でデコードします。最後に HTTP レスポンスノードの属性で Content-Type ヘッダを指定して、デコード結果(画像バイナリ)を返信する、という方法です。プログラミングによるカスタマイズを駆使した、いかにもプログラマーらしい方法ですが、こちらの方法であれば格納時だけでなく取り出し時にも問題なく実現できそうです。

試しにフローを作ってみました。Github でも公開したので良かったらこちらからフローをダウンロードするなどして後述の手順で試してみてください:
https://github.com/dotnsf/nodered_cloudant_binarydata_io


【方法2 サンプルフローの使い方】
このサンプルを使って、実際にバイナリデータ(画像データ)を Node-RED で読み書きできることを確認してみます。

まずは Node-RED 環境を用意します。個別に用意していただいても構いませんが、最終的に Cloudant データベースを用意する必要もあるので、IBM Cloud を使って用意する方法がおすすめです。なお IBM Cloud を使ってここに書かれた方法で Node-RED 環境を構築した場合は、始めから Cloudant-in / Cloudant-out ノードがインストールされた状態になっているので、後述のこれら Cloudant 関連ノードのインストールは不要です。無料のライトアカウントを使って構築することもできる内容なので、Node-RED 環境がない人が試す上でおすすめの方法ではあります。

上記以外の方法で(普通にインストールするなどして)Node-RED 環境を用意した場合は node-red-node-cf-cloudant ノードを別途インストールする必要があります。右上のメニューから「パレットの管理」を選択し、「ノードを追加」から "node-red-node-cf-cloudant" を検索して追加してください:
2021032101

2021032102


併せて IBM Cloud にログインして Cloudant サービスを追加して利用できるようにしておいてください。繰り返しますが、このあたりあまり詳しい自信がない場合は上述の方法で IBM Cloud 環境内に Node-RED 環境を Cloudant データベースや Cloudant ノードなどとまとめて用意するのがおすすめです。


Node-RED が準備できたら、上述の Github リポジトリを使ってサンプルのフローを構築します。この flow.json ファイルがサンプルのフローそのものです。リンク先のテキスト内容をまとめてコピーし、Node-RED の右上メニューから「読み込み」を選択します:
2021032103


読み込みのダイアログで「クリップボード」を選択し、コピーしていた内容をペーストします。そして「新規のタブ」を選択し、最後に「読み込み」ボタンをクリックします:
2021032104


するとこのようなフロー画面が再現されるはずです:
2021032105


このままだとまだ2つの Cloudant ノード(画面上では "mydb" と表示されている2つの水色ノード)が未接続で使えません。どちらかをダブルクリックして設定ダイアログを表示します。すると Service 欄が一瞬だけ空のまま表示されますが、IBM Cloud の Node-RED 環境であれば接続済みの Cloudant サービスを見つけて接続してくれます。Service 欄に Cloudant サービス名が表示されたら「完了」ボタンをクリックします(もう1つの Cloudant ノードも同様にして Service 欄が埋まった状態にします):
2021032106


このように2つの Cloudant ノードの右上に表示されていた赤い印が2つとも消えればサンプルを動かすための準備は完了です。画面右上の「デプロイ」ボタンでデプロイして動作前の準備は完了です:
2021032101


改めてこのタブを見ると、3つの HTTP リクエストを処理するフローが定義されています:
#HTTP リクエスト処理内容
1GET /home画像ファイルアップロード画面
2POST /file画像ファイルアップロード処理
3GET /file(?_id=XXXX)アップロードした画像ファイルを画像として取り出す処理


1番目の GET /home は後述の 2 と 3 の動作を確認するための UI として、ファイルを指定してアップロードできる画面を表示するものです。実際に /home へアクセスすると、以下のような画面が表示されます:
2021032201


非常にシンプルなファイルアップロード機能を持ったページです。「ファイルを選択」ボタンを選んでローカル PC からファイル(今回は画像ファイル)を選択して「送信」ボタンをクリックです。「送信」すると、2番目 POST /file が実行されて、選択したファイルが Cloudant に格納される、というものです。

ここで試しに以下の画像ファイルを指定してアップロードしてみます(お好きな画像で試してください):
dotnsf_logo_200x200


画像ファイルを指定して「送信」します:
2021032202


こんな感じの HTTP POST の結果が表示されます(実際のアプリでは AJAX を使うなどしてこの結果をそのまま表示しないようにします):
2021032203


この後に Cloudant のダッシュボードなどから mydb データベースの中を確認するとデータが1件追加されているはずです:
2021032204


表示を JSON 形式などに切り替えると、格納されたデータファーマットも確認できます(type に画像フォーマット、data に base64 でエンコードした画像バイナリデータが格納されています):
2021032205


このデータの id 値を確認します(上図だと c7c3eb8e3b9ac0fffcd45c1beea6c62a )。この値と3番目の GET /file を使って格納された画像を表示してします。ウェブブラウザで /file?_id=(id の値) にアクセスして、アップロードした画像が表示されることを確認します:
2021032206
(↑アップロードした画像が復元できた!)


Node-RED を使ってバイナリ(画像)ファイルを Cloudant に格納し、また Node-RED から画像を復元することも実現できることがわかりました。


【方法2 解説】
Node-RED の HTTP リクエストでバイナリデータを格納したり、Node-RED の HTTP リクエストで格納したバイナリデータを取り出すことができる、ということがわかりました。以下はこれを実現している上記フローの解説です。

まず画面 UI である GET /home ですが、これはごく普通に enctype="multipart/form-data" を指定したフォームを定義しているだけです。テンプレートノードの中身は以下の内容の HTML です:
<html>
<body>
<form method="POST" action="/file" enctype="multipart/form-data">
<input type="file" accept="image/*" capture="camera" name="image" id="image"/>
<input type="submit" value="送信"/>
</form>
</body>
</html>

次に POST /file の各ノードを説明します。まず HTTP in ノード(POST /file と書かれたノード)はファイルアップロードに対応する処理を行うため「ファイルのアップロード」にチェックを入れている点に注意してください:
2021032207


また直後の function ノードの内容は以下のようになっています。アップロードされたファイルは msg.req.files に配列で格納されます(今回はファイル1つだけですが、配列の0番目に格納されます)。その mimetype と buffer を取り出し、buffer を base64 エンコードして msg.payload に格納し直して、最後にタイムスタンプを追加する、という処理を行っています(Cloudant データベースにはこのフォーマットで格納されていたはずです):
2021032208
//. アップロードしたファイルを base64 エンコーディング
var type = msg.req.files[0].mimetype;
var img64 = new Buffer( msg.req.files[0].buffer ).toString( 'base64' );

//. 独自フォーマット化
msg.payload.type = type;
msg.payload.data = img64;

//. タイムスタンプを追加
msg.payload.timestamp = ( new Date() ).getTime();

return msg;


この function ノードで処理された msg.payload の内容を Cloudant out ノードが受け取って格納します。このノードでは「Only store msg.payload object?」にチェックを入れて、ヘッダ情報などを格納しないようにしています。これで指定したバイナリファイルを(base64 エンコードして)Cloudant に格納する(同時に _id が割り振られます)、までの処理を実現しています:
2021032209


最後に GET /file(?_id=XXXX) のノードを紹介します。まず Cloudant in ノードでは特別な処理は行っておらず、パラメータとして与えられた _id を使って Cloudant の mydb 内を検索して結果を返す内容にしています:
2021032201


直後の function ノードでは mydb から取り出した結果を画像に戻す処理をしています。上述の function ノードの逆を行う形で、msg.payload.data の値を base64 デコードして画像バイナリに戻して msg.payload に代入し直しています:
2021032202
//. base64 エンコードされているバイナリデータをデコード
if( msg.payload && msg.payload.data ){
  msg.payload = new Buffer( msg.payload.data, 'base64' );
}

return msg;


その結果を HTTP レスポンスノードに渡して処理は終了です。が、このノードでは HTTP ヘッダをカスタマイズし、"Content-Type: image/png" を付けています。つまり直前の function ノードで取り出した画像のバイナリを画像(image/png)として送信するための処理を最後に加えています:
2021032203


これらのノードや処理を組み合わせることで Node-RED の HTTP リクエストからバイナリデータを Cloudant に格納したり、格納したデータからバイナリデータを取り出して Content-Type ヘッダを付けて返す、といった一連の処理を実現していました。


この例は Cloudant にバイナリデータを格納する場合のサンプルでしたが、おそらくほぼ同様の方法で他のデータストアにも応用できると思っています。



むかーしから存在している技術なのですが、イメージマップという便利な機能があります(最近あまり使われなくなったという印象もあります)。これは HTML ページ内の画像内にクリッカブルな領域を複数定義し、いずれかの領域がクリックされたら何らかの処理を行う、というものです。クリックされる領域によって処理内容(このページにジャンプするとか、この JavaScript 関数を実行するとか、・・)を変えることができる、というものです。

例えばこんな感じで実現します。いらすとやさんの『ホワイト企業~ブラック企業のイラスト』を例に紹介します:



このイラスト画像は横650ピクセル、縦137ピクセルです。その中にホワイト企業~ブラック企業が5段階のイラストで表示されています。

画像の左上の座標を ( 0, 0 ) とすると、このうち一番左の企業は、そのビルの矩形部分は左上が ( 37, 13 ) から右下 ( 100, 127 ) という矩形でできています。いま、このビルの矩形部分がクリックされたら "white" というメッセージを表示するようにしてみます:
2019100701


同様にして、5つのビルそれぞれにクリッカブルな矩形部分を定義し、それぞれがクリックされた時に以下のようなメッセージが表示されるようにしてみます:
ビル番号(左から何番目のビル)矩形範囲クリックされた時のメッセージ
1 ( 37, 13 ) - ( 100, 127 ) white
2 ( 165, 13 ) - ( 227, 127 ) lightgray
3 ( 291, 13 ) - ( 355, 127 ) gray
4 ( 481, 13 ) - ( 483, 127 ) darkgray
5 ( 546, 13 ) - ( 610, 127 ) black


これをイメージマップで実現すると、 HTML の該当箇所は以下のようになります:
<body>
  <div class="container">
    <img src="https://1.bp.blogspot.com/-BmJohMucVBI/XYhOV_VMQmI/AAAAAAABVHA/sZF8lMjPedUWSkxBwUGZXmVri2OFKEZ4gCNcBGAsYHQ/s650/company_white_black_kigyou_5dankai.png" border="0" usemap="#image_map"/><br/>
    <map name="image_map">
      <area shape="rect" coords="37,13,100,127" href="javascript:alert( 'white' )"/>
      <area shape="rect" coords="165,13,227,127" href="javascript:alert( 'rightgray' )"/>
      <area shape="rect" coords="291,13,355,127" href="javascript:alert( 'gray' )"/>
      <area shape="rect" coords="418,13,483,127" href="javascript:alert( 'darkgray' )"/>
      <area shape="rect" coords="546,13,610,127" href="javascript:alert( 'black' )"/>
    </map>
  </div>
</body>

<img> タグの usemap 属性でイメージマップの名称を定義します。そして <map> タグを使って、対象(name 属性で指定)の <img> のクリッカブル領域と、クリックされた時のハンドラを href 属性に定義します。上記例ではハンドラを javascript 関数の実行にしていますが、普通に URL を記述すればリンクを作成することもできます。

これで各ビルをクリックした時に、(左から順に)"white", "lightgray", "gray", "darkgray", "black" というメッセージが表示されるようになります。元は1枚の画像でしたが、クリッカブルな領域を5箇所定義することができ、それぞれのクリックハンドラを個別に設定することもできました:

(左から2番めのビルをクリックした時の様子)
2019100703


ここまでが普通のイメージマップです。このイメージマップ、非常に便利な反面で「レスポンシブ対応が難しい」という難点がありました。要はクリッカブルな矩形領域をピクセル絶対値で指定しているため、画像がそのまま表示された場合はいいのですが、レスポンシブ対応などで画面サイズに合わせて拡大縮小されて画面いっぱいに表示された場合、縮尺が変更になってしまい、絶対値で指定していたイメージマップの定義がおかしくなってしまうのでした。

具体的に同じコードをスマホのシミュレーターで確認した結果、下の赤点線部が "lightgray" のクリッカブル矩形部分となっていて、本来の位置からズレていました。これをどうにかしたい、というのが今回のテーマです:
2019100702


色々と調べましたが、答としては jQuery RWD Image Maps という jQuery プラグインを使うことで解決できました:
2019100700



以下に具体的な対応手順を紹介します。

まず HTML 内でレスポンシブのための宣言をしておきます。例えば以下のようなコードが既に含まれているものと仮定します:
  :
<meta name="viewport" content="width=device-width,initial-scale=1"/>
  :


次に jQuery と jQuery RWD Image Maps をロードします。以下の例では CDN からそれぞれをロードしています(jQuery RWD Image Maps の方を後にロードする必要があります):
  :
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery-rwdImageMaps/1.6/jquery.rwdImageMaps.min.js"></script>
  :

CSS で画像サイズの調整を行います。この例ではイメージマップを用いる画像の横幅を 100% に、高さは自動調整するように指定しています:
  :
<style>
img[usemap]{
  max-width: 100%;
  height: auto;
}
</style>
  :

そして最後に JavaScript で該当部分に RWD Image Maps を適用します:
  :
<script>
$(function(){
  $('img[usemap]').rwdImageMaps();
});
</script>
  :

これでスマホでおなじページにアクセスした時でもイメージマップの矩形部分がズレることなく利用できるようになりました:
2019100704




2年前に書いたこのブログエントリの続きのような内容です:
類似画像検索とカラーヒストグラム


類似画像検索を実現するアルゴリズムを調べています。上記のブログエントリでは「カラーヒストグラム」と呼ばれる比較的簡単な方法を紹介しました。今回は Average Hash と呼ばれる方法を紹介します。


まず、この方法は名前に "Hash" というキーワードがついています。一般的なハッシュ(ハッシュ値、ハッシュ関数)では入力値が少しでも異なっていると出力値が全然別の値になる、という特徴があり、その特徴をいかしてパスワード(のハッシュ)を安全に保存したり、ブロックチェーンに応用されたりしています。

ただ今回紹介する Average Hash アルゴリズムで使われるハッシュはその点で意味合いが少し異なり、入力されたデータが似ていた場合に似た値を返すようなハッシュ関数を定義します。このようなハッシュ関数を使って、用意された画像のハッシュ値をあらかじめ求めておきます。そして類似画像を探したい画像についても同じハッシュ関数でハッシュ値を求め、そのハッシュ値が似ている画像は入力データが似ているはずと判断して回答の候補とする、という考え方に基づいた類似画像検索アルゴリズムです。つまり画像として類似しているかどうかを、比較演算が容易なハッシュ値の差で判断しよう、というものです。


より具体的にアルゴリズムを紹介します。例えばハッシュ関数を以下のように定義したとします:
  • 画像のバイナリデータをハッシュ関数の入力値とする
  • nxn のサイズを持つ整数配列がハッシュ関数の出力値とする。なおnは整数値とする
  • ハッシュ関数内ではまず画像を正規化する。具体的には入力画像を縦nピクセル、横nピクセルにリサイズし(画素数はnxn)、更にグレースケール変換する
  • 次にnxnにグレースケールされた画像の各ピクセルの色の濃さ(0~255)を調べ、その値の平均値 avg を求める
  • 再度nxnにグレースケールされた画像の各ピクセルの色の濃さを調べ、その値が avg 以上であればそのピクセルの値は 1 、avg 未満であれば 0 とみなす。これをnxnピクセル分求めて配列にする
  • この配列をハッシュ関数の出力値とする

これだけだと理解しにくいと思ったのでもう少し詳しく順を追って説明します。なお以下の例では n = 16 の例を紹介します。

ハッシュ関数への入力データを以下の画像とします(ちなみにいらすとや様からの画像で、元のサイズは 737x800 ピクセルです):
2019051701



ハッシュ関数ではこの入力画像をまず 16x16 にリサイズし、かつグレースケール化して正規化します。この時点で画像のピクセル数は (16x16=)256 です:
2019051702


この (16x16=)256 個のピクセルを左上から1つずつ取り出して RGB 値を調べます。この値は 0 から 255 の間の整数値で、0 に近いほど黒っぽく、255 に近いほど白っぽいことを意味しています。実際には 16 行ありますが、最初の3行はこのような感じでした:
2019051705


こうして 256 個のピクセルの RGB 値の平均値 avg を求めます。この例では avg = 198.763 であったとします。

改めて 256 個のピクセルの RGB 値をこの avg と比較します。ピクセルの RGB 値が avg 以上だった場合は 1 、avg 未満だった場合は 0 とみなしていきます:
2019051706


この avg と比較した結果を画像の左上から順に並べて配列にします(下図では16個ごとに改行して実際の2次元イメージに近い形にしていますが、実際は改行せずに1次元配列とみなします):
2019051703


この配列がハッシュ値となります。なんとなく元画像の中で白っぽい部分を 1 、黒っぽい部分を 0 とした結果になっていることがわかります。また、このハッシュアルゴリズムだと元画像が似ていると関数実行結果のハッシュ値も似る、ということが理解しやすいと思います。しかもハッシュ値は整数 256 個の配列なので類似性の判断も容易です。

なお、別のこちらの画像(サイズは 470x628)で同じアルゴリズムを実行すると、
smartphone_woman_smile


結果はこのようになりました:
2019051704


どのような画像に対してもまず同じサイズのグレースケール画像に変換しています(つまりグレースケールになった状態での類似性を調べるアルゴリズムです)。また各ピクセルを「その画像の各ピクセルの明るさの平均値よりも明るいか暗いか」で 0 または 1 に変換しています。これによって画像そのものが明るいものだったり暗いものだったりする要素を取り除き、その画像の中での明るい部分と暗い部分に分けるようにしています。そしてその結果がどれだけ似ているのか/似ていないのかを整数配列の類似度という比較的簡単な方法で調べられるようにしている、という特徴があることがわかりますね。


このアルゴリズムを使ってあらかじめ用意した画像の Average Hash 値を調べておきます。そして類似した画像を調べたい画像データが送られてきた場合にまず同じアルゴリズムで 256 個の1次元配列である Average Hash 値を求めれば、「0 と 1 が一致している数が多いほど似ている」と判断できることになります。

この方法であれば(実行パフォーマンスの様子を見ながらではありますが)nの値を大きくしたり、Hash 値を 0 か 1 にするのではく RGB 値をそのまま使って nxn 次元のユークリッド距離を求めることでより精度の高い類似画像検索が可能になります。あるいは別の応用としてグレースケールの手続きを省略して、カラー画像のまま比較することも可能です。


という、類似画像検索のアルゴリズムの1つを紹介しました。そんなに難しい数学知識を必要とせず、比較的理解も実装も応用もしやすいアルゴリズムだと思っています。


このページのトップヘ