目次


Visual Recognition + Kotlin で撮影した画像で商品検索が出来る Android アプリを作ろう

Comments

今話題の技術や最新技術への挑戦を応援する【IBM×teratail のコラボ企画】第一弾です。これから 3回に渡って話題の技術と Watson API をかけ合わせた開発サンプルをご紹介していきます。記事を読み進めながら、ぜひみなさんも手を動かして作ってみてください。

第一弾は、2017年5月に Google から Android の公式言語として認定されたことで人気に火がついた Kotlin を使ったサンプルをご紹介いたします。

1

画像で商品検索が出来る検索アプリの概要

Amazon のスマートフォンアプリには撮影した画像から商品を検索できる機能があります。カメラに写っているものをそのまま検索できるので使ってみると、とても便利で面白い機能です。このような魔法のアプリを作るのは難しいと思っていませんか?確かに、画像の類似度を調べたり物体を認識させる機械学習のプログラムを実装するには専門の知識が必要です。しかし、実は IBM Bluemix の Watson Visual Recognition にはこうした難しい機能を実装した API が存在するため、あなたがモバイルアプリケーションの開発に手慣れているのであれば専門的な知識を殆ど知らなくても、簡単に画像で商品を検索出来るアプリを実現することが出来ます。このチュートリアルでは現在話題になっているプログラミング言語の Kotlin とWatson Visual Recognition を用いて、撮影した画像から Amazon で商品検索出来るアプリを作成します。

1.1 完成物のデモ

完成物は以下のような動作をします。

2

利用技術の紹介

今回のアプリケーションでは以下の技術を利用します。

  • 使用言語: Kotlin
  • 画像認識: Watson Visual Recognition
  • 商品検索: Amazon Product Advertising API

それぞれのアカウントへの登録を事前に行ってください。また、今回のサンプルアプリケーションは以下の開発環境で作成しました。

toolversion備考
Android Studio2.3.3
Kotlin1.1.3-2
Nexus 6 EmulatorAndroid 8.01140x2560 560dpi

2.1 Kotlin の概要

Kotlin は jetbrains が開発した Java Virtual Machine 上で動作するプログラミング言語です。Java との互換性が高く、可読性が高いことから Android アプリ開発に使われるようになり、近年人気が高まりつつある言語です。本チュートリアルのサンプルコードは Kotlin のプログラミング、Android アプリのプログラミングに興味があるどのレベルの方でもなるべく多くのことを得ることが出来るようにコーディングのノウハウも多量に詰め込みました。コード内で使われている Kotlin の言語仕様や Android 開発の基本的な概念を 1 から説明することは出来ないため、多少難しいと感じるところがあるかもしれませんが、そう感じられる方にこそ、このチュートリアルはとても有意義なものになると思います。難しいと感じたところを他の書籍や公式のドキュメントなどの情報源などを参照することで、Kotlin、Android プログラミングの重要な概念の理解が進み、Visual Recognition の連携の容易さを実感できるでしょう。

2.2 Visual Recognition の概要

Watson Visual Recognitionは認識させたい物の画像を Watson に覚えさせることによって、それ以降に Watson に送った画像にどんな物体が写っているかを識別してくれるサービスです。

詳しくはこちらの [公式ページ] を参照して下さい。

3

Visual Recognition を準備する

3.1 画像を収集する

識別したい物体の画像を頑張って収集します (最低 50枚)。今回は例として紅茶の商品からメーカーを識別させ、Amazon で検索出来るようにします。スマホのカメラなどで色んな角度や光加減や距離などを変化させて同一メーカーのティーバックなどの商品を被写体とした類似画像を大量に収集し、以下のように 50枚以上用意して zip 圧縮します。

今回はスマートフォンの連写機能を用いて保存した画像を PC に取り込み Watson に学習させることにしました。同様に他の紅茶メーカーの画像も用意します。

3.2 画像を Watson に学習させる

[IBM Bluemix の管理画面] にログインして Visual Recognition の使用を有効にします。カタログにある以下のウィジェットをクリックするとリンクに飛べます。

以上のリンク先で Watson サービスを有効にしたあと、以下のように Watson Visual Recognition の API キーを取得出来るようになります。この情報は後述する Secret.kt`WATSON_API_KEY` に設定します。

また、Watson サービスを有効にした後、以下の管理画面上のペインから識別器の作成と更新を行うことができるようになります。

以上のリンクから遷移した先で識別機には先程収集した画像の zip ファイルを送信します。

この時ラベルをつけることで、送信した画像を何と判断させたいのかを Watson に教えることができます。ここでは、識別機の名前を tea、識別したいラベルを lipton と twinings にして学習させます。以下のように識別器のカテゴリを tea、判別したいメーカーと画像ファイルをセットして create を押すと識別器が作成されます。

3.3 学習済みの分類器を取得するための情報を取得する

3.2 の手順を終えると、以下のように識別器の ID とカテゴリが取得できます。この識別機の ID、カテゴリを覚えておいてください。これらの情報は後で Android 側のアプリに設定します。

4

Amazon Product Advertising API を準備する

4.1 Amazon Associate Program のアカウントを作成する

Amazon Product Advetising Api を使用するためには [Amazon Associate Program] に登録する必要があります。ここでは、WEB アプリケーションあるいはスマートフォンアプリケーションの公開 URL を登録する必要がありますので、開発環境用の URL を作成してください。もしスマートフォンアプリケーションを公開する予定などがなければ、IBM Bluemix 上の Cloud Foundation 上にデプロイされた WEB アプリケーションの URL を登録しておく事もできます。審査に落ちてしまっても、アソシエーションタグは有効なので、登録後に取得したタグを覚えておいてください。

*アソシエーションタグには任意の文字列を指定しても Product Advertising API は動きますが、既に登録されているアソシエーションタグと競合してしまった場合他者に迷惑がかかる可能性があるため絶対に Amazon Associate Program のアカウントを取得しご自身のアソシエーションタグを使用してください。

4.2 Amazon Product Advertising API のアカウントを作成する

