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

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

タグ:hash

"Hash File Storage" という、(IBM Cloud を使って)無料でも運用できるウェブストレージサービスのソースコードを公開します:
https://github.com/dotnsf/hfs


もともとはマンホールマップという自作の位置情報付き画像投稿サービスの機能の一部として開発したものだったのですが、画像投稿機能部分を切り出して、かつハッシュ計算を加えた上で API を整備しました。基本ストレージとして IBM Cloudant を使いますが、IBM Cloud のライトアカウント(無料)の範囲内でランタイム含めて運用可能なので、よかったら IBM Cloud と合わせてお使いください。


機能そのものは「ファイルストレージ」です。用意されたサンプルページや API を使ってファイルをアップロードしたり、アップロードしたファイルをダウンロードしたり、というよくあるものです。各種機能を REST API や Swagger ドキュメントでも提供しており、容易に外部アプリケーションから呼び出して利用することも可能です。

最大の特徴は格納時のファイル ID をファイルバイナリのハッシュ値で管理している点です。したがって既に登録されているファイルと(ファイル名などは異なっていても)バイナリレベルで全く同じファイルを登録しようとすると、同じファイル ID が既に存在しているため「登録できない」というエラーが返ります。またファイルを登録する以外にも「このファイルと同じものが既に登録されているか?」だけを調べる API が用意されていて、一度登録した後になんらかの変更が加わっているか/いないかを ID(ハッシュ値)で調べることができる、という特徴があります。 このサービス自体には含まれていませんが、ブロックチェーンと連携することでバイナリファイルの真偽性保証や、対改ざん性の強化を実現するものです。


実際に動作を確認するにはソースコードを git clone するかダウンロード&展開し、IBM Cloudant のクレデンシャル情報を指定した上で Node.js で起動します。詳しくは README.md を参照ください。



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つを紹介しました。そんなに難しい数学知識を必要とせず、比較的理解も実装も応用もしやすいアルゴリズムだと思っています。


僕自身はオープンソース製品である Hyperledger FabricHyperledger Composer を使ったブロックチェーンプラットフォームや、その対応アプリケーションを業務で開発する機会が多くあります。

ありがたいことに実際に作る機会も多いのですが、「作る」という決断に到達する前に方針を変更することも少なくありません。よくある理由としては「(現行の)ブロックチェーンに向かない要件」であるというケースです。パフォーマンス要件だったり、検索機能の要件だったり、現在のブロックチェーンでは本格運用に向かないケースが前提になっていたりすると、そのことを伝えた上で「それでも作るかどうか」を判断していただいています。そして「作らずに検討し直す」と判断されることもある、という意味です。

でも、これはブロックチェーンを使いたいと思いながらも「現在のブロックチェーン技術」では向かない、と判断されたからです。需要としては存在していることも意味しています。

そんな場面を多く見ている中で「だったらブロックチェーンといえるかどうかはともかく、同じような仕組みで耐改竄性を高めながら、この要件も満たせるようなデータストアの仕組みを作れないか?」と考えるようになりました。それが今回紹介するものです。


【「ブロックチェーン」の定義】
まず最初に、以下で紹介するアプリケーションは「ブロックチェーン」と呼べるものかどうかと言われると、私個人的には「呼べないのではないか」と考えています。これは「ブロックチェーンの定義」をどのようにするか、という問題にも関わってくるのですが、ここでは1つの目安として、JBA("Japan Blockchain Association" 日本ブロックチェーン協会)が定義した以下の2つの定義を満たすものをブロックチェーンと定義するものとします:

・ビザンチン障害を含む不特定多数のノードを用い、時間の経過とともにその時点の合意が覆る確率が0へ収束するプロトコル、またはその実装をブロックチェーンと呼ぶ。
・電子署名とハッシュポインタを使用し改竄検出が容易なデータ構造を持ち、且つ、当該データをネットワーク上に分散する多数のノードに保持させることで、高可用性及びデータ同一性等を実現する技術を広義のブロックチェーンと呼ぶ。

(参照)
http://jba-web.jp/archives/2011003blockchain_definition

2018062901



要するに「ブロックチェーンとは『ブロック』が『チェーン』のようにつながって格納される仕組み」というゆるい定義ではなく(注 私個人的にはこれがブロックチェーンの定義でもいいと思ってます)、「ハッシュポインタを用いて接続し、データを複数のノード上に分散して保持し、かつその複数ノードが耐ビザンチン障害性を持っている実装」のことを「ブロックチェーン」と定義する、ということです。 そしてこの定義の上で考えた場合、以下で紹介するものはブロックチェーンではない、ということになることを予め伝えておきます。

自分の考えは、上記のものだと耐ビザンチン障害性をもたせるためのコンセンサスアルゴリズムに問題が発生したり(最近だと「51%攻撃」と呼ばれる手法が話題になりました)、そのためにトランザクションのパフォーマンスを高く実現するのが難しかったりする、と思っています。なのでこの要件が必須でないケースでは、この条件を確保しないことでパフォーマンスや検索機能など他の要件機能を向上させることができるのではないかと考えました。


