駅すぱあと x Cognitive Services で 画像DE路線当てBOT を作ろう! [Bot実装 (C# ver) 編]

  • 1
    Like
  • 0
    Comment

Microsoft Cognitive ServicesCustom Vision Service は、オリジナルの画像判定エンジンを作成して API で推定値を取得できるサービスです。また、駅すぱあとwebサービス は、国内初の乗換案内ソフト 駅すぱあと がもつ様々な情報をWebAPIで提供しているサービスです。

今回は Custom Vision Service で作成した画像判定エンジンを利用して、
鉄道の路線を画像から判別し、 駅すぱあとwebサービス を利用して路線に含まれる駅を検索し、それを返す BOT を作成する方法を紹介します。

Microsoft Cognitive Services の Custom Vision Service は、オリジナルの画像判定エンジンを作成して API で推定値を取得できるサービスです。
機械学習などで画像判定ロジックを構築しなくても、画像をアップロードしてタグ付けを行うことで、画像判定エンジンを構築できます。

駅すぱあとwebサービス は、国内初の乗換案内ソフト 駅すぱあと がもつ様々な情報をWebAPIで提供しているサービスです。
経路探索はもちろん、駅データや時刻表などの情報を取得することができます。
無料で使えるフリープランを用意しており、企業にかぎらず個人でもご利用いただけます。

ベースは Microsoft Bot Framework (V3.0 以降) テンプレートを利用し、Cognitive Services Custom Vision (Prediction) C# クラスライブラリーを用いて呼び出しを行います。

この BOT アプリは Bot Framework Channel Emulator を使ってローカル環境で稼働確認することが可能です。また、Web 公開 & Bot Framework に登録すると、埋め込み可能な Web Chat が利用できます。

駅すぱあと x Cognitive Services で 画像DE路線当てBOT を作ろう!
(1) Custom Vision 編
(2) Bot実装編
- Node.js ver 編
- C# ver 編 ※このページ

Custom Vision 編、Bot実装 編を通して作成できる 画像DE路線当てBOT↓

Bot Framework & Cognitive Services 利用に必要な環境、サブスクリプションの準備

開発環境

Visual Studio、Bot Framework Template & Bot Framework Channel Emulator

無償の Visual Studio 2017 Community or 2015 Community でOKなので、既存の環境がない場合は、ダウンロードしてインストールします。
Visual Studio 2017 Community ダウンロードサイト
Visual Studio 用の Bot Framework C# テンプレート
Bot Framework Channel Emulator (Windows版) ※Mac/Linux は Console版 をご利用ください

Bot Framework 開発環境の作り方は、Microsoft Bot Framework v3.0 からはじめる BOT 開発: Bot Framework を使うための開発環境 をご覧ください。

駅すぱあと Webサービス のアクセスキー

駅すぱあと Webサービス のフリープランをお申込みいただき、アクセスキーを取得してください。
https://ekiworld.net/service/sier/webservice/free_provision.html

Custom Vision 編 を実施していない場合

Custom Vision 編 を実施していない場合はMicrosoft アカウントを準備してください。([Custom Vision 編] と同じもので問題ありません。)

Microsoft アカウント

Cognitive Services (と必要に応じて Azure) サブスクリプションの申し込みに必要ですので、持っていない場合は取得しておきます。

Microsoft アカウント登録手続き

BOT アプリの実装

Bot アプリケーションの作成

Visual Studio テンプレートから Bot アプリケーションの作成 と同じ手順で、新規 Bot アプリケーションを作成します。今回は TrainFinderBot という名称で作成しています。

Cognitive Services Emotion API の C# ライブラリーのインストール

ソリューションエクスプローラーでプロジェクト名 (ソリューションの下) を右クリックして、NuGet パッケージの管理 を選択します。

このソリューションに一部のNuGetパッケージが見つかりません・・・ というメッセージが表示される場合は、その横にある [復元] をクリックしてインストールします。(表示されない場合はそのまま次に進んでください)

参照 をクリックし、custom vision と入力して検索します。Microsoft.Cognitive.CustomVision.Prediction を選択し、インストール をクリックしてインストールします。

Custom Vision (Prediction) のライブラリーと、依存関係のあるライブラリーが合わせて表示されますので、OK をクリックしてインストールします。

インストールが終了したら、NuGet のウインドウを閉じます。

会話のハンドリングの記述 (基本)

Dialogs フォルダー をクリックして開きます。RootDialog.cs をクリックして表示し、こちらを編集していきます。RootDialog.cs にはメッセージを受信したときの動作を記述します。

冒頭に、先ほど追加した Microsoft.Cognitive.CustomVision を追加します。合わせて System.Net.Http も追加しておきます。

RootDialog.cs
using Microsoft.Cognitive.CustomVision;
using System.Net.Http;

初期メッセージの追加

メッセージを受信したとき最初に StartAsync が呼びだされ、StartAsync から MessageReceivedAsync が呼び出されます。
MessageReceivedAsync 呼び出し前に初期メッセージを追加します。

RootDialog.cs
public Task StartAsync(IDialogContext context)
{
    // デフォルトのメッセージをセット
    context.PostAsync($"こんにちは!画像DE路線当てBot です。");
    context.Wait(MessageReceivedAsync);
    return Task.CompletedTask;
}

Custom Vision を呼び出す準備

MessageReceivedAsync に Custom Vision の画像分析エンジンを呼び出すコードを追加していきます。
下記コードの YOUR_PREDICTION_KEY には Custom Vision の Prediction URL 表示画面で表示される Prediction-key、YOUR_PROJECT_ID には URL に含まれている Project ID をコピーします。
Custom Vision 編 の API アクセス情報画面から取得できます。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;

    // 変数定義
    string tag = "";     // 電車カテゴリータグ
    string msg = "";     // 返答メッセージ

    // Custom Vision API を使う準備
    var cvCred = new PredictionEndpointCredentials("YOUR_PREDICTION_KEY");
    var cvEp = new PredictionEndpoint(cvCred);
    var cvGuid = new Guid("YOUR_PROJECT_ID");

    // 画像が送られてきたら Custom Vision を呼び出してタグを取得
    // メッセージをセット
    // ※次以降の項目で作成します

    await context.PostAsync(msg);
    context.Wait(MessageReceivedAsync);
}

Custom Vision の呼び出し、結果の取得

画像を Stream として取得し、Custom Vision を呼び出します。Probability (≒信頼度) の高いものからリストされるため、最初にリストされるタグのみを取得します。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    :
    中略
    :
    // 画像が送られてきたら Custom Vision を呼び出してタグを取得
    if (activity.Attachments?.Count != 0)
    {
        // 送られてきた画像を Stream として取得
        var photoUrl = activity.Attachments[0].ContentUrl;
        var client = new HttpClient();
        var photoStream = await client.GetStreamAsync(photoUrl);

        try
            {
                // 画像を判定
                var cvResult = await cvEp.PredictImageAsync(cvGuid, photoStream);

                // タグを取得
                tag = cvResult.Predictions[0].Tag;
            }
            catch
            {
                // Error Handling
            }
    }

    // メッセージをセット
    // ※次以降の項目で作成します

    await context.PostAsync(msg);
    context.Wait(MessageReceivedAsync);
}