Amazon Associate Program のアカウントと共に [Amazon Product Advertising Service] アカウントを作成します。認証情報の取得に関しては、[Amazon の公式ページ] を参考にしてください。以下、コンソールの新しいアクセスキーの作成から ID と Secret Key を取得した画面です。

アカウントを作成後、認証情報を使用するため、コピーしておきます。

5

Android アプリの作成

5.1 事前準備

このチュートリアルにおいては Android Studio2.3.3 を使用して作成しました。このチュートリアルを始めるにあたってサンプルコードを使用しない場合は、Android 用の空のプロジェクトを作成してください。プロジェクトの詳細な作成手順や Java コードから Kotlin コードを自動生成する方法については省略します。構成管理は Maven ではなく、Gradle を使用したほうがスムーズです。プロジェクトの最初に起動するアクティビティファイルは MainActivity.kt とします。また、非常にコード量が多くなるため、コードは要点の解説のみとなります。完全な実装は、[サンプルコード]() を参照してください。

注意 1:今回のアプリケーションはなるべく Kotlin 以外の言語や Android 以外の実装の手間を増やさずにサンプルが実行確認できるようにしているため、AWS や Watson の認証情報を端末の Secret.kt というファイルに保存していますが、本来これらはサーバーサイドで秘匿しておくべき情報です。そのため、このサンプルで Secret.kt を設定した状態のアプリケーションを他人に公開したり、ダウンロード出来る状態には絶対にしないでください。もしそれらを一般公開してしまった場合、アカウントが乗っ取られてしまうなどの危険性があります。サンプルコードはあくまで個人での使用に限定し、Watson Visual Recognition と Kotlin との連携の一例として、動作確認用としてお使いください。サンプルコードの利用および、改変したコードを用いたことによって生じた損害に対する賠償につきましては一切致し兼ねます。ご注意ください。

注意 2:本チュートリアルは API レベル 21 (Android 5.0 Lolipop) 以上の Android 端末を対象にしています。そのため、それより以前のバージョンの Android 端末では動作させることができません。ご了承ください。また、Android SDK はバージョンアップ毎に予告なく破壊的変更が発生することがあります。そのため、将来リリースされる Android SDK のバージョンによっては、本サンプルアプリケーションが正しく動作しなくなる可能性があることにご注意ください。

5.1.1 環境変数の準備

必要な環境変数は今回 Secret.kt に書くようにしています。以下のようにご自身の環境に合わせて 3章、4章で取得したパラメータを設定します。

package com.leverages.imagesearchbluemix

val WATSON_API_KEY = "{your watson's api key'}"
val VR_USE_CLASSIFIERS = listOf("{your clsasifier'}")

val AWS_ACCESS_KEY_ID = "your aws access key"
val AWS_SECRET_KEY = "your aws secret key"
val ASSOCIATE_TAG = "your amazon's associate tag"

サンプルコードでは、以上のパラメータを設定するだけで、動作確認が出来るようになっています。詳しくはサンプルコードおよび各 API のドキュメントを御覧ください。

5.2 Android 開発の基礎知識

実際に開発に入る前に、この節では Android 開発において知っておくべき独特の概念について説明します。馴染みがなく難しい概念もあるかと思いますが、Android アプリを開発する上では避けては通れない重要な概念なので、理解しておきましょう。もし、各節の見出しを見てどんな概念なのか既に理解されている場合は、この節を飛ばして 5.3節から読み進めても構いません。

5.2.1 アクティビティとフラグメント

Android アプリの開発では、アクティビティとフラグメントという概念を理解することが重要になります。アクティビティはアプリケーションが担う各機能の単位、フラグメントが各機能で使われる部品の単位で、どちらも同じように、レイアウトファイルを持ち画面表示と機能を実装するためのクラスです。Activity クラスは Context クラスを継承しており、Context クラスには現在の画面の制御処理とデータが保持されています。対して、フラグメントはアクティビティの中のレイアウトに埋め込まれるページやページに使われる部品に対して制御処理とデータを持つオブジェクトです。つまり、1個の Activity クラスは「今ユーザーが開いている画面のデータとページをすべて管理するクラス」であり、フラグメントは「そのアクティビティの中で使われているページそのものや、ページに使われている部品に対してデータと処理を管理する*コンポーネントのようなクラス」と考えるとよいでしょう。アクティビティ内では Intent というクラスによって、次に呼び出すアクティビティを指定できます。この時、前の Activity から必要な情報を引き渡したり、明示的に次のアクティビティを指定せずにある特定の条件を満たすアクティビティを探すように指定することもできます(暗黙的インテント)。また、指定するアクティビティは自分が作ったアプリの中のアクティビティだけでなく外部のアプリのアクティビティを指定して起動させることもできます。

* ここでいうコンポーネントは MVVM の VM に対応する概念おけるコンポーネントを指しています。独自に作成した HTML タグに画面に表示するべきデータとデータの処理方法等について役割を与えたもののことを指します。

5.2.2 ライフサイクル

