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

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

2016/10

Java で一時的な作業ディレクトリを用意する場合に、こんなコードを書いていました:
String tmpdir = System.getProperty( "java.io.tmpdir" );  //. システムの一時ディレクトリ
tmpdir += ( File.separator + "_abc" + File.separator );
   :

まず System.getProperty( "java.io.tmpdir" ) で、システム上の一時ディレクトリ(例えば "/tmp")を取得し、そこにファイルセパレータと任意の文字列(上記例では "_abc" )を追加して作ってました。

これらの処理が終わった後の tmpdir 文字列の値は "/tmp/_abc/" となることを想定していて、例えば一時ファイルを作るのであれば、
  :
String tmpfilepath = tmpdir + "test.txt"
  :

みたいにして、tmpdir にファイル名を付けるだけでよい、という風に使っていました。


ところが、この System.getProperty( "java.io.tmpdir" ) の実行結果にシステム間で違いがあることがわかりました。Linux 系システムだと "/tmp" のような値が返ってきますが、Windows システムで実行すると "C:\Users\username\AppData\Local\Temp\" のような、最後に File.separator が付いている値が返ってくるようです。なのでその結果に対して、
tmpdir += ( File.separator + "_abc" + File.separator );
を実行してしまうと、tmpdir の値は "C:\Users\username\AppData\Local\Temp\\_abc\" となってしまい、File.separator が2回繰り返しで付いてしまうことになってしまうようでした。

要するに System.getProperty( "java.io.tmpdir" ) の実行結果はシステムによって末尾に File.separator が付いたり付かなかったりするようでした。これは想定外。

というわけで、今度からはこう書くことにしました:
String tmpdir = System.getProperty( "java.io.tmpdir" );
tmpdir += ( ( tmpfolder.endsWith( File.separator ) ? "" : File.separator ) + "_abc" + File.separator );
   :

File.separator で終わっていなかった場合だけ File.separator を付けて、で "_abc" と・・・ という感じに。これでシステム依存部分を回避できました。




IBM Bluemix を通じて提供されている Watson API の1つ Visual Recognition(画像認識) API に「類似イメージ検索」というベータ版の新機能が追加されました:
https://new-console.ng.bluemix.net/catalog/services/visual-recognition/


2016101101


この API はその名の通り、「ある画像に似た画像を(あらかじめ登録しておいた画像群の中から)探す」というものです。個々の REST API としては画像の登録や削除、そして類似検索といった機能が用意されており、それらを組み合わせて実装することになります。また他の Watson API 同様、検索した結果に対してはスコアという検索結果に対する自信の数値根拠が合わせて返される、という特徴があります。


またこの類似画像検索 API のデモサイトも公開されています。選択した画像に似た画像が表示される、というものです。一度使ってみると、どういったことが可能になるのか、というイメージが付きやすくなると思います:
https://similarity-search-demo.mybluemix.net/

2016101201


このブログは公私混同を売りにしている(笑)こともあるので、実際に自分のサービスを使って試してみた様子を以下に紹介します。自分の場合は(お約束ですが)マンホールマップの画像を使わせていただきました。複数の種類のマンホール画像をあらかじめ登録しておいて、後から登録していないマンホール画像を指定して類似検索した時に同じ絵柄のマンホールを見つけることができるかどうか!? という挑戦です。

実はこの挑戦はかなりハードルの高い挑戦でもあります。なぜなら「単にマンホールとして認識」されてしまうと、登録画像は全てマンホールなので「似た絵柄のマンホール」を探すことができないと思われるからです。またマンホールの外側にある部分の類似性は無視してほしいわけですが、その辺りの所、ワトソンはどうなのよ!? ということを確認するための実験的要素の強い作業です。

なおこちらのページのマンホール画像はあらかじめ登録しておく画像の1つとしています:
2016101202


