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

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

タグ:image

某所で話題になっていた記事を読んで、自分にも興味ある内容だったので参戦してみました:

絶対に画像をダウンロード&スクレイピングさせないWebページを本気で作ってみた


内容は上記サイトのタイトル通りです。自分はマンホールマップお絵描き日記といった画像投稿機能を持つサービスを開発・運営しています。そんな中でサービスに参加していただいたユーザーが投稿した作品を「勝手にダウンロードして別のところで使われる」ことはサービス運営の観点で好ましくない自体ではあります。一方で「ウェブで閲覧できる画像のダウンロードを技術的に禁止する」のは結構難しいテーマで、これまで真剣に取り組んでいませんでした(ブロックチェーンを使ったり、作品の NFT 化といった対処は既に実装しているのですが、これらはいずれも「オリジナル作品のダウンロードそのものを禁止する」という技術ではありません)。で、上記サイトを見て、改めて自分でも本気でこのテーマに取り組んでみようと思ったわけです。


【画像のダウンロード禁止がなぜ難しいか?】
ここからは少しウェブの知識を必要とする内容になります。まずは「そもそも何故ウェブ画像のダウンロード禁止が難しいか?」を説明します。

普通のウェブページで普通に画像を表示しようとすると、<img> という HTML タグを使って表示します。例えばこんな感じです:
<img src="https://avatars.githubusercontent.com/u/3183150?v=4" />


<img> というタグが画像を表示する機能を持ち、その "src" という属性で画像の URL(場所)を指定します。この例であれば "https://avatars.githubusercontent.com/u/3183150?v=4" が画像の URL になっています。この URL をウェブブラウザで直接指定すると画像だけが表示されます(他にも <img> の属性があり、指定内容によって見え方を変えることができますが、ここでは省略します)。このような「普通の」画像は画像上を右クリックしてから「画像を保存」することで簡単にダウンロードできてしまいます:

2023060403


このように「ウェブで画像を表示するには <img> タグを使う」のですが、「<img> の src 属性に画像の URL が指定されている」ため、ウェブページの HTML を見ることでどこに画像があるかが分かってしまうのです。ウェブページそのものをうまく作ることで右クリックを禁止する(右クリックのメニューからの「画像を保存」をできなくする)ことはできても、画像の URL がわかってしまうと、その URL を直接指定して開くことで「うまく作ったつもりの」ウェブページの仕組みを回避して保存できてしまうことになります。ウェブページの HTML を開けなくすることはできないため、「HTML を見られても画像の URL やデータがバレないようにする」必要がありました。

ややこしいことに「スクリーンショットで画像を保存する」ことへの対策も必要です。Windows や macOS のスクリーンショットで画像が表示されている状態のウェブページを丸ごとコピーして、画像アプリなどにペーストし、必要であれば画像部分を切り抜いて保存する方法です。この方法を使うと厳密な意味では画像の解像度なども変わってしまうのですが、そこまで気にしなければ(少なくともウェブや PC の画面で見る上では)ほぼ同じ画像を手に入れることができてしまいます。これは最早ウェブだけでどうにかできる話ではなく、OS レベルで対策が必要になるため、技術的にも非常に面倒な対策が必要になるものです。

そしてもっと面倒なこともあります。「開発者ツール」とか「Developer Tool」と呼ばれるウェブページ開発時に使われる確認ツール(ウェブブラウザの機能)があるのですが、これを使うと HTML 上ではわかりにくい画像(JavaScript を使ってプログラミングで動的に作成する画像)なども検知できてしまいます。ここまで含めて対策するのはかなり厄介といえます(なので私も対策の優先度を下げていました)。

以上のようにウェブで画像のコピーやダウンロードを禁止しようとすると、以下3つの対策が必要になります:
  1. HTML 内に画像の URL を残さない
  2. スクリーンショットへの対策
  3. 開発者ツールへの対策

【上記サイトでの対策方針】
先述した上記サイトではこれら3つの対策が全て実装されていました。内容を読んだ私の理解が正しければ以下のような方針が取られていました:

1. および 3. への対策

  • <img> タグを使わず、JavaScript を使って <canvas> タグ内に画像を描画して表示する
  • サーバー側で画像データを暗号化し、ブラウザ側で復号化し、データ URL("data:image/png;base64,xxxxxx...")形式で画像データを扱う
  • JavaScript は難読化する
  • (ここまでやればあまり意味はなく、ページ内でも「おまけ」と表現されていますが)ページ内で右クリックを無効化する JavaScript を実装する


2. への対策

  • オリジナル画像を表示せず、コピーライト文章を画像に上書きして表示する(上述の <canvas> 内にテキストを上書きして表示する)


まず目からウロコだったのは 2. への対策でした。表示される画像がオリジナルのものではないので(画像の右下にコピーライト文章が上書きされた画像なので)スクリーンショットを撮っても元画像は保存できない、という考え方でした。なるほど、その手があったか、、という印象でした。