Android アプリは端末の少ないメモリを有効活用するために、ユーザーから見えなくなった Activity インスタンスを破棄し、必要になった段階で再生成する仕組みを持っています。したがって、Activity インスタンス上のみに保持しているステートは失われてしまうことがあります。そのため Android アプリの開発では、ステートに依存しないようなアプリを設計するか、初期化状態のアクティビティから継続中だったアクティビティを復元できる状態にするのに必要なデータを保存しておく事が必要です。前者の対応では単純なアプリしか作れないため、基本的には後者の対応を迫られることになります。そこで、アプリケーションが起動してから、終了するまでの間にユーザーがどのような行動をしたかによって発生する Android に予め登録されているイベントをまとめた概念図であるライフサイクルを理解することが重要になります。ライフサイクルの全容については、[Android Developers のアクティビティ項目] を参照してください。例えば、Android アプリで、ホームボタンを押すとアプリが中断されますが、その時に起きるイベントが onPause と呼ばれるイベントです。また、保留中のアプリケーションから元のアプリケーションに戻った時に Activity のインスタンスが消去されていなければ onResume というイベントが発生します。もしメモリから Activity が削除されていれば、onCreate->onStart->onResume という順番で呼ばれて Activity が再生成されます。そのため、絶対に存在していなければならないデータは他のアプリに移動した段階で、onSaveInstanceState、onPause イベント発生時にデータの保存処理をしておき、onCreate、onStart イベント発生時にデータの復元処理を設定しておくといいということになります。特にライフサイクルで気を付けるべきことは、ハンドラの非同期処理の実行中にユーザーがそれらの実行完了を待たずにアクティビティを離脱した場合、遷移先でハンドラにキューされている処理が実行されてしまい、予期せぬ挙動やエラーを引き起こすことがあります。例えば、数秒後に次のアクティビティに遷移する処理をハンドラに設定したとして、ユーザーがハンドラの実行前にアクティビティを離脱した場合に、ユーザーの離脱先でハンドラが実行されてしまい、遷移前のアクティビティの処理の続きに戻されてしまうということが起こります。onPause イベントは、このようなユーザーの中断によって続行する必要のないハンドラを停止・削除するため等に用いられます。onResume イベントは onPause イベントによって、停止・削除されたハンドラを再実行させる必要がある場合に使用するとよいでしょう。onResume イベントではアクティビティが破棄されていなくても、アクティビティの復帰時に処理が呼ばれるため、アクティビティ中断時に保持されてる変数や処理を再設定したり状態のリセットを行いたい場合に使います。ライフサイクルを理解することで、アプリケーション開発時のバグ原因の特定や未然の防止につながったり、Android アプリケーションに対する柔軟な制御ができるようになります。

5.2.3 イベントハンドラのオーバーライド

ユーザーが何かを入力した、あるいは時間が何秒間経過したなどの様々な条件を満たした際に行われる処理に使われる関数のことを*イベントハンドラと呼び、実際にどんな条件でイベントハンドラを処理するかを定めるオブジェクトのことをイベントリスナーと言います。Android の標準ライブラリに実装されている UI ライブラリにはそれぞれの条件に応じたイベントリスナーを設定できます。例えば、MainActivity で Kotlin のコードで button を押した時に Hello World. とメッセージを表示させたい場合、

var button = findViewById(R.id.some_button)
button.setOnClickListener(object:View.OnClickListener{
  override fun onClick(v:View) {
    Toast.makeText(this@MainActivity,"Hello World.",Toast.Long).show()
  }
})

と書くことができます。view.OnClickListener はインターフェースで、object は無名クラスを指しその場で view.OnClickListener インターフェースを継承した無名クラスをイベントリスナーとして作成します。View.OnClickListener インターフェースは、onClick というイベントハンドラの実装を必ず強制するので、override でイベントハンドラの処理に "Hello World." という出力を行うように再定義します。このように、Android はインターフェースで定められているイベントハンドラをオーバライドして UI などのイベント処理を行います。

*イベントハンドラとイベントリスナーを同一視し、イベントハンドラをコールバックと呼ぶこともありますが、文脈に応じて意味を解釈し分けてください。ここでは、イベントリスナーで処理される関数についてはイベントハンドラという呼び方に統一して説明を行います。広義にはイベントハンドラはコールバックに含まれます。前述の Handler クラスの場合は慣例的にハンドラとコールバックと呼ばれます。

5.3 Android のカメラ撮影機能を実装する

この章では、Android 端末でのカメラ撮影機能を実装していきます。Android 5.0 以上からは、それより下のバージョンで使われていた camera モジュールが非推奨になり、camera2 モジュールを使用するようになりました。ここでは camera2 のモジュールを Activity という単位で使いまわせるようにするため、camera2 モジュールを用いて CameraFragment を実装するようにします。

5.3.1 初期画面からカメラを起動出来るようにする

Android 端末のカメラの起動には Permission が必要です。Android 5.0 以上からは、ユーザーから直接カメラの使用許可を得るコードを書く必要があります。また、今回は、インターネットの接続許可を行う必要もあるため、同時に AndroidManifest.xml に設定を追加しておきます。

<!-- /app/src/main/AndroidManifest.xml -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  class ConfirmationDialog : DialogFragment() {
      override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
          return AlertDialog.Builder(activity)
                  .setMessage(R.string.request_permission)
                  .setPositiveButton(android.R.string.ok) { dialog, which ->
                      FragmentCompat.requestPermissions(parentFragment,arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION) }
                  .setNegativeButton(android.R.string.cancel
                  ) { dialog, which ->
                      parentFragment.activity?.finish()
                  }
                  .create()
      }
  }

また、fragment_camera.xml を記述しておきます。TextureView はカメラで撮影している画像や撮影した画像を反映させる View になります。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@color/colorPrimaryDark"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <TextureView
        android:id = "@+id/PreviewTexture"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

ConfirmationDialog は後述のカメラを起動するメソッドの中で呼び出しています。カメラのパーミッションを設定するコードが書けたら、カメラの起動についての説明に進みます。

カメラの処理はそのプロセスを終了するまでルーパーで処理を専有するため、カメラに係る処理はすべて BackGround スレッドで実行する必要があります。そのため、onResume にカメラのプロセスのためのスレッドを用意しておきます。

    override fun onResume() {
        super.onResume()
        startCamera()
    }

    private fun startCamera(){
        mBackgroundThread = HandlerThread("CameraBackground")
        mBackgroundThread?.start()
        mBackgroundHandler = Handler(mBackgroundThread?.looper)

        if(mTextureView!!.isAvailable){
            openCamera(mTextureView!!.width,mTextureView!!.height)
        }else{
            mTextureView!!.surfaceTextureListener =  mTextureListener
        }
    }

mTextureView は現在カメラに写っている画像を映し出すための view です。ここで、正常に mTextureView が呼び出せる状態になったら openCamera でカメラを呼び出します。

    private fun openCamera(width:Int, height:Int){
        if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission();
            return;
        }
        mCameraInfo = CameraChooser(activity,width,height).chooseCamera()
        mCameraInfo?.let{
            val size = mCameraInfo!!.getPictureSize()
            mImageReader = ImageReader.newInstance(size.width,size.height,
                    ImageFormat.JPEG,2)
            mImageReader!!.setOnImageAvailableListener(mOnImageListener,mBackgroundHandler)
            transformTexture(width,height)
            var manager : CameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
            try {
                manager.openCamera(mCameraInfo!!.getCameraId(),mStateCallBack,mBackgroundHandler)
            }catch (e : CameraAccessException){
                e.printStackTrace()
            }
        }
    }