詳しくは API Reference を参照いただきたいのですが、あらかじめ画像を登録する際には画像データに加えて、JSON 形式のメタデータファイルを用意する必要があります。このメタデータに登録された内容は検索結果に含まれて返されることになるものです。そのため ID や作成日時、作成ユーザーといった情報をこのメタデータに加えておくと、画像検索した結果から作成ユーザー情報まで取り出す、ということも可能になります。

今回は登録するデータとしてこの画像と、



画像に加えて、こんな内容のメタデータを用意しておきました:
{
  "id":120002,
  "username":"morimo_t",
  "created":"2010-08-17 22:03:26",
  "text":"川崎市の色蓋。よく通る道路だったのにはじめて気がつきました。",
  "address":"神奈川県川崎市幸区堀川町",
  "lat":35.53229904174805,
  "lng":139.697998046875,
  "nice":5
}

↑細かい説明は省きますが、画像検索した結果にこの画像が含まれていた場合に、これらのメタデータと合わせて結果が取得できる、というためのものです(なのでメタデータが不要であれば空オブジェクトでも構いません)。このように画像とメタデータの組み合わせを登録する画像全てに対して用意しておきます。

実際に画像を登録する前にいくつか準備が必要です。まず当たりまえですが、IBM Bluemix にログインし、Visual Recognition サービスを追加しておきます:
2016101203


サービス追加後、「サービス資格情報」の「資格情報の表示」を選択して、認証情報の "api_key" の値を確認しておいてください(この後で使います):
2016101204


ここからは curl を使って REST API を実行するので、Linux か Mac のユーザーはターミナルを開いてください。Windows ユーザーの場合、curl は別途インストールする必要があります。以下のサイトから環境にあった Windows 用 curl をダウンロードし、インストールしてください:
https://curl.haxx.se/download.html

2016101205


curl コマンドを使う準備ができたら実際に REST API を実行してみましょう。画像を登録するにはまず入れ物(「コレクション」といいます)を用意します。ちなみに類似画像検索はこのコレクション単位で実行して、回答を取得することになります。上記デモサイトのようにショッピング用の商品画像から類似画像を探す、という使い方であれば商品の画像を登録するコレクションを作って、そこに商品画像をまとめて登録して検索する、という使い方をすることになります。

今回は「類似マンホール検索」が目的なので、マンホール画像だけを登録するコレクションをあらかじめ作っておくことにします(そしてそこにはマンホール画像以外は登録しないようにします)。新たにコレクションを作成するにはこの REST API を実行します:
# curl -X POST -F "name=XXXXXXXX" "https://gateway-a.watsonplatform.net/visual-recgnition/api/v3/collections?api_key=(apy_key)&version=2016-05-20"

メソッドは POST で、マルチパートフォームデータとして name パラメータでコレクションの名前(上の例では "XXXXXXXX")を指定します。また URL パラメータで Visual Recognition サービスを参照して確認した api_key と、version("2016-05-20")を指定して実行します。

この実行が成功すると、指定した名前のコレクションが作成され、以下のような実行結果 JSON が返ってきます:
{
  "collection_id": "XXXXXXXX_xxxxxx",
  "name": "XXXXXXXX",
  "status": "available",
  "created": "2016-10-11T05:58:47.129Z",
  "images": 0,
  "capacity": 1000000
}

"name" に指定したコレクション名が入っていて、その名称に "_xxxxxx" という形式が付いた "collection_id" が得られているはずです。この後からはこの collection_id を使って API を実行するので、この値をメモしておきましょう。またこのコレクションには現在 0 枚の画像が登録されており("images" の値)、最大で 1000000 枚の画像が登録できる、という状態になっているようです。

では入れ物ができたので、この入れ物に画像を登録します。画像ファイル名が image.png、対応するメタファイル名が image.json である場合、以下の API を実行します:
# curl -X POST -F "image_file=@image.png" -F "metadata=@image.json" "https://gateway-a.watsonplatform.net/visual-recgnition/api/v3/collections/XXXXXXXX_xxxxxx/images?api_key=(apy_key)&version=2016-05-20"

