ぴよぴよエンジニアの日記

クラウドベンダーに勤める見習いSEの日記です。発言は私自身の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Xamarin で Watson Visual Recognition を使ってみた

IBM Watson には .NET Standard 用の SDK があります(α版).

santea.hateblo.jp


しかし、残念ながら現状では Xamarin で使用することはできません.
(Issue を上げたのでそのうち改善されるかと思います)
Is it possible to support Xamarin? · Issue #91 · watson-developer-cloud/dotnet-standard-sdk · GitHub


そこで今回は、Web API を直接叩く形で Watson API を Xamarin で使ってみます.

使用する Watson API は画像認識を行う Visual Recognition で、
画像中の文字を認識する Recognize Text を使ってみます.





Bluemix 上で Watson Visual Recognition の作成

Bluemix で サービズの中の Watson を選びます.

f:id:Santea:20170502231917p:plain:w280


Watson サービスの作成ボタンをクリックします.

f:id:Santea:20170502232103p:plain:w200


一覧の中から Visual Recognition を選びます.

f:id:Santea:20170502232658j:plain


価格プランを選んで作成ボタンを押します.

f:id:Santea:20170502233233p:plain


作成したインスタンスのサービス資格情報の中の API KEY を控えておきましょう.

f:id:Santea:20170502234201j:plain


以上が Bluemix 上の操作です.



Xamarin の実装

Xamarin の実装として、今回はカメラで撮影した画像に対し文字認識を行います.

以下 MainPage のコードビハインドです.

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Plugin.Media;
using Plugin.Media.Abstractions;
using Xamarin.Forms;

namespace FlipChartRecognitionApp
{
	public partial class FlipChartRecognitionAppPage : ContentPage
	{
		private static readonly string Url = "https://gateway-a.watsonplatform.net/visual-recognition/api/v3/recognize_text?api_key=...&version=2016-05-20";

		public FlipChartRecognitionAppPage()
		{
			InitializeComponent();
		}

		public async void Handle_Clicked(object sender, EventArgs e)
		{
			await CrossMedia.Current.Initialize();

			if (!CrossMedia.Current.IsCameraAvailable || !CrossMedia.Current.IsTakePhotoSupported)
			{

				await DisplayAlert("No Camera", "No camera available.", "OK");
				return;
			}

			var file = await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions
			{
				Directory = "Sample",
				Name = $"{DateTime.Now.ToString("yyMMdd-hhmmss")}.jpg",
				PhotoSize = PhotoSize.Small
			});

			if (file == null)
				return;

			RecognizeImage(file);

			image.Source = ImageSource.FromStream(() =>
			{
				var stream = file.GetStream();
				file.Dispose();
				return stream;
			});
		}

		void RecognizeImage(MediaFile file)
		{
			var content = new MultipartFormDataContent();

			var imageContent = new ByteArrayContent(ReadFully(file.GetStream()));
			imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg");
			content.Add(imageContent);

			var httpClient = new HttpClient();
			var response = httpClient.PostAsync(Url, content).Result;

                       DisplayAlert("Result", response.Content.ReadAsStringAsync().Result, "OK");
		}

		byte[] ReadFully(Stream input)
		{
			using (MemoryStream ms = new MemoryStream())
			{
				input.CopyTo(ms);
				return ms.ToArray();
			}
		}
	}
}

ひとつずつ解説していきます.

コールする API は下記のようになっているので、{API KEY} の部分を先ほど作成したインスタンスAPI KEY に書き換えてください. 

private static readonly string Url = "https://gateway-a.watsonplatform.net/visual-recognition/api/v3/recognize_text?api_key={API KEY}&version=2016-05-20";


カメラの撮影部分では拡張子に .jpg を指定します.
また、PhotoSize を Samll にしたほうが無難です.
(Medium の場合、何度か送信に失敗しました)

var file = await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions
{
	Directory = "Sample",
	Name = $"{DateTime.Now.ToString("yyMMdd-hhmmss")}.jpg",
	PhotoSize = PhotoSize.Small
});


画像認識部分です.

送信するファイルは、Image を Byte配列に変換した上で ByteArrayContent に渡します.

ByteArrayContent.Headers.ContentType には、先ほど保存した .jpg ファイルと整合を取る形で、image/jpeg を指定します.