以上のメソッドを実行すると、Camera の起動が始まります。CameraInfo はカメラの設定情報を保持して ImageReader クラスなどのサイズや端末の向きによって値を調整する役割を担っています。 ImageReader クラスは CaptureBuilder メソッドの引数に取ることで画像のキャプチャに成功した際に画像のデータにアクセスすることが出来るようになります。OnImageAvailableListener は、画像がキャプチャされた状態になった場合に、発火するイベントハンドラを設定できます。このイベントハンドラには ImageReader クラスから取得した画像を学習済みの Watson Visual Recognition の識別器に送信する処理を記述します。CameraManager の openCamera メソッドで CameraInfo クラスから取得したカメラのどれを使用するかを id で渡し、カメラ起動後の後続処理を mStateCallBack で設定し、mBackgroundHandler でそれぞれの処理を Background のスレッドで実行するようにしています。このようにカメラの制御は非同期制御になっているためカメラを制御するには、カメラを操作しているオブジェクトのコールバックによって処理を後続させていくことになります。カメラの取得に成功した後はコールバックでカメラのオブジェクトを保持するようにします。以下は openCamera 実行後に設定されるコールバックになります。

    private val mStateCallBack:CameraDevice.StateCallback = object: CameraDevice.StateCallback() {

        override fun onOpened(cameraDevice: CameraDevice?) {
            mCameraDevice = cameraDevice
            createCameraPreviewSession()
        }


        override fun onDisconnected(cameraDevice : CameraDevice?) {
            cameraDevice?.close()
            mCameraDevice = null
        }


        override fun onError(cameraDevice: CameraDevice?, error: Int) {
            cameraDevice?.close()
            mCameraDevice = null
            if(activity!=null) activity.finish()
        }
    }

createCameraPreviewSession は次節のカメラ撮影のためのオブジェクトの初期化を行うメソッドです。ここまででカメラ起動についての説明は終わりました。次は画像の撮影の説明に入ります。

5.3.2 カメラで撮影ができるようにする

前節で説明したように createCameraPreviewSession の実装の概要について説明します。

    private fun createCameraPreviewSession(){
        try{
            var texture:SurfaceTexture = mTextureView!!.surfaceTexture
            val size = mCameraInfo!!.getPreviewSize()
            texture.setDefaultBufferSize(size.width,size.height)
            val surface = Surface(texture)
            mCaptureRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            mCaptureRequestBuilder.addTarget(surface)
            mCameraDevice!!.createCaptureSession(Arrays.asList(surface,mImageReader!!.surface), mSessionStateCallback,null)
        }catch (e :CameraAccessException){
            e.printStackTrace()
        }
    }

createCameraPreviewSession によって撮影した画像の buffer を設定して、画像のキャプチャ時に mImageReader と mTextureView の両方に撮影した画像を流し込むようにしています。カメラの撮影にはわかりやすく takePicture という名前の関数を用意しましょう。カメラの撮影毎に mCaptureSession に CaptureRequestBuilder によって生成される CaptureRequest オブジェクトを渡すと撮影が開始されます。CaptureSession オブジェクトの capture メソッドが実行された後、captureStillPicture によりカメラの撮影方法を調整してキャプチャします。

    private fun takePicture(){
        try{
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_START)
            captureStillPicture()
        }catch(e:CameraAccessException){
            e.printStackTrace()
        }
    }
    private fun captureStillPicture(){
        try{
            if(activity==null||mCameraDevice==null){
               return
            }
            var captureBuilder:CaptureRequest.Builder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
            captureBuilder.addTarget(mImageReader!!.surface)
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
            var rotation:Int = activity.windowManager.defaultDisplay.rotation;
            var sensorOrientation = mCameraInfo!!.getSensorOrientation()
            var jpegRotation:Int = getPictureRotation(rotation,sensorOrientation)
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION,jpegRotation)
            var captureCallBack :CameraCaptureSession.CaptureCallback = object:CameraCaptureSession.CaptureCallback(){
                override fun onCaptureCompleted(session: CameraCaptureSession?, request: CaptureRequest?, result: TotalCaptureResult?) {
                    unlockFocus()
                }
            }
            mSound.play(MediaActionSound.SHUTTER_CLICK)
            mCaptureSession!!.stopRepeating()
            mCaptureSession!!.capture(captureBuilder.build(), captureCallBack ,mBackgroundHandler)
        }catch (e:CameraAccessException){
            e.printStackTrace()
        }
    }
    private fun unlockFocus(){
        try{
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
            mCaptureSession?.capture(mCaptureRequestBuilder.build(),null,mBackgroundHandler )
            mCaptureSession?.setRepeatingRequest(mCaptureRequest, mCaptureCallback, mBackgroundHandler)
        }catch (e:CameraAccessException){
            e.printStackTrace()
        }
    }

また、画面をタッチすると takePicture メソッドが呼び出され画像を撮影することが出来るように設定します。

    override fun onClick(_v: View?) {
        takePicture()
    }
    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        var view = inflater!!.inflate(R.layout.fragment_camera, container, false)
        view.setOnClickListener(this)
        return view
    }

takePicture() で、mCaptureSession!!.capture(captureBuilder.build(), captureCallBack ,mBackgroundHandler) が呼ばれると、ImageReader.OnImageAvailableListener のコールバックで、撮影画像の buffer が受け取れるようになります。

    private val mOnImageListener = ImageReader.OnImageAvailableListener { reader ->
        if (!pause) {
            pause = true
            val image = reader.acquireLatestImage()
            val buffer = image.planes.first().buffer
            val data = ByteArray(buffer.remaining())
            buffer.get(data)
            image.close()
            mCaptureSession?.setRepeatingRequest(mCaptureRequest, mCaptureCallback,mBackgroundHandler)
        }
    }