マルチパートフォームデータで画像ファイルとメタデータファイルを POST メソッドで送り、URL パラメータの中で collection_id を指定しています。この API の実行が成功すると、このような JSON が返ってきます:
{
  "images": [
    {
      "image_id": "mmmmmm",
      "image_file": "image.png",
      "created": "2016-10-11T16:12:16.435Z",
      "metadata": {
        "id":120002,
        "username":"morimo_t",
        "created":"2010-08-17 22:03:26",
        "text":"川崎市の色蓋。よく通る道路だったのにはじめて気がつきました。",
        "address":"神奈川県川崎市幸区堀川町",
        "lat":35.53229904174805,
        "lng":139.697998046875,
        "nice":5
      }
    }
  ],
  "images_processed": 1
}

実行結果として image_id などが生成されています。また metadata の中身は指定してメタデータファイルの中身になっているはずです。

この時点でコレクションの状態を確認してみることにします。コレクションの状態を確認するにはこの APIを実行します:
# curl "https://gateway-a.watsonplatform.net/visual-recgnition/api/v3/collections/XXXXXXXX_xxxxxx?api_key=(apy_key)&version=2016-05-20"

実行が成功すると、以下のような JSON が返ってきます。画像が1つ登録されたので "images" の値が 0 から 1 に変化しています:
{
  "collection_id": "XXXXXXXX_xxxxxx",
  "name": "XXXXXXXX",
  "status": "available",
  "created": "2016-10-11T05:58:47.129Z",
  "images": 1,
  "capacity": 1000000
}

こんな調子であらかじめ登録しておく画像(=検索対象となる画像)をすべて登録しておきます。なお、コレクションに追加できる画像は(API Reference によると) 2MB 以内にするべきとのこと。また1つのコレクションに 1000000 (100万)画像まで登録できることになっていますが、1つのファイルを登録するのに仮に1秒かかるとすると、100 万画像で 100 万秒、つまりノンストップで行っても約 11.5 日かかる計算になります。派手に使う場合は準備に2週間~程度かかる、という心づもりが必要になりますね。

全ての画像が登録されたら、最後に類似画像認識を実行してみます。今回はこの画像を検索してみました(この画像はコレクションには登録していません):



↑上記で紹介した登録画像と同じ絵柄のマンホール画像です。この画像を search.png として保存し、以下の REST API を実行します:
# curl -X POST -F "image_file=@search.png" "https://gateway-a.watsonplatform.net/visual-recgnition/api/v3/collections/XXXXXXXX_xxxxxx/find_similar?api_key=(apy_key)&version=2016-05-20&limit=5"

collection_id を指定して、マルチパートフォームデータが画像ファイルを POST 送信しています。またこの例では URL パラメータに limit=5 を指定して、類似度の上位5つまでを取得するように指示しています。

この API が正しく実行されると、以下のような結果が取得できます:
{
  "image_file": "5137462154683827112.png",
  "similar_images": [
    {
      "image_id": "2f98c6",
      "image_file": "120002.png",
      "score": 0.7548828,
      "created": "2016-10-11T15:22:25.824Z",
      "metadata": {
        "address": "神奈川県川崎市幸区堀川町",
        "created": "2010-08-17 22:03:26",
        "id": 120002,
        "lat": 35.53229904174805,
        "lng": 139.697998046875,
        "nice": 5,
        "text": "川崎市の色蓋。よく通る道路だったのにはじめて気がつきました。€‚",
        "username": "morimo_t"
      }
    },
    {
      "image_id": "d20e86",
      "image_file": "804002.png",
      "score": 0.7416992,
      "created": "2016-10-11T15:59:02.442Z",
      "metadata": {
        "id": 804002,
           :
           :
      }
    },
    {
      "image_id": "b6f1e7",
      "image_file": "1594034.png",
      "score": 0.74121094,
      "created": "2016-10-11T15:17:22.733Z",
      "metadata": {
        "id": 1.594034e+06,
           :
           :
      }
    },
    {
      "image_id": "f0bb1f",
      "image_file": "1888484723171738767.png",
      "score": 0.73828125,
      "created": "2016-10-11T15:31:14.768Z",
      "metadata": {
        "id": 1.888484723171739e+18,
           :
           :
      }
    },
    {
      "image_id": "9f72ed",
      "image_file": "90003.png",
      "score": 0.72998047,
      "created": "2016-10-11T15:31:21.935Z",
      "metadata": {
        "id": 90003,
           :
           :
      }
    }
  ],
  "images_processed": 1
}