【色々出てきている】
現状のブロックチェーンにはパフォーマンス遅延や電力消費など当初は想定していなかったことが問題になっていることもあって、それらの苦手部分に対処した新しい仕組みも生まれています(それらが上記ブロックチェーンの定義を満たしているかどうかはともかく)。例えば全参加者のコンセンサスを得る上でのパフォーマンス遅延を解決するために、ランダムに選ばれた一部参加者のコンセンサスを得るように改良したものも出ているようです:
ブロックチェーンを超える?ハッシュグラフ(Hashgraph)とは?


【作ってみようと思ったもの】
今回、自分は以下4つの特徴を持つようなデータストアシステムを新たに開発してみようと思いました:
(1)ハッシュポインタを利用した、改竄検出が容易なブロックチェーンの仕組みはそのまま取り入れる
(2)データは中央集権管理型とする。これによりコンセンサスアルゴリズムや耐ビザンチン障害性の問題を発生させなくした上で、トランザクションパフォーマンスの向上を実現する
(3)データそのものは分散データストアに格納して(格納可能にして)高可用性を実現する。つまり単なる1ピア運用とは異なる
(4)バイナリデータやファイル添付を含む大きなレコードのデータストアへの格納や全文検索機能、データを読み書きするための REST API を標準装備する



(2)の時点で上記ブロックチェーンの定義を満たさなくなっています(ブロックチェーンは非中央管集権管理型)。(3)ただ単に1ピアにデータを格納するのではなく、論理的な1データストアが実際には複数のロケーションに分散できるようにしており、これにより特定ロケーションが単一障害点とはならないようにします。そして(4)ブロックチェーンが比較的苦手とする巨大オブジェクトの格納や検索機能もこのシステムでは標準装備することを目標としています。加えて標準で REST API を準備することで(導入すればすぐ使えるようになって)導入時の負担軽減も実現するつもりです。

2018062902



【作っているもの】
上記仕組みを実現するためのソフトウェア(というかプラットフォームというか、API 群というか・・)を開発中です。まだ開発中ですが Github にて MIT ライセンスで公開しています:
https://github.com/dotnsf/hashchainsolo


上記の事情もあり、開発コード名には「ブロックチェーン」は含めず「ハッシュチェーンソロ」という表現にしました。レコードのブロックをチェーンのようにつなげていくものではあるのですが、上述の定義を満たすようなものではなく(むしろそこを犠牲にしている)、自分も「ブロックチェーン」という表現にこだわるわけではないので「ハッシュチェーン」という表現にしたかったのですが、これはこれで別の意味に使われており、ややこしいことになりそうだったので(ブロックチェーンだと単一ピアに相当する設計になるという意味で)「ハッシュチェーンソロ」と命名しています。某○icrosoft Office 365 Solo の影響とかではありませんw

インフラとして、分散データストアには IBM Cloudant を採用しています。Apache CouchDB をベースとした NoSQL 型のマネージド DBaaS で、容量・トランザクションパフォーマンスに制約があるものの無料プランを選択することもできるので、「とりあえず動作確認してみる」ためのハードルは低く実現できると思っています。一方でプランを見直して容量やパフォーマンスを変えたり、要件によってはプライベートクラウド版やオンプレミスソフトウェア版を使うことも可能です。またこの Cloudant 自体がバイナリ(添付ファイル)格納や検索インデックスといった機能を内包しているので、ブロックチェーンでいうところの「ステート DB」に相当する仕組みや外部ファイルシステム等がなくても各種データを格納することができ、上述の4つの特徴を兼ね備えた単独の仕組みを比較的作りやすいと思っています。

#現時点では実装していない余談ですが、IBM Cloudant をデータストアに使っていることで、携帯版の PouchDB と組み合わせてモバイルプラットフォームへの移植も想定しやすい設計にしています。

ハッシュポインタを使って耐改ざん性を高める部分は全てスクラッチで実装しています。タイムスタンプと nonce を併用してマイニングを行い、特定条件(変更可)を満たした時だけハッシュを採用できるようにしています。

2018/Jun/30 の現時点での開発状況は「とりあえず一通りの REST API は動くようになっている」レベルです。時間を見つけてぼちぼちと改良中です。


【Call for Code】
この製品で Call for Code に参加予定です。自然災害の支援を目的としたソリューションのコンテストで、ブロックチェーンを使う事例は多く存在すると思いますが、そのブロックチェーン技術の選択肢の1つとして、既存技術では難しいものも場合によっては解決できる、と思っていて、その実装として提供・参加予定です。




 

ファイルのアップロードを伴うアプリケーションを作っていて悩ましいことの1つに「中身の変わらない(全く同じ)ファイルが別のファイル名でアップロードされることがある」ことです。