以上のコードの変数 data が撮影した画像をバイト配列に変換したものになります、これにより撮影した画像をストリームに書き出せるようになったため、画像をファイルに書き込んだり、API に送信することができるようになりました。スタンダードなカメラ撮影の処理の実装は以上になります。

5.4 Watson Visual Recognition の実装

5.4.1 Watson Visual Recognition のレスポンスから診断結果を抽出出来るようにする

Watson のレスポンスは JSON 形式で返ってくるので、学習済みの識別器の中で送信した画像にもっとも近かったラベルを抽出できるようにします。

klaxon というライブラリを使うと簡単に parse できるので、build.gradle に以下を追記します。また、Watson Developer Cloud Java SDK をダウンロードする設定も行います。

dependencies{
    //略...
    compile 'com.ibm.watson.developer_cloud:visual-recognition:3.8.0'
    compile 'com.beust:klaxon:0.31'
}
repositories {
    //略...
    mavenCentral()
    jcenter()
}

このように追加したいライブラリを build.gradle に記述することで、gradle は外部リポジトリからライブラリを自動でダウンロードしてくれるようになります。その後以下のように parser を作ります。以下のコードは Visual Recognition の JSON 形式のレスポンスの中から score という値が一番大きいクラスを抽出して、その物体が何と判断されたかを取得します。ここでは、3.3節で取得したカテゴリ`tea`を指定してレスポンスの絞込みをかけるように実装していますが、識別器を複数用いる場合はカテゴリを指定しない実装に変更することも出来ます。

import com.beust.klaxon.*;

object WatsonParser{
    var klaxonParser:Parser = Parser()
    fun vrResponseParse(strJson:String,category: String = "tea"):String?{
        var json:JsonObject = klaxonParser.parse(StringBuilder(strJson)) as JsonObject
        var images = json.array<JsonObject>("images")

        var classifiers = images?.firstOrNull {
            (it.array<JsonObject>("classifiers") as JsonArray).any {
                it.string("name") == category
            }
        }?.array<JsonObject>("classifiers")
        var selectedClassifier = classifiers?.maxBy<JsonObject,Double> {
            var _class = it.array<JsonObject>("classes")?.maxBy<JsonObject,Double> nest@ {
                return@nest if(!it.isEmpty()) it.double("score")!! else .0
            }
            _class?.double("score")!!
        }
        var selectedClass = selectedClassifier?.array<JsonObject>("classes")?.maxBy<JsonObject,Double> {
            return@maxBy it.double("score")!!
        }?.string("class")
        return selectedClass
    }
}

以上で送信した画像のスコアを元にもっともその被写体に近いラベルを抽出できるようになりました。

5.4.2 カメラで撮影した画像を Watson Visual Recognition に送信する

カメラでキャプチャされた画像は OnImageAvailableListener によって読み出すことができます。Watson へのレスポンスを受け取ってラベルを取得出来たらそれを resultQuery として保存し、Amazon Product Advertising Api に送信できるようにします。

    private val mOnImageListener = ImageReader.OnImageAvailableListener { reader ->
        if (!pause) {
            pause = true
            val image = reader.acquireLatestImage()
            val buffer = image.planes.first().buffer
            val data = ByteArray(buffer.remaining())
            buffer.get(data)
            image.close()
            var options: ClassifyImagesOptions = ClassifyImagesOptions.Builder()
                    .classifierIds(VR_USE_CLASSIFIERS)
                    .images(data, "test.jpeg")
                    .build();
            var result = vr.classify(options).execute().toString()
            WatsonParser.vrResponseParse(result)?.let {
                resultQuery = it
                (activity as CameraFragmentInterface).onCameraFragmentInteraction(this)
                pause = false
            } ?: run {
                Toast.makeText(activity, "画像が認識できませんでした", Toast.LENGTH_LONG).show()
                pause = false
                mCaptureSession?.setRepeatingRequest(mCaptureRequest, mCaptureCallback,
                        mBackgroundHandler)
            }
        }
    }

もし撮影した画像を採用しない場合や後述の処理で Watson Visual Recognition のレスポンスで該当商品と判断されなかった場合は mCaptureSession?.setRepeatingRequest(mCaptureRequest, mCaptureCallback,mBackgroundHandler) を実行されることで、キャプチャセッションをやり直すことができます。以上でカメラアプリの実装の説明は終了です。完全なアプリケーションとして動作確認したい場合は、サンプルコードも参照して実装に挑戦してみてください。(activity as CameraFragmentInterface).onCameraFragmentInteraction(this) はこのフラグメントのインターフェースメソッドの呼び出しの実装の詳細は 6章で説明します。以上で画像を読み出す処理を実装することができました。

5.5 Amazon Advertising API で検索する

5.5.1 フラグメントを切り替えて画面遷移させる

CameraFragment は撮影を終了し検索クエリを得た段階で役割を終えるため、検索結果のビューへ遷移できるようにしたいと思います。画面遷移のメソッドはアクティビティクラスのインスタンスから呼び出さなければなりません。ここで、問題になるのは、フラグメントの中から Activity クラスのインスタンスを呼び出して遷移を実装しようとすると遷移先をフラグメント側のコードで実装しなければならず、フラグメント内で次の遷移先が定まった状態になってしまうため、遷移先が定まっている特定のアクティビティでしか使えないフラグメントになってしまいます。もちろんフラグメントの遷移先を必ず同一にしたい場合であれば、そのような実装をしてもかまわないのですが、多くの場合、フラグメント内のコードから次のフラグメントの遷移先が決められてしまうのはよいことではありません。

そこでインターフェースを実装してこのフラグメントを呼び出している Activity クラスに遷移先を指定するためのインスタンスメソッド onCameraFragmentInteraction(fragment:CameraFragment) を実装することを強制することにします。

interface CameraFragmentInterface {
        // TODO: Update argument type and name
        fun onCameraFragmentInteraction(fragment:CameraFragment)
    }