MultipartFormDataContent に ByteArrayContent を追加し、HttpClient.PostAsync をコールすれば画像認識が行えます.

void RecognizeImage(MediaFile file)
{
	var content = new MultipartFormDataContent();

	var imageContent = new ByteArrayContent(ReadFully(file.GetStream()));
	imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg");
	content.Add(imageContent);

	var httpClient = new HttpClient();
	var response = httpClient.PostAsync(Url, content).Result;
        
	DisplayAlert("Result", response.Content.ReadAsStringAsync().Result, "OK");
}



実装結果

こちらが実装結果です.

f:id:Santea:20170503001302j:plain

VOX のアンプラグを撮影してみました.

JSON の score という部分は Watson の回答に対する自信度を表しています.

  • VOX のロゴは装飾が入っていて判別しずらい
  • NIGHT は塗装が剥がれて判別しずらい

上記2点が score に表れていますね.
反対に TRAIN はかなりの自信度で判別しているようです.



まとめ

今回は Watson API を Xamarin で使ってみました.

Web API なので特に工夫なく、普通に使えることがお分かりいただけたかと思います.

通信部やモデルを自前で書かないといけないのは多少おっくうですが、
冒頭でも述べたように、近い将来 Xamarin に対応した .NET Standard の SDK がリリースされると思いますので、ぜひそれを楽しみにしていただければと思います!


以上です.

WPF で IBM Watson SDK for .NET Standard を使ってみた(Conversation 編)

santea.hateblo.jp

先日、IBM Watson に .NET Standard 用の SDK があることを紹介しました.

今回はこの SDK の Watson Conversation を使って WPF(Windows Presentation Foundation) のチャットアプリを作ってみます.


www.ibm.com

Watson Conversation の公式ページはこちら.


以下、Bluemix 上の Watson Conversation と、 WPF の実装について説明していきます.



Watson Conversation

https://www.change-makers.jp/technology/11268

Conversation の具体的な使用方法はこちらが分かりやすいです.
多少 UI の変更などありますが、おおよそニュアンスで読み取れる範囲です.

GUI 上でモデルを構築していくのは直観的でそれなりに使いやすいと思います.


f:id:Santea:20170416132821p:plain

今回作ったモデルは非常に簡易なもので、

  1. ピザの種類を聞く
  2. サイズを聞く

この2つしか機能はありません.

この程度ならば5分も掛からず構築できます.簡単!



WPF の実装

www.nuget.org

IBM Watson SDK for .NET Standard は .NET Standard 1.6 用なので、まずは .NET Framework 4.7 の WPF プロジェクトを作りましょう.


f:id:Santea:20170416134123p:plain

.NET Framework 4.7 を選びます.
(一覧に表示されない場合は、"Windows 10 Creators Update" を当てたあと、Visual Studio 2017 に "NET Framework 4.7 開発ツール" を追加してください)


f:id:Santea:20170416134704p:plain

NuGet パッケージマネージャーで IBM.WatsonDeveloperCloud.Conversation を追加します.
(同時に IBM.WatsonDeveloperCloud が追加されます)


f:id:Santea:20170416135245p:plain

素の状態だと実行時に System.Security.VerificationException が発生するので、System.Net.Http を最新版(現時点では 4.3.1)に上げます.


以上で準備は完了です.

以下は具体的な実装になります.



namespace WpfConversationSample
{
    public class MessageModel
    {
        public string Message { get; set; }
        public bool IsUserMessage { get; set; }
    }
}

モデルはメッセージと、ユーザのメッセージか Watson の応答かを判別する IsUserMessage を持ちます.



<Window x:Class="WpfConversationSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfConversationSample"
        mc:Ignorable="d"
        Title="WPF Conversation Sample" Height="350" Width="525">

    <Window.Resources>
        <local:ColorConverter x:Key="ColorConverter" />
        <local:AlignmentConverter x:Key="AlignmentConverter" />
        <DataTemplate x:Key="MessageTemplate">
            <Border Background="{Binding IsUserMessage, Converter={StaticResource ColorConverter}}"
                    CornerRadius="20" 
                    Padding="5" 
                    Margin="5"
                    HorizontalAlignment="{Binding IsUserMessage, Converter={StaticResource AlignmentConverter}}">
                <Grid>
                    <Label Content="{Binding Message}"/>
                </Grid>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <ListBox 
            ItemsSource="{Binding MessageCollection}" 
            ItemTemplate="{StaticResource MessageTemplate}" 
            HorizontalContentAlignment="Stretch" 
            IsSynchronizedWithCurrentItem="True" 
            Background="#7294c2"/>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <TextBox Grid.Column="0"
                     Text="{Binding Message.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Grid.Column="1"
                    Padding="10,0,10,0"
                    Content="Send"
                    Command="{Binding MessageCommand}"
                    CommandParameter="{Binding Message.Value}"/>
        </Grid>
        
    </Grid>