返答の作成

返答メッセージにタグをセットします。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    :
    中略
    :
    // メッセージをセット
    if (tag != "")
    {
        msg = tag + "、に いちばんにてるね!";
    }
    else
    {
        // 判定できなかった場合
        msg = "電車の写真を送ってね";
    }

    await context.PostAsync(msg);
    context.Wait(MessageReceivedAsync);
}

忘れずに RootDialog.cs を保存しておきます。

アプリケーションの動作確認

ここで一旦 BOT の動作確認を行います。、F5 または デバック>デバックの開始 をクリックして、プロジェクトのビルドおよび起動を行います。ブラウザが起動して Bot Framework のデフォルト画面が表示されたら、Bot Framework Channel Emulator を起動してアクセスを行います。
Bot Framework Channel Emulator の上部中央にある Bot Url に、起動しているブラウザと同じ URL (デフォルトでは http://localhost:xxxx) に /api/messages を追加したアドレス (http://localhost:xxxx/api/messages) を指定します。

何か文字を入力して送信すると、StartAsync で指定したデフォルトの回答が返信されることを確認してください。
入力エリアの画像アイコンをクリックして、分析した画像を選択、BOT に送信すると、判定されたタグが返信されるのを確認してください。

会話のハンドリングの記述 (駅すぱあと Web サービス を利用した情報の取得とセット)

画像を判定して取得できたタグに応じてメッセージを変更します。
今回は、駅すぱあと Web サービス を利用して、路線名から停車駅を取得し、表示するようにします。

路線名、停車駅はこちらの API を利用して取得できます。今回は予め路線コードはアプリ内に保持し、停車駅のみを API で取得しています。
- 路線名の取得: http://api.ekispert.jp/v1/xml/operationLine?name=路線名&key=アクセスキー
- 停車駅の取得: http://api.ekispert.jp/v1/json/station?&operationLineCode=路線コード&key=アクセスキー

停車駅を取得する準備

駅すぱあと API で取得した情報は XML または JSON で取得できます。
今回は JSON でデータを取得し、その後利用しやすくするためのクラスを作成します。

プロジェクトを右クリックして [追加]>[新しいフォルダー] をクリック、Models というフォルダーを作成します。

作成した Models フォルダーを右クリックして、[追加]>[クラス]をクリックします。

名前に StationModel.cs と入力して、[追加] をクリックして新規クラスファイルを作成します。

StationModel.cs を開き、デフォルトで作成されている StationModel クラスを一旦削除します。

API のフォーマットを取得するため、ブラウザーを起動して API にアクセスを行います。
http://api.ekispert.jp/v1/json/station?&operationLineCode=113&direction=down&key=アクセスキー

表示される結果をすべて選択して、コピーします。

Visual Studio に戻り、namespace .Models 内をクリックして選択してから、上部ツールバーから [編集]>[形式を選択して貼り付け]>[JSON をクラスとして貼り付ける] を選択して、コピーした内容を貼り付けます。

class Rootobjectclass StationModel に変更します。

適時ファイルの保存を行っておきます。

路線名、路線コードのセット

画像を判定して取得できたタグによって、路線名と路線コードをセットします。
まず、変数定義のところで、路線名と路線コードを扱う変数 (lineName, lineCode) を追加します。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;

    // 変数定義
    string tag = "";     // 電車カテゴリータグ
    string msg = "";     // 返答メッセージ
    string lineName = "";   // [追加] 路線名 (駅すぱあと運航路線名)
    string lineCode = "";   // [追加] 路線コード (駅すぱあと運航路線コード)
    :
    (後略)

次に取得したタグに応じて lineName と lineCode をセットするように変更します。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;
    :
    中略
    :
    // メッセージをセット
    if (tag != "")
    {
        //msg = tag + "、に いちばんにてるね!";
        // タグに応じてメッセージをセット
        switch (tag)
        {
            case "Chuo_Sobu":
                lineName = "JR中央・総武線各駅停車";
                lineCode = "110";
                break;
            case "Chuo_Ex":
                lineName = "JR中央線快速";
                lineCode = "109";
                break;
            case "Keihin-Tohoku":
                lineName = "JR京浜東北線";
                lineCode = "115";
                break;
            case "Tokaido":
                lineName = "JR東海道本線";
                lineCode = "117";
                break;
            case "Yamanote":
                lineName = "JR山手線";
                lineCode = "113";
                break;
            case "Yokosuka_SobuEx":
                lineName = "JR横須賀線";
                lineCode = "116";
                break;        
        }

        // 路線情報を取得してセット
        // ※次以降の項目で作成します
    }
    else
    {
        msg = "電車の写真を送ってね";
    }


    await context.PostAsync(msg);
    context.Wait(MessageReceivedAsync);
}

