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

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

タグ:image

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


ゆるキャラを画像で検索するサービスを作って公開してみました:
http://yuru.mybluemix.net/

まず最初に、自分はある程度ゆるキャラに詳しいと思っています。積極的な興味というよりは、マンホールに詳しくなっていると、最近はそのマンホールのデザインにゆるキャラが使われることが珍しくなくなってきたので、自然と(?)ゆるキャラにも詳しくなってしまうのでした。。
2017011500
 

さて、ゆるキャラに限らないのですが、イマドキ何かを調べようとした時にはまず『ググる』のが定番です。ただ、それは調べるためのキーワードが分かっている場合です。ゆるキャラの場合、名前が分かっていれば名前でググれば確実ですし、名前が分からなくても出身地とかが分かれば「ゆるキャラ 東京都」などで検索すればいくつか候補が見つかるのでそこから調べる方法もあります。

しかし問題は名前も出身地も分からず、検索するためのキーワードがない場合です。例えば目の前に着ぐるみそのゆるキャラ本体がいて、写真は撮れるんだけど、そのゆるキャラがなんという名前で、どこのゆるキャラで、どんな特徴を持っているのか、、、といった情報を調べる具体的な方法がなかったように感じていました。

といった背景もあり、「画像からゆるキャラを調べる」ことができるようなウェブサービスを作ってみた、という経緯です。いわゆる「類似画像検索」を行うため、コグニティブエンジンである IBM WatsonVisual Recognition API を使って実装してみました:
http://yuru.mybluemix.net/


使い方はシンプルで、ウェブサイトをPCかスマホのブラウザで開き、ファイル選択ボタンを押して、ローカルシステムやフォトライブラリ等から目的の画像を選択するだけです:
2017011501


PC の場合に限り、目的の画像ファイルを画面上部のこの辺りにドラッグ&ドロップしても構いません:
2017011502


例えばこの画像のゆるキャラを調べてみることにしました。まあ有名なヒトなので答はわかっているのですが、ちょっとトリッキーなアイテムも写っていて普段と違う画像になっているので、いいサンプルかな、と:
く


この画像を選択するか、画面内にドラッグ&ドロップすると画面が暗転して検索が始まります:
2017011503


暗転から戻ると、検索結果として候補キャラが画面下部に表示されているはずです。最大で100件表示されます:
2017011504


今回の画像の場合、下の方にそれっぽいのが見つかりました:
2017011505


該当する結果の画像をクリックすると、そのゆるキャラの情報がポップアップします。ご存知「くまモン」でした。なお PC であればマウスオーバーでも表示されます:
2017011506


まだまだ学習量が充分ではなく、第一候補となることはまだ珍しいとか、(着ぐるみの写真ではなく)絵の場合には精度が落ちるとか、横から写した画像に弱いとか、背景画像に左右されることが多いとか、まだまだ問題はありますが、一応動くものが作れたと思ってます。

実際の用途としてはある程度いけるかな、と思ってます。もちろん精度高く検索できることが理想ですが、上記で書いたように「名前が分かれば色々調べる方法はあるんだけど、肝心の名前が出てこない」のを解決するツールとして考えると、検索結果の候補の中に含まれていれば、それはそれでオッケーかな、と。


なお、今回のサービスは Visual Recognition の中では現時点でベータ版の /v3/collections/ で始まる API を使って開発しています。今後の仕様変更などがあった場合にサービスがどうなるか何とも言えませんが、なるべく対応させていく予定です。合わせてもう少し学習データの量を増やして検索精度を上げられないか、がんばってみます。


最近はスマホのカメラの性能が上がり、その結果として画像ファイルサイズが大きくなりました。解像度が上がったことは嬉しいのですが、画像ファイルをウェブアプリなどにアップロードしようとする際に時間がかかったり、パケット通信量が増えてしまったりします。また送信先の API やシステム設定によってはアップロードサイズに上限が設定されていて、そのままでは送れなかったりすることもあります。

そんな場合に「画像を小さくしてから処理する」方法が考えられますが、Java によるその一例を紹介します。画像ファイルデータが img 変数にバイト配列で格納できているという前提で、横幅を 800 ピクセル、縦横比を変えずに高さをそれにあわせる形で変更するような処理内容を記述しています(こうすると、大抵 1MB 以下になります):

byte[] img = null; //. この変数に画像データが byte 配列で格納されているものとする
   :
   :

//. 画像のサイズを変更
if( img != null ){
  BufferedImage src = null;
  BufferedImage dst = null;
  AffineTransformOp xform = null;

  InputStream is = new ByteArrayInputStream( img );
  src = ImageIO.read( is );

  int width = src.getWidth();    //. オリジナル画像の幅
  int height = src.getHeight();  //. オリジナル画像の高さ

  int w = 800; //. 幅をこの数値に合わせて調整する

int new_height = w * height / width; int new_width = w;
//. 画像変換 xform = new AffineTransformOp( AffineTransform.getScaleInstance( ( double )new_width / width, ( double )new_height / height ), AffineTransformOp.TYPE_BILINEAR ); dst = new BufferedImage( new_width, new_height, src.getType() ); xform.filter( src, dst );
//. 変換後のバイナリイメージを byte 配列に再格納 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write( dst, "jpeg", baos ); img = baos.toByteArray(); }

上記のように ImageIO と java.awt のアフィン変換を使って画像サイズに変換をかけて実装しています。

 

業務に関係して、ちょっとした画像アップローダーが欲しくなったので、IBM Bluemix でも動くようなものを作って一般化したものを公開します:
https://github.com/dotnsf/ImgUploader


PHP + MySQL + HTTP サーバー環境で動きます。Bluemix であれば PHP ランタイムClearDB試験提供の MySQL サービスを組み合わせた環境を想定しています:
2016061000


中身は非常にシンプルで、画像をアップロードして一覧で表示したり、プレビューしたり、というものです。画像データは BLOB 型にして MySQL 内に格納しているので、MySQL のバックアップを取ると画像データがバックアップされます。ここにアップロードした画像は URL でアクセスできるようになります。

アップロードに際しては PHP 設定の制約などを受けます。Bluemix 環境であれば最大 2MB です。PHP の設定を変更するなどしてください。必要に応じて認証を付けて運用してください。

その他、シンプルなので普通に使えると思っていますが、詳しくは GitHub の README を参照ください。


このページのトップヘ