このサンプルコードでは CameraFragmentInterface をアクティビティに継承させることによって、CameraFragment を利用するときは遷移先を任意に選ぶこと、あるいは、遷移させずに処理を継続させることを強制させています。このように、インターフェースを利用し、アクティビティによって異なる分岐処理をフラグメントではなくアクティビティ側のコードに委ねることでアクティビティ独自の処理がフラグメントに対して疎結合に保たれ、フラグメントの汎用性が非常に高くなります。5章で登場した (activity as CameraFragmentInterface).onCameraFragmentInteraction(this) のメソッドは MainActivity では以下のように実装しています。ここでは、検索クエリを次のフラグメントに putString で渡して画面を遷移させています。フラグメントのインスタンスはインターフェースメソッドの引数に取ることで、アクティビティに簡単に渡すことができます。

  override fun onCameraFragmentInteraction(fragment: CameraFragment) {
      this.runOnUiThread({
          var itemsFragment = ItemsFragment()
          var args = Bundle()
          args.putString("query",fragment.resultQuery)
          itemsFragment.arguments = args
          fragmentManager.beginTransaction()
                  .replace(R.id.Container,itemsFragment,"camera")
                  .addToBackStack("camera")
                  .commit()
      })
  }

5.5.2 Amazon Product Advertising API で検索する

遷移先の Fragment でどのように ListView を初期化するかはそれを使うアクティビティに依存しているため、List の初期化を行う onListFragmentInit(fragment:ItemsFragment,adapter: RecyclerView) というメソッドを実装するインターフェースを作成し、アクティビティに継承させ、実装させます。

アクティビティでの実装は以下のようになります。AmazonApi.search は検索結果の XML を取得できる URL 文字列を認証用パラメータを結合した状態で返します。詳しい実装についてはサンプルコードと Amazon Prouduct Advertise API のドキュメントを御覧ください。また、このコードでは Amazon Product Advetising API に対するエンドポイントに HTTP GET リクエストを行うために [Fuel] というライブラリを使用しています。

    override fun onListFragmentInit(fragment:ItemsFragment,adapter: RecyclerView) {
        if(fragment.arguments.containsKey("query")){
            var query = fragment.arguments.getString("query")
            val requestUrl = AmazonApi.search(listOf(query))
            requestUrl?.httpGet()?.responseString { request, response, result ->
                when (result) {
                    is Result.Failure -> {
                        Toast.makeText(this@MainActivity,"検索結果が見つかりませんでした",Toast.LENGTH_LONG).show()
                        Log.d("Error",result.component2().toString());
                    }
                    is Result.Success -> {
                        fragment.item_list = Item.ItemXmlParser.getParseItems(result.getAs<String>() as String)
                        fragment.setDataToRecyclerView(adapter)
                    }
                }
            }
        }
    }

上記のように検索結果が見つかった場合は検索結果の XML ファイルを、Item.ItemXmlParser によってパースして後述の ItemsFragment の RecyclerView に渡します。Item.ItemXmlParser の実装については次節で説明します。

5.5.3 Amazon Product Advertising API の検索結果の XML をパースする

XML のパーサーは公式でも推奨されている org.xmlpull.v1.XmlPullParser を使用します。これをラップした Item.ItemXmlParser の実装は以下のようになっています。

class Item{
    var title:String
    var link:String
    var image:String

    constructor(title:String,link:String,image:String ) {
        this.title = title
        this.link = link
        this.image = image
    }
    object ItemXmlParser {

        fun getParseItems(xml:String): List<Item>? {
                var parser = Xml.newPullParser()
                parser.setInput(StringReader(xml))
                parser.nextTag()
                return readFeed(parser)
        }

        fun readFeed(parser:XmlPullParser) :MutableList<Item>{
            var entries =  mutableListOf<Item>()
            searchTag(parser,"Items")
            parser.require(XmlPullParser.START_TAG, ns, "Items")
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.eventType != XmlPullParser.START_TAG) {
                    continue;
                }
                var name = parser?.name
                // Starts by looking for the entry tag
                if (name.equals("Item")) {
                    entries.add(readItem(parser));
                } else {
                    skip(parser);
                }
            }
            return entries;
        }

        private val ns: String? = null

        fun readItem(parser: XmlPullParser): Item {
            parser.require(XmlPullParser.START_TAG, ns, "Item")
            var title: String? = null
            var image: String? = null
            var link: String? = null
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.eventType != XmlPullParser.START_TAG) {
                    continue
                }
                val name = parser.name
                if (name == "ItemAttributes") {
                    title = readTitle(parser)
                } else if (name == "MediumImage") {
                    image = readImageUrl(parser)
                } else if (name == "DetailPageURL") {
                    link = readLink(parser)
                } else {
                    skip(parser)
                }
            }
            return Item(title!!, link!!, image!!)
        }

        @Throws(IOException::class, XmlPullParserException::class)
        private fun readTitle(parser: XmlPullParser): String {
            var depth = searchTag(parser,"Title")
            parser.require(XmlPullParser.START_TAG, ns, "Title")
            val title = readText(parser)
            while(parser.next() != XmlPullParser.END_TAG || parser?.name != "ItemAttributes"){

            }
            parser.require(XmlPullParser.END_TAG, ns, "ItemAttributes")
            return title
        }

        @Throws(IOException::class, XmlPullParserException::class)
        private fun readLink(parser: XmlPullParser): String {
            parser.require(XmlPullParser.START_TAG, ns, "DetailPageURL")
            var link = readText(parser)
            parser.require(XmlPullParser.END_TAG, ns, "DetailPageURL")
            return link
        }

        @Throws(IOException::class, XmlPullParserException::class)
        private fun readImageUrl(parser: XmlPullParser): String {
            var depth = searchTag(parser,"URL")
            parser.require(XmlPullParser.START_TAG, ns, "URL")
            val imageUrl = readText(parser)
            while(parser.next() != XmlPullParser.END_TAG || parser?.name != "MediumImage"){

            }
            parser.require(XmlPullParser.END_TAG, ns, "MediumImage")
            return imageUrl
        }

        // For the tags title and imageUrl, extracts their text values.
        @Throws(IOException::class, XmlPullParserException::class)
        private fun readText(parser: XmlPullParser): String {
            var result = ""
            if (parser.next() == XmlPullParser.TEXT) {
                result = parser.text
                parser.nextTag()
            }
            return result
        }

        @Throws(XmlPullParserException::class, IOException::class)
        private fun searchTag(parser: XmlPullParser,name:String) :Int{
            if (parser.eventType != XmlPullParser.START_TAG) {
                throw IllegalStateException()
            }
            var depth = 1
            while (depth != 0) {
                if(parser?.name == name) return depth
                when (parser.next()) {
                    XmlPullParser.END_TAG -> depth--
                    XmlPullParser.START_TAG -> depth++
                }
            }
            return depth
        }

        @Throws(XmlPullParserException::class, IOException::class)
        private fun skip(parser: XmlPullParser) {
            if (parser.eventType != XmlPullParser.START_TAG) {
                throw IllegalStateException()
            }
            var depth = 1
            while (depth != 0) {
                when (parser.next()) {
                    XmlPullParser.END_TAG -> depth--
                    XmlPullParser.START_TAG -> depth++
                }
            }
        }

    }
}