lineCode を使って停車駅情報を取得して (line に代入)、表示する内容に変更します。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;
    :
    中略
    :
    // メッセージをセット
    if (tag != "")
    {
        //msg = tag + "、に いちばんにてるね!";
        // タグに応じてメッセージをセット
        switch (tag)
        {
            case "Chuo_Sobu":
                lineName = "JR中央・総武線各駅停車";
                lineCode = "110";
                break;
            :
            中略
            :
        }

        // 路線情報を取得してセット
        var list = await GetStationList(lineCode); // ※GetStationList は次以降の項目で作成
        msg = lineName + "、に いちばんにてるね!\n\n" 
            + lineName + "は、以下のえきをはしるよ。\n\n---- - \n\n"
            + list;
    }
    else
    {
        msg = "電車の写真を送ってね";
    }


    await context.PostAsync(msg);
    context.Wait(MessageReceivedAsync);
}

停車駅の取得とセット

停車駅を取得してセットします。
JSON を扱えるように Newtonsoft.JSON と、前の項目で作成した Model を RootDialog.cs の冒頭に追加しておきます。

RootDialog.cs
using Newtonsoft.Json;
using TrainFinderBot.Models;

GetStationList を追加し、路線情報を取得します。
取得できた停車駅は "→" で繋いで 1 行にします。

RootDialog.cs
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    :
    中略
    :
}
private async Task<string> GetStationList(string lineCode)
{
    var client = new HttpClient();

    // 路線名、アクセスキーをセット
    var ekiRequest = "http://api.ekispert.jp/v1/json/station?&operationLineCode="
                + lineCode
                + "&direction=down&key="
                + "YOUR_ACCESSKEY"; //アクセスキー

    // 路線情報の取得
    var ekiResult = await client.GetStringAsync(ekiRequest);
    var ekiStr = Uri.UnescapeDataString(ekiResult.ToString());
    var ekiModel = JsonConvert.DeserializeObject<StationModel>(ekiStr);

    // 停車駅情報を編集
    var stationList = "";
    foreach (var point in ekiModel.ResultSet.Point)
    {
        stationList = stationList + "→" + point.Station.Name;
    }
    stationList = stationList.Substring(1);

    return stationList;

}

アプリケーションの動作(再)確認

F5 または デバック>デバックの開始 をクリックして、プロジェクトのビルドおよび起動を行います。ブラウザが起動して Bot Framework のデフォルト画面が表示されたら、Bot Framework Channel Emulator を起動してアクセスを行います。

入力エリアの画像アイコンをクリックして、分析した画像を選択、BOT に送信します。
設定したメッセージが返答されたら完成です。

Appendix

完成形のソースコードを GitHub にて公開しました。
https://github.com/a-n-n-i-e/CognitiveCustomVision-TrainFinderBot