この形式だとわかりにくいので、実行結果を表&画像にしてみました:
#画像スコア
10.7548828
20.7416992
30.74121094
40.73828125
50.72998047


なんとも評価の難しい結果になりました。まず1位は期待通りに、事前に登録しておいた同じ絵柄のマンホールを取得することができました!これは素晴らしい!! そして問題は2位以下です。これらはマンホールの絵柄としては明らかに異なり、「類似画像」としてはふさわしくないのですが、そのスコアが1位の画像と大差ない、という結果になりました。

まず今回登録した画像には正解(というか、同じ絵柄のマンホール)が1つしかないので、それが1位になってることは評価に値すると思っています。ただ間違いである2位との差がほとんどないというのも実は厄介で、「似た画像はこの1つだけです」という判断をさせたり、正解が1個もないような検索時に「似た画像はありません」という判断をするのが難しくなってしまうのでした。うーむ・・・

一方で、今回は 500 枚のマンホール画像を事前に登録したのですが、この枚数が少なすぎた可能性はありますね。

とはいえ、最初に触れているように、今回の検証は「マンホールの絵柄の類似性を確認できるか?」というかなりハードルの高い検証をしたつもりでしたが、ある程度は判断できているようにも見えます。今後は登録画像枚数も増やした上で再度色んなパターンで検証することになると思ってます。



なお、この類似画像検索 API の詳細については API Reference を参照ください:
http://www.ibm.com/watson/developercloud/visual-recognition/api/v3/#collections


以前に Cloudant データベースにインデックスを作成する方法を紹介しました:
Cloudant 内のデータをインデックスして検索する


上記エントリの (2) で紹介した方法は単独のフィールド(cputemp)にインデックスを作成して、数値が一定値よりも大きいレコードを探す、というクエリーができるようにしたものでした。

では例えば、複数フィールドで検索条件を指定するようなクエリーを実行したい場合はどのようにすればいいでしょうか? 例えば以下のようなレコードが Cloudant のデータベース db に入っているものとします:
IDNAMEAGE
1鈴木20
2鈴木41
3佐藤20
4田中35
5田中15
: : :

(↑ RDB っぽく書いてますが、実際には JSON フォーマットで {"ID":1, "NAME":"鈴木", "AGE":20} のように格納されているとします)


この時に「30歳以上の鈴木さん」とか「20歳の田中さん(実際には見つからない)」という条件でレコードを検索したい場合、どのようなインデックスを作って、どのように検索すればいいでしょうか?


まず、NAME と AGE の2つのフィールドにまたがるインデックスを用意する必要があります。Cloudant の管理画面から目的のデータベースを選び、"Design Documents" の右にある + をクリックして、"Query Indexes" を選択してください:
2016101001


Index の定義画面が表示されたら、テキストフィールド内に以下の値を記述します:
{
  "index": {
    "fields": [
      {
        "name": "NAME",
        "type": "string"
      },
      {
        "name": "AGE",
        "type": "number"
      }
    ]
  },
  "type": "text",
  "name": "db-index"
}

string 型の NAME と、number 型の AGE を指定して db-index という名前のインデックスを作ります。入力後に "Create Index" ボタンをクリックすると実際にインデックスが作成されます:

2016101002


インデックス作成後、実際にクエリーを実行してみましょう。例えば「30歳以上の鈴木さん」を検索するには以下のような POST リクエストを発行します(curl で実行する場合の例):
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"鈴木"},"AGE":{"$ge":30}}}'
(Cloudant のサーバーが XXX-bluemix.cloudant.com 、ユーザー名が username 、パスワードが password であるものと仮定しています)

同様にして「20歳の田中さん」を検索する場合はこのようなリクエストを発行することになります:
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"田中"},"AGE":{"$eq":20}}}'

それぞれ _find にセレクター(selector) を指定して POST リクエストを発行しています。そしてそのセレクターの中で存在するインデックスを使った検索条件を指定して、クエリーを実行しています。その際にあらかじめ作成しておいたインデックス(db-index)が使われて、想定したレコードを検索することができるようになる、というものです。

この「想定するクエリーを実行するためのインデックスをあらかじめ用意しておく」というのが RDB に比べてちと面倒で、クエリーの柔軟性にかける部分ではありますが、Cloudant はこういった方法でクエリーに対応しています。


自分は、仕事柄サンプルコードを書くことが多く、 Java とかで例外処理が必要な時に、
try{
    :
    :
}catch( Exception e ){
  // 例外が発生したらここで処理
}

という書き方でまとめちゃうことが多いです。要は特定の Exception を個別にキャッチするのではなく、まとめて捕まえてやれ、的な例外処理です。これはエラーが発生したりすると原因追求がわかりにくくなるし、コードレビュー受けると「わかりにくい」って言われる、あまりいいやり方とはいえない方法です、弁解ではないですが、サンプルコードを書いて提供することが多く、例外処理がメインではないようなコードを紹介するような場合に便宜的にこういうコードを書いちゃうことが多いです。はい。以上言い訳でしたw

で、こういう「全ての例外を捕まえてやる!」的な例外処理方法のことを "Pokemon Exception Handling(ポケモン例外処理)" って呼ぶんですね。知らなかった・・・
http://stackoverflow.com/questions/2308979/exception-handling-question/2308988


でも日本では聞いたことないな・・・アメリカで流行ってるんだろうか・・・


Apache Derby(或いは "Cloudscape")という RDB をご存知でしょうか?
derby-logo-web


最近は SQLite や HTML5 のローカルデータストアの台頭であまり名前を聞かなくなりましたが、Pure Java で記述された軽量の RDB です。元々は Cloudscape Inc. によって開発されましたが、Informix Software を経て IBM 製品として扱われていた時代もあります。その影響もあってか "DB2 互換 SQL" に対応し、DB2 の SQL が動く軽量の Java RDB という立ち位置でした。軽量であるが故に組み込み系のアプリケーション内で使われることが多いようです。歴史的には 2004 年に IBM から Apache 財団へソースコードが寄贈され、現在の Apache Derby という名称のプロジェクトになりました。また Oracle JDK 1.6 以降に(オプションとして)組み込まれている JavaDB の実装はこの Apache Derby です。

私自身は "Cloudscape" と呼ばれていた頃に使ったことがありました。今回、久しぶりに Apache Derby を使ってみました。

JDK のオプションに組み込まれているとはいえ、せっかくなので最新版を使ってみることにしました。まずは Apache Derby のダウンロードページから最新バージョン(2016/Oct/07 時点では 10.12.1.1)のリンクをクリックします:
2016100601


最新版のアーカイブファイル:db-derby-(バージョン番号)-bin.zip をクリックしてダウンロードします:
2016100602


ダウンロードした zip ファイルを展開し、lib フォルダ内の必要な JAR ファイルを取り出します。今回は本体である derby.jar と、日本語ロケールが含まれた derbyLocale_ja_JP.jar の2ファイルを取り出します:
2016100603