加えて、<img> を使わずに(HTML5 の)<canvas> を使って画像を表示すること & 暗号化された画像データを受け取って復号化すること によって、HTML 内の情報から画像データを推測したり取り出したりすることを困難にする、ということも行っているようです。データ URL のテキストすら残さないやり方なので、かなり徹底した対策になっていると感じました。

一方で、同ページ内にも書かれていましたが、これでもまだ 100% の対策とは言えない要素もあります。JavaScript は難読化されているとはいえ解読不可能というわけではないため、暗号化したデータを復号化するコードを特定されてしまうと、同じロジックでデータを復号化して画像データを取り出し、その画像を保存する、ということは不可能ではないのでした。

また、これは私自身の印象なのですが、1枚の画像を守るためのページでこれだけの処理をするのはまだいいとして、画像が一覧で表示されるようなページで、全ての画像にこれだけの処理を実行して守った場合に、ページそのものの処理がかなり重くなってしまうのではないか?という懸念を感じました。例えば1ページ内に 100 枚の画像があったら、これだけの処理をブラウザ内で 100 回行う必要があるわけです。検索の仕組みなどがあると、検索結果となる画像全てにこの処理を行う必要もでてきます。今自分が開発・運営しているサービスの中でこの方法をそのまま実装するのは・・・ と感じています。


【自分で考えた対策方針】
では、実際に自分のサービスに実装する前提で、自分だったらどうやって画像のダウンロードを禁止するかを考えてみました。

ヒントとなったのは上述の 2. への対策(スクリーンショット対策)として書かれている方法でした。これはダウンロードそのものを禁止するとか、HTML からオリジナル画像にアクセスするための痕跡を消すとかいう考え方ではなく「そもそもオリジナル画像ではなく、コピーライト文章を上書きした画像を表示することで、オリジナル画像の保存を不可能にする」という方法および実装でした。これを自分のサービスの特性に合わせて応用する方法を思いつきました。

たまたま自分が実装を考えていたウェブアプリは「(IDとパスワードによるログインで)ユーザーの認証」を行うタイプのものでした。ログインしなくてもアプリを参照することはできますが、画像投稿はログイン後でないとできないタイプのものです。この場合は以下のような方法でもオリジナル画像のダウンロードを禁止することができるのではないかと考えました:

1. 画像の参照はオブジェクトストレージなどに格納して URL で参照するのではなく、画像の ID をパラメータに受け取って画像(バイナリ)情報を返す REST API の形で実現する。
2. ユーザーのログインをサーバー側でセッションとして管理する(セッション管理は必須ではないが、1. の REST API リクエストがどのユーザーからのものかを識別できるような仕組みを用意する)
3. 1. の REST API をリクエストしてきたユーザーが画像の持ち主と一致した場合は画像データをそのまま返す。リクエストユーザーが画像の持ち主とは別ユーザーだったり、そもそもログインしていないユーザーからのリクエストだった場合は画像データに加工(画像の一部にコピーライトメッセージを上書き)してから返す。

例えば画像 ID が xxxx である画像を表示するための REST API を "https://servername/attachment/xxxx" のように用意します。この URL にログイン済みのユーザーから、かつその画像の持ち主ユーザーからリクエストがあった場合のみ投稿された画像をそのまま返し、それ以外の場合は画像を加工してから返す、とすればいいのではないかと考えました。特定の画像を表示するための URL は(ログイン有無やユーザーの違いに関係なく)常に一意でいて、しかし返される画像のバイナリ情報はユーザーによって異なり、オリジナルの画像が返されるのは投稿ユーザーからのリクエストだった場合だけ、というものです。この方法だと同じ URL でも返される内容が異なるのでキャッシュは使えなくなりますが、既存のウェブアプリからの変更箇所は(REST API のパス部分に変更がないので、画像を返す REST API の中だけを変更し、セッション内のユーザーと画像の投稿者が同一であることを確認し、異なっていた場合のみコピーライトメッセージを上書きする処理を追加するだけで済むので)UI 側にほとんど変更がなく、比較的影響範囲が小さくなると思いました。

更にこの方法では、

- 画像を表示する方法はあくまで1つなので(パラメータも同じ REST API なので)、画像 URL を隠さなくても投稿者本人がログインした上でのリクエストでない限りはオリジナル画像を取り出すことができない(複雑な仕組みで画像 URL を隠す必要がない)

- 複数の画像が表示される一覧ページでも、1枚の画像が表示される詳細ページでも HTML や JavaScript に特別な違いを意識することなく同様に表示することができる

という副産物のメリットもあります。

加えて、これは私のウェブアプリでは 1 と 2 の部分はほぼ実装済みであったため、実質 3 の変更だけをすれば済む形になりました。自分の設計的にもこの方法が効率的だったという理由もあります。


