Watsonの画像認識を用いて文字認識アプリを作成してみた
前回、 Xamarin で Watson Visual RecognitionのRecognize Text を使うエントリーを書きました.
今回はより具体的な使用用途として、フリップチャートに貼ったポストイットを認識するスマートフォンアプリを作ってみます.
ポストイットに書いてある文字と、ポストイットの色をセットで認識します.アプリの実装は Xamarin.Forms で行います.
Xamarin.Forms の実装
まずは写真を撮影するページの実装です.
写真の撮影には Media Plugin for Xamarin and Windows を用いました.
また画像の文字領域を切り取るのに @muax__x さんの Image Edit Plugin for Xamarin を使わせていただきました.
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.xaml.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 _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();
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(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 を取得します.
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 Words { get; set; }
public PostItsPage(List 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 の実装でした.
実装結果
9つのポストイットに対し、6つしか認識されませんでした.
何回か試しましたが、毎回の認識具合も多少振れがありました.
Recognize Text はβ版ですが、GA時にはもう少し精度が上がっていてほしいですね.
まとめ
今回は Recognize Text を使ってフリップチャートのポストイットを認識するアプリを作ってみました.
Recognize Text は文字を認識するだけでなく、認識した文字の座標値も含めて返してくれるので色々応用の幅が広いと思います.
今回は色を組み合わせてみましたが、他にも色々使い道がありそうです.
以上です.
PS.
Xamarin の高度な抽象化はハマると本当に簡単に書けてよいですね!今回ロジック部分は200行も書いてないです.本当に助かりました.
ディスカッション
コメント一覧
まだ、コメントがありません