読者です 読者をやめる 読者になる 読者になる

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

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

フリップチャートを認識するアプリを作ってみた

前回、Xamarin で Watson Visual Recognition の Recognize Text を使うエントリーを書きました.

santea.hateblo.jp


今回はより具体的な使用用途として、フリップチャートに貼ったポストイットを認識するスマートフォンアプリを作ってみます.
ポストイットに書いてある文字と、ポストイットの色をセットで認識します.

アプリの実装は Xamarin.Forms で行います.





Xamarin.Forms の実装

まずは写真を撮影するページの実装です.

写真の撮影には Media Plugin for Xamarin and Windows を用いました.

github.com

また画像の文字領域を切り取るのに @muax__x さんの Image Edit Plugin for Xamarin を使わせていただきました.

github.com


PhotoPage.xaml です.
Button と Image を配置しています.

<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:FlipChartRecognitionApp"
             x:Class="FlipChartRecognitionApp.PhotoPage"
             Title="Photo">

    <StackLayout>
        <Grid HorizontalOptions="FillAndExpand"
              VerticalOptions="FillAndExpand">

            <Label x:Name="label"
                   Text="Please take a picture of the flip chart."
                   VerticalOptions="Center"
                   HorizontalOptions="Center"/>

            <ActivityIndicator x:Name="indicator"
                               IsRunning="True"
                               IsVisible="False"
                               VerticalOptions="Center"
                               HorizontalOptions="Center"/>

            <Image x:Name="image"
                   HorizontalOptions="FillAndExpand"
                   VerticalOptions="FillAndExpand"
                   Margin="5,12,5,0" />
        </Grid>
        <Button BackgroundColor="#2196F3"
                TextColor="White"
                Text="Take Photo"
                Clicked="Handle_Clicked" />
    </StackLayout>

</ContentPage>


PhotoPage.xmal.cs です.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Plugin.ImageEdit;
using Plugin.Media;
using Plugin.Media.Abstractions;
using Xamarin.Forms;

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

        private Texts _texts;
        private List<Word> _words;

        public PhotoPage()
        {
            InitializeComponent();
        }

        public async void Handle_Clicked(object sender, EventArgs e)
        {
            label.IsVisible = false;
            image.Source = null;
            indicator.IsVisible = true;
            _words = new List<Word>();

            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:yyMMdd-hhmmss}.jpg",
                PhotoSize = PhotoSize.Small
            });

            if (file == null)
                return;

            var imageByteArray = ReadFully(file.GetStream());

            RecognizeTexts(imageByteArray);
            AnalyzeColor(imageByteArray);

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

            indicator.IsVisible = false;
        }

        private void RecognizeTexts(byte[] imageByteArray)
        {
            var content = new MultipartFormDataContent();

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

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

            _texts = JsonConvert.DeserializeObject<Texts>(response.Content.ReadAsStringAsync().Result);
        }

        private async void AnalyzeColor(byte[] imageByteArray)
        {
            if (_texts.images.Length == 0 || _texts.images[0].words.Length == 0)
            {
                DisplayAlert("Result ", "Could not recognize character", "OK");
                return;
            }

            foreach (var word in _texts.images[0].words)
            {
                var editableImage = await CrossImageEdit.Current.CreateImageAsync(imageByteArray);

                var pixels = editableImage.Crop(word.location.left,
                        word.location.top,
                        word.location.width,
                        word.location.height)
                    .ToArgbPixels()
                    .OrderBy(x => x % 0x1000000 / 0x10000 + x % 0x10000 / 0x100 + x % 0x100)
                    .ToArray();

                var instace = new Word
                {
                    Text = word.word,
                    Color = "#" + Convert.ToString(pixels.Last(), 16)
                };
                _words.Add(instace);
            }

            await Navigation.PushAsync(new PostItsPage(_words));
        }

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

HttpClient の部分は前回のエントリーと一緒なので省略します.
以下、AnalyzeColor を抜粋して説明します.

var pixels = editableImage.Crop(word.location.left,
        word.location.top,
        word.location.width,
        word.location.height)
.ToArgbPixels()
.OrderBy(x => x % 0x1000000 / 0x10000 + x % 0x10000 / 0x100 + x % 0x100)
.ToArray();