XmlPullParser は、next() を実行すると Xml 文字列を一行ずつ読み込んでいき、操作可能な値を包含するイテレータを返します。返されたイテレータの eventType は XmlPullParser.START_TAG,XmlPullParser.END_TAG,XmlPullParser.TEXT のいずれかに分類され、行が読み込まれた段階で、それが開始タグなのか、終了タグなのか、innerText なのかを調べることができます。また、それがタグであれば、そのタグの名前を name プロパティで取得することができます。以上の name プロパティと eventType を使って直近の開始タグと終了タグを指定し、そのタグの中身を抽出することができます。Amazon Advertising API のレスポンスには Item タグの中に ItemAttributes, MediumImage, DetailPageURL の 3つのタグがあり、今回はこのタグの中に表示したいデータとクリック時に遷移したい URL があります。そこで、これらのタグを見つけるまでは、イテレータを進め続け、欲しいタグを見つけたらそのタグの中を走査し、抽出したい innerText を持つタグの中身を抽出します。requre メソッドは、現在のイテレータの位置の値が、条件を満たしているかどうか確認するために使用します。これによって、メソッドを抜けた時にきちんとイテレータが所望の位置に存在しているかどうかを検証することができます。最終的には画面に表示できるように Item クラスにパースし、リストに保存します。次章では、リストに保存したアイテムを画面に表示させる実装を行います。

5.6. Amazon から取得した商品リストを表示する

5.6.1 商品を表示するためのフラグメントを作成する

Android アプリにリストを表示させるには ListView あるいは、RecyclerView を使います。画面サイズに収まる静的なリストには ListView が適しており、スワイプでアイテムをスクロールしたいか動的にデータを取得したい時や、レイアウトの柔軟な変更を行いたい、データのキャッシュを有効にしたい時には、RecyclerView が適しています。Amazon の商品数は検索結果によって、可変であり、かつデータを逐次的に読み込んだ方が画面が見やすくなるため、このチュートリアルでは RecyclerView を使用します。RecyclerView を使用するためには、build.gradle の dependencies に以下を追記します。

compile 'com.android.support:recyclerview-v7:26.+'

そして使いたいレイアウトファイルに以下を追記します (id は任意)。

    <android.support.v7.widget.RecyclerView
        android:id="@+id/item_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </android.support.v7.widget.RecyclerView>

今回は、ItemList の表示部分もフラグメントを使いまわしたいので、layout/fragment_items.xml を以下のように作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/item_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </android.support.v7.widget.RecyclerView>

</LinearLayout>

そして、ItemsFragment を作成します。

import android.app.Fragment
import android.content.Context
import android.os.Bundle
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class ItemsFragment : Fragment(),ItemsRecyclerViewAdapter.ItemViewInterface{
    override fun onAdaptorInteraction(item: Item) {
        (activity as OnFragmentInteractionListener).onListItemClick(this,item)
    }

    private var mColumnCount = 1
    var item_list :List<Item>?=null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val root = inflater!!.inflate(R.layout.fragment_items, container, false)
        val rview = root.findViewById<RecyclerView>(R.id.item_list) as RecyclerView
        (activity as OnFragmentInteractionListener).onListFragmentInit(this,rview)
        return root
    }

    fun setDataToRecyclerView(rview: RecyclerView) {
        if (rview is RecyclerView) {
            val context = rview.context
            if (mColumnCount <= 1) {
                rview.layoutManager = LinearLayoutManager(context)
            } else {
                rview.layoutManager = GridLayoutManager(context, mColumnCount)
            }
            rview.adapter = ItemsRecyclerViewAdapter(item_list!!,this)
        }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
    }

    override fun onDetach() {
        super.onDetach()
    }

    interface OnFragmentInteractionListener {
        fun onListFragmentInit(fragment:ItemsFragment,adapter: RecyclerView)
        fun onListItemClick(fragment:ItemsFragment,item:Item)
    }

}

ここで着目して欲しいポイントは,以下のインターフェース

    interface OnFragmentInteractionListener {
        fun onListFragmentInit(fragment:ItemsFragment,rview: RecyclerView)
        fun onListItemClick(fragment:ItemsFragment,item:Item)
    }

によって、Item のリストのデータを RecyclerView にいれる処理を onListFragmentInit、リストのアイテムがクリックされた時の処理を onListItemClick と定義して、実装をアクティビティに任せていることです。これもフラグメントの汎用性を保つため、データの扱い方をフラグメントに実装しないようにしています。さらに ItemsRecyclerViewAdapter.ItemViewInterface によってアイテムクリック時に実行される onAdaptorInteraction メソッドは onListItemCheck を実行するメソッドにオーバーライドされ、ItemsFragment->ItemsRecyclerViewAdapter を経ることによってアクティビティのメソッドの実装が Item 一つ一つの UI のクリックイベントに紐づけられることになります。 `rview.adapter = ItemsRecyclerViewAdapter(item_list!!,this)` によって RecylerView にリストのデータが紐づけられます。リストのデータを UI に紐づける場合は、このように ViewAdaptor クラスを作成します。次の章では、どのように ViewAdaptor を実装しているか見てみます。

