Android は TensorFlow、iOS は CoreML、Windows 10 は onnx という感じで各 OS でディープラーニングの学習モデルをサポートするような API が追加されてきてますね!
Xamarin.Forms を使えば Android, iOS, UWP アプリの開発が同時に出来る(UI を各 OS ごとに凝るなら Xamarin Native を選んだ方が最終的に楽なケースもあるけど)ので、うまいことやればインターネット接続不要で画像判別とかを AI ちっくにやるようなアプリが全部 C# で書けそうなのでやってみましょう。
やりながら書くので、最終的にダメでしたになる可能性もありますがとりあえずね。
モデルの作成
Tensorflow とか CNTK とか etc... を使ってディープラーニングするのが一番いんでしょうが、そこらへんから勉強してたらブログ記事書くのに何か月もかかるので今回は Microsoft Cognitive Services の Custom Vision を使ってみたいと思います。
これは、ポータルサイトで画像登録してタグづけしてトレーニングさせると WebAPI や CoreML や onnx や TensorFlow などの形で公開できる素敵なやつです。
そこに、今回は Microsoft の Drew さんが結構前に作ってくれたハンズオンにある揚げ物かどうかを判定する学習用の画像セットを使って学習されたものをエクスポートして使ってみたいと思います。
余談ですが Drew さんが先日行われた TechSummit でセッションしたのですが、最初に powerpoint.exe をデリートしてデモとコードメインで色んな人とやりとりしながらやってたセッションは面白かったです。
そうこうしてるうちにリポジトリのクローンが終わったので https://customvision.aiにアクセスしてプロジェクトを作ります。
プロジェクトを作るときに Project Types は Classification、Domains は General (compact) を選びます。
Add images を選択してダウンロードしたファイルの中の assets/Training/Fries 以下の画像を Fries とタグ付けしてアップロードします。 NotFries フォルダの画像を NotFries でタグ付けしてアップロードします。
アップロードが完了したら Train をして Performance から Export を選んでモデルをダウンロードします。
ONNX だけバージョンが選択できるのですが、今回は最新の Windows 10 SDK を使おうと思うので ONNX 1.2 を選択しました。
Xamarin.Forms アプリの作成
Xamarin.Forms でアプリを作りましょう。Blank で Android, iOS, UWP で .NET Standard を選択して OK を選びます。
画像撮影機能の追加
前は Xamarin Plugins を使うのが一般的でしたが今は Xamarin.Essentials を使う方がいいのかな?
と思ってみたらカメラ無い??えっ???まじで???ということで Media Plugin for Xamarin and Windows を使います。
全プロジェクトに上記パッケージをインストールします。インストールすると readme.txt が出てくるので、それに従って各プロジェクトの設定を変更します。
設定が終わったら MainPage.xaml を以下のようにします。画像表示用コントロールと写真をとるためのボタンを置いてます。
<?xml version="1.0" encoding="utf-8"?><ContentPagex:Class="AIApp.MainPage"xmlns="http://xamarin.com/schemas/2014/forms"xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"xmlns:local="clr-namespace:AIApp"Title="Safe Area"ios:Page.UseSafeArea="True"><StackLayout><Imagex:Name="picture"Aspect="AspectFill"VerticalOptions="FillAndExpand" /><Label x:Name="output"HorizontalOptions="CenterAndExpand" /><StackLayout Orientation="Horizontal"><ButtonClicked="PickPhotoButton_Clicked"HorizontalOptions="FillAndExpand"Text="Pick a picture" /><ButtonClicked="TakePhotoButton_Clicked"HorizontalOptions="FillAndExpand"Text="Take a picture" /></StackLayout></StackLayout></ContentPage>
として、MainPage.xaml.cs を以下のようにして AI を使う手前まで実装します。
using Plugin.Media; using Plugin.Media.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; namespace AIApp { publicpartialclass MainPage : ContentPage { public MainPage() { InitializeComponent(); } private async void TakePhotoButton_Clicked(object sender, EventArgs e) { await ProcessPhotoAsync(true); } private async void PickPhotoButton_Clicked(object sender, EventArgs e) { await ProcessPhotoAsync(false); } private async Task ProcessPhotoAsync(bool useCamera) { await CrossMedia.Current.Initialize(); if (useCamera ? !CrossMedia.Current.IsTakePhotoSupported : !CrossMedia.Current.IsPickPhotoSupported) { await DisplayAlert("Info", "Your phone doesn't support photo feature.", "OK"); return; } var photo = useCamera ? await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions()) : await CrossMedia.Current.PickPhotoAsync(); if (photo == null) { picture.Source = null; return; } picture.Source = ImageSource.FromFile(photo.Path); // TODO: using AI. } } }
これで撮影して画像を表示するところまで出来ました。Android 9 のエミュレーターで動かすと以下のように動きます。
では、ここに AI の機能を追加していきましょう。
インターフェースを考える
ここから先は各プラットフォームごとの実装が必要になります。.NET Standard のプロジェクトにはインターフェースを作成します。画像のストリームを受け取って Fries か NotFries の enum を返すようにしました。
using System.IO; using System.Threading.Tasks; namespace AIApp { publicinterface IPhotoDetector { Task<FriesOrNotFriesTag> DetectAsync(Stream photo); } publicenum FriesOrNotFriesTag { None, Fries, NotFries, } }
そして MainPage.xaml.cs に、このインターフェースを使った処理を書きます。
using Plugin.Media; using Plugin.Media.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; namespace AIApp { publicpartialclass MainPage : ContentPage { public MainPage() { InitializeComponent(); } private async void TakePhotoButton_Clicked(object sender, EventArgs e) { await ProcessPhotoAsync(true); } private async void PickPhotoButton_Clicked(object sender, EventArgs e) { await ProcessPhotoAsync(false); } private async Task ProcessPhotoAsync(bool useCamera) { await CrossMedia.Current.Initialize(); if (useCamera ? !CrossMedia.Current.IsTakePhotoSupported : !CrossMedia.Current.IsPickPhotoSupported) { await DisplayAlert("Info", "Your phone doesn't support photo feature.", "OK"); return; } var photo = useCamera ? await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions()) : await CrossMedia.Current.PickPhotoAsync(); if (photo == null) { picture.Source = null; return; } picture.Source = ImageSource.FromFile(photo.Path); var service = DependencyService.Get<IPhotoDetector>(); if (service == null) { await DisplayAlert("Info", "Not implemented the feature on your device.", "OK"); return; } using (var s = photo.GetStream()) { var result = await service.DetectAsync(s); output.Text = $"It looks like a {result}"; } } } }
この状態で実行すると、まだ実装がないので以下のようになります。
プラットフォームごとの実装
では、各プラットフォームごとに実装していきます。
UWP
まずは UWP からいってみましょう。UWP では October 2018 Update で追加されたものを使うので UWP プロジェクトのプロパティの Application の Target version と Min version を 1809 に設定します。
Windows 10 で使う API は Windows Machine Learning です。
Custom Vision からダウンロードした onnx ファイルを FriesOrNotFries.onnx` にリネームして UWP プロジェクトの Assets フォルダに追加します。 ビルドアクションはコンテンツにしておきましょう。
こんな感じのコードが生成されているはずです。
using System; using System.Collections.Generic; using System.Threading.Tasks; using Windows.Media; using Windows.Storage; using Windows.Storage.Streams; using Windows.AI.MachineLearning; namespace AIApp.UWP { publicsealedclass FriesOrNotFriesInput { public ImageFeatureValue data; // BitmapPixelFormat: Bgra8, BitmapAlphaMode: Premultiplied, width: 227, height: 227 } publicsealedclass FriesOrNotFriesOutput { public TensorString classLabel; // shape(-1,1)public IList<Dictionary<string,float>> loss; } publicsealedclass FriesOrNotFriesModel { private LearningModel model; private LearningModelSession session; private LearningModelBinding binding; publicstatic async Task<FriesOrNotFriesModel> CreateFromStreamAsync(IRandomAccessStreamReference stream) { FriesOrNotFriesModel learningModel = new FriesOrNotFriesModel(); learningModel.model = await LearningModel.LoadFromStreamAsync(stream); learningModel.session = new LearningModelSession(learningModel.model); learningModel.binding = new LearningModelBinding(learningModel.session); return learningModel; } public async Task<FriesOrNotFriesOutput> EvaluateAsync(FriesOrNotFriesInput input) { binding.Bind("data", input.data); var result = await session.EvaluateAsync(binding, "0"); var output = new FriesOrNotFriesOutput(); output.classLabel = result.Outputs["classLabel"] as TensorString; output.loss = result.Outputs["loss"] as IList<Dictionary<string,float>>; return output; } } }
では、これを使って先ほど作ったインターフェースの実装を作っていきます。
PhotoDetector.cs を UWP プロジェクトに作成して以下のようなコードを書きます。先ほどの自動生成されたファイルを使って画像を判別しています。
using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Windows.AI.MachineLearning; using Windows.Graphics.Imaging; using Windows.Media; using Windows.Storage; using Xamarin.Forms; [assembly: Dependency(typeof(AIApp.UWP.PhotoDetector))] namespace AIApp.UWP { publicclass PhotoDetector : IPhotoDetector { private FriesOrNotFriesModel _model; public async Task<FriesOrNotFriesTag> DetectAsync(Stream photo) { await InitializeModelAsync(); var bitmapDecoder = await BitmapDecoder.CreateAsync(photo.AsRandomAccessStream()); var output = await _model.EvaluateAsync(new FriesOrNotFriesInput { data = ImageFeatureValue.CreateFromVideoFrame(VideoFrame.CreateWithSoftwareBitmap(await bitmapDecoder.GetSoftwareBitmapAsync())), }); var label = output.classLabel.GetAsVectorView().FirstOrDefault(); return Enum.Parse<FriesOrNotFriesTag>(label); } private async Task InitializeModelAsync() { if (_model != null) { return; } var onnx = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/FriesOrNotFries.onnx")); _model = await FriesOrNotFriesModel.CreateFromStreamAsync(onnx); } } }
この状態で UWP のプロジェクトをスタートアッププロジェクトにすると以下のように画像認識機能が動きます。
Android
Android で使う API は…、ネイティブの Android だと以下のライブラリになるみたいです。
Maven Repository: org.tensorflow » tensorflow-android
実際に blog.xamarin.com の中に、これをネイティブバインディングしてるライブラリの Xam.Android.Tensorflow を使っている記事があります。
(Using TensorFlow and Azure to Add Image Classification to Your Android Apps)https://blog.xamarin.com/android-apps-tensorflow/
NuGet はこちら。
実際に Android プロジェクトに Xam.Android.Tensorflow を追加します。そして、上記ブログと以下の Java のサンプルを参考にしてコードを書きます。
まず、model.pb と labels.txt を Android プロジェクトの Assets フォルダーに追加して、ビルドアクションを AndroidAsset に設定します。PhotoDetector.cs を Android プロジェクトに追加して以下のように実装します。
using Android.Graphics; using Org.Tensorflow.Contrib.Android; using Plugin.CurrentActivity; using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Xamarin.Forms; [assembly: Dependency(typeof(AIApp.Droid.PhotoDetector))] namespace AIApp.Droid { publicclass PhotoDetector : IPhotoDetector { privatestaticreadonlystring ModelFile = "model.pb"; privatestaticreadonlystring LabelFile = "labels.txt"; privatestaticreadonlystring InputName = "Placeholder"; privatestaticreadonlystring OutputName = "loss"; privatestaticreadonlyint InputSize = 227; privatereadonly TensorFlowInferenceInterface _inferenceInterface; privatereadonlystring[] _labels; public PhotoDetector() { _inferenceInterface = new TensorFlowInferenceInterface(CrossCurrentActivity.Current.Activity.Assets, ModelFile); using (var sr = new StreamReader(CrossCurrentActivity.Current.Activity.Assets.Open(LabelFile))) { _labels = sr.ReadToEnd().Split('\n').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToArray(); } } public async Task<FriesOrNotFriesTag> DetectAsync(Stream photo) { var bitmap = await BitmapFactory.DecodeStreamAsync(photo); var floatValues = GetBitmapPixels(bitmap); var outputs = newfloat[_labels.Length]; _inferenceInterface.Feed(InputName, floatValues, 1, InputSize, InputSize, 3); _inferenceInterface.Run(new[] { OutputName }); _inferenceInterface.Fetch(OutputName, outputs); var index = Array.IndexOf(outputs, outputs.Max()); return (FriesOrNotFriesTag)Enum.Parse(typeof(FriesOrNotFriesTag), _labels[index]); } private async Task<byte[]> LoadByteArrayFromAssetsAsync(string name) { using (var s = CrossCurrentActivity.Current.Activity.Assets.Open(name)) using (var ms = new MemoryStream()) { await s.CopyToAsync(ms); ms.Seek(0, SeekOrigin.Begin); return ms.ToArray(); } } privatestaticfloat[] GetBitmapPixels(Bitmap bitmap) { var floatValues = newfloat[InputSize * InputSize * 3]; using (var scaledBitmap = Bitmap.CreateScaledBitmap(bitmap, InputSize, InputSize, false)) { using (var resizedBitmap = scaledBitmap.Copy(Bitmap.Config.Argb8888, false)) { var intValues = newint[InputSize * InputSize]; resizedBitmap.GetPixels(intValues, 0, resizedBitmap.Width, 0, 0, resizedBitmap.Width, resizedBitmap.Height); for (int i = 0; i < intValues.Length; ++i) { var val = intValues[i]; floatValues[i * 3 + 0] = ((val & 0xFF) - 104); floatValues[i * 3 + 1] = (((val >> 8) & 0xFF) - 117); floatValues[i * 3 + 2] = (((val >> 16) & 0xFF) - 123); } resizedBitmap.Recycle(); } scaledBitmap.Recycle(); } return floatValues; } } }
Android のエミュレーターで実行すると…
iOS
続いて iOS の CoreML をしていきます。Xamarin の公式ドキュメントはこちら。
前に試したときは mac に mlmodel を持っていってコマンドを打ってとかしてましたが、そこらへんの手順は簡略化されてるみたいですね。iOS プロジェクトの Resources フォルダに mlmodel を追加します。そしてビルドアクションを CoreMLModel にします。
そして、PhotoDetector.cs を iOS プロジェクトに追加して以下のように実装します。
using CoreFoundation; using CoreImage; using CoreML; using Foundation; using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Vision; using Xamarin.Forms; [assembly: Dependency(typeof(AIApp.iOS.PhotoDetector))] namespace AIApp.iOS { publicclass PhotoDetector : IPhotoDetector { privatereadonly MLModel _mlModel; privatereadonly VNCoreMLModel _model; public PhotoDetector() { var assetPath = NSBundle.MainBundle.GetUrlForResource("FriesOrNotFries", "mlmodelc"); _mlModel = MLModel.Create(assetPath, out var _); _model = VNCoreMLModel.FromMLModel(_mlModel, out var __); } public Task<FriesOrNotFriesTag> DetectAsync(Stream photo) { var taskCompletionSource = new TaskCompletionSource<FriesOrNotFriesTag>(); void handleClassification(VNRequest request, NSError error) { var observations = request.GetResults<VNClassificationObservation>(); if (observations == null) { taskCompletionSource.SetException(new Exception("Unexpected result type from VNCoreMLRequest")); return; } if (!observations.Any()) { taskCompletionSource.SetResult(FriesOrNotFriesTag.None); return; } var best = observations.First(); taskCompletionSource.SetResult((FriesOrNotFriesTag)Enum.Parse(typeof(FriesOrNotFriesTag), best.Identifier)); } using (var data = NSData.FromStream(photo)) { var ciImage = new CIImage(data); var handler = new VNImageRequestHandler(ciImage, new VNImageOptions()); DispatchQueue.DefaultGlobalQueue.DispatchAsync(() => { handler.Perform(new VNRequest[] { new VNCoreMLRequest(_model, handleClassification) }, out var _); }); } return taskCompletionSource.Task; } } }
実行すると…
動いてますね!
ソースコード
GitHub に置いてます。