この2ファイルを Java の開発環境から使えるようにします。J2SE/EE プロジェクトであれば、WebContent/WEB-INF/lib 以下にコピーするなどして、コンパイル/実行時に参照できるようにしておきます:
2016100604


試しに以下のような index.jsp ファイルを用意してみました:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.sql.*" %>
<%@ page import="me.juge.derby.*" %>
<%
  request.setCharacterEncoding("utf-8");
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title>Derby JDBC Sample</title>
</head>
<body>

<table border="1">
<tr><th>ID</th><th>NAME</th><th>PRICE</th></tr>
<%
final String driverName = org.apache.derby.jdbc.EmbeddedDriver.class.getCanonicalName();
final String dbName = "derbydb";
final String connURL = "jdbc:derby:" + dbName + ";create=true";  //. DBが存在していない場合は作成するオプション

try{
  Class.forName( driverName );
  Connection conn = null;

  try{
    conn = DriverManager.getConnection( connURL );
  }catch( Exception e ){
  }

  if( conn != null ){
    ResultSet rs = null;
    //. 初期化
    try{
      //. 試しに items テーブルにアクセス
      Statement s0 = conn.createStatement();
      rs = s0.executeQuery( "select count(*) from items" );
    }catch( SQLException e ){
      //e.printStackTrace();

      String state = e.getSQLState();
      if( state.equals( "42X05" ) ){
        //. テーブルが存在しない
        try{
          Statement s1 = conn.createStatement();
          s1.execute( "create table items("
            + " id int generated always as identity primary key"  //. この辺りが DB2 互換 SQL
            + ", name varchar(100)"
            + ", price int"
            + " )");

          PreparedStatement s2 = conn.prepareStatement( "insert into items( name, price ) values( ?, ? )" );
          s2.setString( 1, "シャンプー" );
          s2.setInt( 2, 1000 );
          s2.execute();

          PreparedStatement s3 = conn.prepareStatement( "insert into items( name, price ) values( ?, ? )" );
          s3.setString( 1, "石鹸" );
          s3.setInt( 2, 500 );
          s3.execute();
        }catch( Exception e1 ){
        }
      }else if( state.equals( "42X14" ) || state.equals( "42821" ) ){
        //. テーブル定義が不正

      }else{
        //. その他の想定外の例外

      }
    }finally{
      if( rs != null ) rs.close();
    }

    //. レコード表示
    try{
      rs = conn.createStatement().executeQuery( "select * from items" );
      while( rs.next() ){
        int id = rs.getInt( 1 );
        String name = rs.getString( 2 );
        int price = rs.getInt( 3 );
%>
<tr><td><%= id %></td><td><%= name %></td><td><%= price %></td></tr>
<%
      }
    }catch( SQLException e ){
    }finally{
      if( rs != null ) rs.close();
    }
  }
}catch( Exception e ){
  e.printStackTrace();
}
 %>
</table>
</body>
</html>

コードそのものは一般的な JDBC プログラミングです。DB を開く際に目的の DB が存在していない場合はその場で作成するようなオプション("create=true")を付与しています。また items というテーブルのレコード数を取得して、エラーが出るようであればテーブルが存在していないと判断し、新規に items テーブルを定義して作成し、2つほどデータを insert するようにしています。 最終的には items テーブル内の全レコードを画面に出力する、という内容にしています。


このプロジェクトを実行して、index.jsp にブラウザでアクセスすると以下のような表形式で2つのレコード内容が確認できます:
2016100701

まあ普通に RDB として使うこともできますが、組み込み系以外であればフットプリントの小ささからローカルレプリカ DB として利用する、というケースも考えられます。特にサーバー側が DB2 だったりすると SQL 互換が便利に使えたりしますね。

久しぶりに Cloudscape を使ってみました。相変わらずですが( Java が動いている前提はありますが)JAR ファイル置くだけで使える RDB は便利ですね。



このページのトップヘ