Image Edit Plugin for Xamarin を用い、編集可能な Image を取得します.

Recognize Text の JSON からは文字領域の座標を取得できるので、それの left, top, width, height を指定し Crop します.
このとき Crop の返り値は Crop 処理された editableImage 自身なので注意が必要です(Crop で新たな Image インスタンスが生成されているわけではありません).

Crop された Image を ARGB 情報の int配列に変え、ソートしています.
(黒とポストイットの色を判別できればいいだけなので、ソートのアルゴリズムはテキトーです.)

ここまでが写真撮影ページの実装です.


次に認識した文字を一覧で表示するページの実装です.

PostItsPage.xaml です.

<?xml version="1.0" encoding="UTF-8"?>

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="FlipChartRecognitionApp.PostItsPage">

    <ListView x:Name="listView"
              Margin="0,10,0,10"
              VerticalOptions="FillAndExpand"
              HorizontalOptions="FillAndExpand">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <StackLayout Orientation="Horizontal"
                                 BackgroundColor="{Binding Color}">
                        <Label Text="{Binding Text}"
                               TextColor="White"
                               FontSize="32"
                               Margin="10,0,0,0"/>
                    </StackLayout>
                    </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

</ContentPage>

カスタムの ViewCell に StackLayout を配置し、BackgroundColor をバインドしています.

StackLayout の中には Label を配置し、Text をバインドしています.


PostItsPage.xaml.cs です.

using System;
using System.Collections.Generic;

using Xamarin.Forms;

namespace FlipChartRecognitionApp
{
	public partial class PostItsPage : ContentPage
	{
		public List<Word> Words { get; set; }

		public PostItsPage(List<Word> words)
		{
		    InitializeComponent();

		    Words = words;

		    listView.ItemsSource = Words;
		}
	}
}

コードビハインドは説明不要かと思います.


最後に画像の文字領域の色をポストイットの色にマッピングする Word モデルの実装です.

using System;

namespace FlipChartRecognitionApp
{
    public class Word
    {
        private const string Yellow = "#fded7d";
        private const string Red = "#fbb6cd";
        private const string Blue = "#96cdea";
        private const string Green = "#b9fcba";

        public string Text { get; set; }

        private string _color;

        public string Color
        {
            get => _color;
            set => _color = GetSimilarColor(value);
        }

        private static string GetSimilarColor(string targetHexColor)
        {
            var targetColor = Xamarin.Forms.Color.FromHex(targetHexColor);

            if (targetColor.G > targetColor.R && targetColor.G > targetColor.B)
                return Green;

            var distance = 1.8;
            var returnColor = "";

            foreach (var hexColor in new[] {Yellow, Red, Blue, Green})
            {
                var color = Xamarin.Forms.Color.FromHex(hexColor);

                var d = Math.Sqrt(Math.Pow(targetColor.R - color.R, 2)
                                  + Math.Pow(targetColor.G - color.G, 2)
                                  + Math.Pow(targetColor.B - color.B, 2));

                if (d >= distance) continue;

                distance = d;
                returnColor = hexColor;
            }

            return returnColor;
        }
    }
}

基本的には文字領域の色と、ポストイットの色のRGB3次元ユークリッド距離が最も小さい色を選びます.

ただし、緑は黄色との判別が上手くいかなかったので、RGBの中でGが最も大きい場合は問答無用で緑を選択します.


以上が Xamarin.Forms の実装でした.



実装結果

f:id:Santea:20170504211920j:plain

9つのポストイットに対し、6つしか認識されませんでした.
何回か試しましたが、毎回の認識具合も多少振れがありました.

Recognize Text はβ版ですが、GA時にはもう少し精度が上がっていてほしいですね.



まとめ

今回は Recognize Text を使ってフリップチャートのポストイットを認識するアプリを作ってみました.

Recognize Text は文字を認識するだけでなく、認識した文字の座標値も含めて返してくれるので色々応用の幅が広いと思います.
今回は色を組み合わせてみましたが、他にも色々使い道がありそうです.


以上です.


PS.
Xamarin の高度な抽象化はハマると本当に簡単に書けてよいですね!
今回ロジック部分は200行も書いてないです.本当に助かりました.

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 がリリースされると思いますので、ぜひそれを楽しみにしていただければと思います!


以上です.