【実装してみた】
実際に自分が運用しているお絵描き日記サービスでこの機能を有効にしてみました。興味ある方は実際に参照してみて(よかったら ID も作成してみて)ください。

まずログイン前にトップページを表示するとこのように表示されます(右上のボタンが黄色の場合はログイン前です)。表示される全てのお絵描き画像は画像を取り出す REST API 経由で取得しており、今回はログインしていないユーザーからのリクエストなので、全てのお絵描き画像にコピーライトメッセージ(図の青枠部分、実際の画像には青枠はありません)が付与されています。お絵描きの一覧ページでも、後述の個別ページでも同様です:
2023060400


ちなみにこのコピーライトメッセージ部分を拡大するとこのように表示されます。赤字で "© (投稿者の ID)@MyDoodles" と表示されています。"MyDoodles" はアプリ名です。オリジナルのお絵描き画像にはこのコピーライトメッセージはなく、API がこのメッセージをお絵描き画像の右下に上書きした上で結果を返しています:
2023060401


画面を下にスクロールすると色んな利用者から過去に投稿されたお絵描き画像が表示されていきます。この時点ではサービスにログインしていないので、全てのお絵描き画像にコピーライトメッセージが上書きされて表示されています:
2023060401


特定の1つのお絵描き画像を選択すると、そのお絵描き画像画像の詳細画面に移動します。このお絵描き画像は本当は私自身が投稿したものですが、まだログインしていないので(投稿者本人かどうか確認できないので)ここにもコピーライトメッセージが表示されています:
2023060402


別の利用者が投稿したお絵描き画像を詳細画面で表示した場合も、同様にコピーライトメッセージが上書きされた画像として表示されます:
2023060403



次にログイン後に同じページを参照してみます。画面右上の黄色いログインボタンをクリックしてログイン(またはサインアップしてからログイン)します:
2023060400


ログインに成功すると画面右上のボタンは緑になり、自分のアイコン画像に切り替わります。ログイン後の一覧画面では自分が投稿したお絵描き画像は(コピーライトメッセージが付与されず)オリジナル画像の状態で表示され、自分以外の利用者が投稿したお絵描き画像にはコピーライトメッセージが残る形で表示されます:
2023060401


下にスクロールして先ほどと同じ部分を見ています。上のお絵描き画像は私が投稿したお絵描きなので、今度はコピーライトメッセージが表示されていません。下のお絵描き画像は私以外の利用者が投稿したものなので、私の ID でログインしていてもコピーライトメッセージが表示されます:
2023060402


各お絵描き画像を詳細画面で見た場合です。私のお絵描き画像にはコピーライトメッセージがなく、他ユーザーのお絵描き画像にはコピーライトメッセージが上書きされて、それぞれ表示されていることがわかります:
2023060403



2023060404


繰り返しますがログイン前後で画像表示部分の HTML は変わっていませんが、ログインの有無によって同じ REST API の返り値が変わる形で実装しているので、画像データそのものの取得はシンプル(<img src="xxxx" /> 部分はログイン前後で全く同じ)です。しかしここがシンプルなだけに、画像表示部分の HTML や JavaScript を見られたとしても、投稿したユーザーの ID でログインしない限りはオリジナル画像を取得する方法がない、という形で実装できたことになります。


最初は「どうやってオリジナル画像の情報を隠そうか」と色々な実現方法を考えていたのですが、スクリーンショット対策の「オリジナル画像ではない画像を表示する」というアイデアを応用させる形でこの方法を思いつきました。この方法は上でも書いている通り「キャッシュを有効にできない」という弱点はあるものの、(自分のように笑)そこまでアクセスの多くないログイン付きサービスで、API 取得以外の方法で画像にアクセスできないようにすることができれば、投稿者本人だけはオリジナル画像に簡単にアクセスできるというメリットまで含めて、この実装方法はかなり現実的なものになり得ると考えています。

これで投稿者のオリジナル画像の無断コピーを(投稿者本人以外からは)絶つことができそう、と思う。近いうちにマンホールマップにもこの実装を移植しよ。

※回避できるツワモノがいらっしゃったら教えてください。 m(__)m



最初にお断りを。今回紹介するサービスはソースコードを提供していますが、実際にブラウザから使える便利なサービスの形では提供していません(理由は後述します)。実際に試すには環境を準備の上、ソースコードを各自の 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> だけでここに画像を直接ペーストできるようになるのが理想なんですが、実現の可否含めてその方法がわかっていません。実現方法のヒントがありましたら教えていただけると嬉しいです。

 

むかーしから存在している技術なのですが、イメージマップという便利な機能があります(最近あまり使われなくなったという印象もあります)。これは 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つを紹介しました。そんなに難しい数学知識を必要とせず、比較的理解も実装も応用もしやすいアルゴリズムだと思っています。


このページのトップヘ