アプリケーション側の実装としては、アップロードされたファイルをストレージ等に保存する処理を用意しているのですが、その際のファイル名をどうするか? という問題があります。

例えば "a.jpg" という画像ファイルと、"b.jpg" という異なる画像ファイルがあり、これら2つがアップロードされるケースだけを考えるのであれば、ストレージに保存する際にも元のファイル名をそのまま使えることになります:
2017052201
↑異なる2つのファイルを元のファイル名のまま保存する場合(このケースは問題なし)


ところが更に "a.jpg" というファイル名で、既に保存済みの "a.jpg" とは異なるファイルがアップロードされることもあります。この3つ目のアップロードファイルは "a.jpg" というファイル名で(上書き)保存するわけにはいきません(元のファイルが消えてしまう)。ということは元のファイル名をそのまま使うことは正しい処理ではなくなります:
2017052202
↑同じファイル名で中身の違うファイル保存しようとすると上書きすることになってしまう


また別のケースとして、"c.jpg" というファイル名で、"a.jpg" と中身が全く同じ画像ファイルがアップロードされるケースを考えてみます。この場合、ファイル名そのものは元のもの(c.jpg)を使っても被ることがなくいいのですが、全く同じ画像ファイルを2つ保存することになり、無駄にストレージを消費することになります。画像ファイルであればそのサイズもたかが知れているのかもしれませんが、これが仮想イメージとかだったりすると1ファイルで数10Gバイト消費することもあるため、中身の全く同じファイルであれば複数保存せずにすませたいものです:
2017052203
↑異なる名前で中身の同じファイルを元のファイル名のまま保存すると、無駄な保存領域を使うことになる


上記の問題点を実現する方法として、「同じ中身(バイナリ)のファイルは同じファイル名で、異なる中身のファイルは異なるファイル名を用意して保存する」仕組みを用意する方法があります。で、これを比較的簡単に実現する方法がファイルのハッシュ値を使うことが考えられます:
2017052204
↑ファイルのバイナリデータのハッシュ値をファイル名に使えば、中身が異なるファイルは異なるファイル名になる


試しに Node.js で実装したものを Github に公開しました:
https://github.com/dotnsf/node-upload-sample


画像のバイナリデータ(バイト配列)からハッシュ値を生成しているのは app.js 内の以下の箇所です。Node.js 標準の crypto ライブラリを使って、SHA512 アルゴリズムで path のファイルストリームからハッシュ値を生成している箇所です:
  :
var crypto = require( 'crypto' );

  :
  var path = req.file.path;
  var destination = req.file.destination;

  //. Name after Hash value
  var hash = crypto.createHash( 'sha512' );
  var fstream = fs.createReadStream( path );
  hash.setEncoding( 'hex' );
  fstream.on( 'end', function(){
    hash.end();
    var result = hash.read();

      :

ファイル(画像)をストリーム化してハッシュ値を求め、後ろに元の拡張子を付けたものを保存時のファイル名にしています。


このアプリを実際に動かしてみた様子を以下に紹介します。まず動作確認するにあたって、3種類4つのファイルを用意しました。01.png と 02.png はファイル名は異なりますが、全く同じファイルです。03.png は名前も中身も異なります:
2017052301


これらとは別に、中身の異なる 01.png というファイルを用意しました(ファイル名だけ前述のものと被ります)。これら3種類4つのファイルを全て順にアップロードした時の様子が以下です:
2017052302


まずアプリケーションを起動するとこのような画面になります。登録した画像ファイルが一覧表示されますが、まだ何も登録していない状態では何も表示されません。ここで 01.png (03.png と同じ画像の方)を選択して登録してみます:
2017052301


01.png のハッシュ値が生成され、その値をファイル名として保存されました:
2017052302


このリンクをクリックすると、元の 01.png が登録されていることが確認できます:
2017052303


同様にして、今度は 03.png を登録してみます。すると 01.png と 03.png は異なる画像ファイルであるため、ハッシュ値も異なります。そのため別画像として新たにされます:
2017052304


続けて今度は 02.png (01.png と同じ画像ファイル)を登録します。このファイルはファイル名こそ別ですが実体が 01.png と同じものであるためハッシュ値は 01.png と同じものになります。そのため「既存のデータ」と判断され、新たに画像は登録されません(正確には同じ名前のファイル名で、同じ内容を上書きすることになるので、ファイルは増えず、中身が変わることもありません):
2017052305


更にもう1つの 01.png(元の 01.png とはファイル名は同じだが、中身の異なるもの)を登録してみると、今度は中身の違うファイルなのでハッシュ値も別のものになり、新しいファイルとして登録されます:
2017052306


結果的には3種類4つのファイルを登録しましたが、内容の異なる3つのファイルがアップロードされました。中身の同じファイルは二重登録しない、という当初の目的が達成できました:
2017052307


これでストレージの無駄な空間を使わずに済ませられそうです。

このページのトップヘ