</Window>

View は大きく ListBox, TextBox, Button で構成されています.

モデルの IsUserMessage を ColorConverter と AlignmentConverter で変換することでメッセージの送信者ごとに見た目を変えています.
(ColorConverter と AlignmentConverter の実装は説明を省略します)



using IBM.WatsonDeveloperCloud.Conversation.v1;
using IBM.WatsonDeveloperCloud.Conversation.v1.Model;
using Reactive.Bindings;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.Security;

[assembly: SecurityRules(SecurityRuleSet.Level1, SkipVerificationInFullTrust = true)]
namespace WpfConversationSample
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<MessageModel> MessageCollection { get; set; }
        public ReactiveProperty<string> Message { get; set; }
        public ICommand MessageCommand { get; private set; }
        private ConversationService _conversation;
        private Context _context;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            MessageCollection = new ObservableCollection<MessageModel>();
            Message = new ReactiveProperty<string>();
            MessageCommand = new DelegateCommand<string>(ExecuteMessageCommand);

            _conversation = new ConversationService();
            _conversation.SetCredential("<username>", "<password>");

            // Watson の初期メッセージを受け取るために明示的に呼び出す
            ExecuteMessageCommand("");
        }

        private void ExecuteMessageCommand(string message)
        {
            Message.Value = "";

            if (!string.IsNullOrEmpty(message))
            {
                MessageCollection.Add(new MessageModel
                {
                    Message = message,
                    IsUserMessage = true
                });
            }

            var messageRequest = new MessageRequest()
            {
                Input = new InputData()
                {
                    Text = message
                },
                Context = _context
            };

            var result = _conversation.Message("<workspace-id>", messageRequest);
            _context = result.Context;
            
            MessageCollection.Add(new MessageModel
            {
                Message = result.Output.Text[0],
                IsUserMessage = false
            });

        }
    }
}

今回は ViewModel を用いず、コードビハインドに直書きしました.

Conversation SDK の部分をピックアップして説明します.



_conversation = new ConversationService();
_conversation.SetCredential("<username>", "<password>");

ConversationService の初期化を行います.


f:id:Santea:20170416141019p:plain

ユーザ名とパスワードは Conversation のワークスペース作成画面の値を用います.



private void ExecuteMessageCommand(string message)
{
    var messageRequest = new MessageRequest()
    {
        Input = new InputData()
        {
            Text = message
        },
        Context = _context
    };

     var result = _conversation.Message("<workspace-id>", messageRequest);
    _context = result.Context;
            
    MessageCollection.Add(new MessageModel
    {
        Message = result.Output.Text[0],
        IsUserMessage = false
    });

}

ExecuteMessageCommand の SDK に関わる部分を抜粋しました.

ポイントは MessageRequest 生成時に Context を保持しておいて代入することです.
Context は会話の履歴のようなもので、これが無いと Watson は初期状態のレスポンスを繰り返します.
(今回の例だと「こんにちは!ご注文をどうぞ!」を繰り返します)

ConversationService#Message の "workspace-id" の引数は、先程のワークスペース作成画面の WorkspaceID です.


f:id:Santea:20170416143535g:plain

こちらが実装結果になります.



まとめ

今回は IBM Watson SDK for .NET Standard の Conversation を試してみました.
簡単にチャットボットを作れるのでとても有用だと思います.

SDK としては .NET Standard 1.6 用であるというなかなかに厳しい制約があるので、現状発展途上と言わざるを得ません.
(Xamarin で使えなかったりします)

ただ、.NET Standard は今後まさしくスタンダードな選択肢になっていく(スタンダードになりますよね?)と思うので、その SDK が開発されているのは非常に良いと思います.

次回以降もこの SDK を試していきたいと思います.


以上です.




参考