5.6.2 商品リストのデータを UI に紐づける

では、アダプターの実装を見ていきましょう。RecyclerView に紐づける View は、item_view.xml というファイルに以下のように記述しています。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_view"
    android:background="@drawable/item_list"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <FrameLayout
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_weight="1"
        android:background="@drawable/image_frame">

        <ImageView
            android:id="@+id/item_image"
            android:layout_gravity="center"
            android:layout_width="148dp"
            android:layout_height="148dp" />
    </FrameLayout>


    <FrameLayout
        android:layout_width="233dp"
        android:layout_height="150dp"
        android:layout_weight="19.21">


        <TextView
            android:id="@+id/item_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left|center"
            android:layout_margin="@dimen/text_margin"
            android:textAppearance="?attr/textAppearanceListItem"
            android:textSize="16dp" />

    </FrameLayout>


</LinearLayout>

RecyclerView に表示される商品一個ぶんのレイアウトで、アイテムが追加されるたび、このレイアウトファイルが複製され、それぞれの商品のデータとイベントが設定されます。次に ItemsRecyclerViewAdapter.kt の実装を見てみます。以下のコードでは、画像の URL を簡単に ImageView にロードするライブラリとして [Picasso] というライブラリを使用しています。

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.support.v7.widget.RecyclerView
import com.squareup.picasso.Picasso

class ItemsRecyclerViewAdapter(private val mValues: List<Item>, private val mListener: ItemViewInterface?) : RecyclerView.Adapter<ItemsRecyclerViewAdapter.ViewHolder>(){

    interface ItemViewInterface {
        fun onAdaptorInteraction(item:Item)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false) as View
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.mItem = mValues[position]
        holder.mTitle.text = mValues[position].title
        holder.mLink = mValues[position].link

        Picasso.with(holder.mImageView!!.context).load(holder?.mItem?.image).into(holder.mImageView)
        holder.mView.setOnClickListener {
            if (null != mListener) {
                // Notify the active callbacks interface (the activity, if the
                // fragment is attached to one) that an item has been selected.
                mListener!!.onAdaptorInteraction(holder.mItem as Item)
            }
        }
    }

    override fun getItemCount(): Int {
        return mValues.size
    }

    inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
        var mTitle: TextView = mView.findViewById<TextView>(R.id.item_name)
        var mItem: Item? = null
        var mImageView: ImageView? =null
        var mLink: String? = null

        init {
            mImageView = mView.findViewById<ImageView>(R.id.item_image)
        }

    }
}

RecyclerView.Adaptor には holder 生成した数の情報と、そのインデックスが保存されています。ユーザーが RecyclerView を開くと画面に映せるアイテム数分だけデータの Lazy Load が始まりスワイプなどで画面外のアイテムを表示させようとした際に、アイテムがまだ存在していて UI が作られていなければ、onCreateViewHolder イベントが呼ばれます。このとき、戻り値に自作の ViewHolder クラスを作成して返すと、onBindViewHolder 実行時に、表示されている ViewHolder のインデックスを利用して、アイテムリストのインデックスの位置にあるデータを結びつけることができます。以降、キャッシュが利用され、メモリの消去のたびに onBindViewHolder が呼ばれてデータが紐づくようになります。

*ここまでで、ItemsRecyclerViewAdapter.ItemViewInterface は adaptor 間で共通化すれば、Adaptor の種類に寄らず、ItemsFragment を使いまわせるのではないかと気づいた方は素晴らしい着眼点を持っています。ItemsRecyclerViewAdapter.ItemViewInterface は便宜上、ItemsRecyclerViewAdapter クラスに実装していますが、AdaptorInterface を独立のファイルに実装して、引数の Item 型もインターフェースで抽象化してしまえば、アダプターとモデルの種類によらず、ItemsFragment の実装を使いまわせることになり、リストのデータ構造に変更がある場合でもレイアウトと、アダプターの実装のみを変更するか、新しいアダプターを追加するだけで済むようになります。Android アプリ開発ではこのような着眼点で抽象化のレイヤーをいかにうまく使えるかによってコードの実装量や保守性が変わってきます。

5.6.3 商品リストをクリックした際に飛べるようにする

商品リストをクリックした際に飛べるようにする実装は Interface を介して MainActivity に実装させます。onListItemClick がリストのアイテムをクリックした時の処理と定義しているので、

    override fun onListItemClick(fragment: ItemsFragment, item: Item) {
        var uri = Uri.parse(item.link);
        var i = Intent(Intent.ACTION_VIEW,uri);
        startActivity(i)
    }

と実装することで、暗黙的インテントにより、ブラウザなどを開いて Amazon のリンクに飛ぶことができるようになります。この時、アクティビティは finish していないため、Back ボタンでアプリに戻ることもできます。

以上で画像を Watson Visual Recognition に送信してから Amazon で検索することができるようになりました。

画像検索を作るためのチュートリアルはこれで終了になります。

6

まとめ

いかがだったでしょうか?今回は画像を集める方法として自分で認識させたい物体を撮影するという手段を取りましたが、外部の API などで画像を収集する作業を半自動化することもできます。この時、ある程度本来認識させたい物体ではないものが混ざることがありますが、Watson Visual Recognition で同じ物体ではないと思われる画像をある程度排除することが出来ます。また、今回は商品検索の手段として画像の判定結果の文字列を検索に使うという方法を取りましたが、Watson Visual Recognition API には、送信した画像と今まで学習した画像の類似度を計算して検索できる API があります。もし、ご自身の EC サイトを運用する際には以上の Android カメラの実装と Watson Visual Recognition を組み合わせて魔法のような画像検索システムを作れます。非常に便利な機能なのでぜひ挑戦してみてはいかがでしょうか。以上、お読みいただきありがとうございました。この記事がお読みいただいた皆さんの Kotlin・Android 開発のお力になれれば幸いです。


ダウンロード可能なリソース


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=1048462
ArticleTitle=Visual Recognition + Kotlin で撮影した画像で商品検索が出来る Android アプリを作ろう
publish-date=08182017