かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

CoreML を Xamarin.Forms で使ってみよう

なんか iOS 11 から CoreML ってのが使えて簡単にいうと機械学習の学習結果を iOS ローカルで動かせるぜ!っていう感じのものらしいですね。強い。

ということで、Apple Developer と Xamarin のドキュメントを見ながら試してみたいと思います。

Core ML | Apple Developer Documentation

developer.xamarin.com

ちなみに、CoreML で使える学習結果のファイルは、Cognitive Services の Custo Vision API で作れるということなので、今回はこれも使ってみたいと思います。

azure.microsoft.com

Custom Vision API でいい感じの画像データを用意するのがめんどくさかったので、Drew さんの作ってくれた、このハンズオンにある画像をそのまま使いたいと思います。

github.com

customvision.ai

ではさくっと Custom Visoin を使えるようにしましょう。本題じゃないので注意点だけを。

Custom Vision のサイトでプロジェクトを作るときに General (compact) を使うことです。これをしないと CoreML で使えるファイルをエクスポートできません。

f:id:okazuki:20170919203915p:plain

あとは、Drew さんのリポジトリにある画像を適当に投げ込んで Fries / Not Fries のカテゴリを作って学習させます。

学習させたら下の青で囲ったエクスポートボタンを押します。

f:id:okazuki:20170919210929p:plain

こんな画面になるので、あとは支持に従うだけです。mlmodel という拡張子のファイルが取得できます。

f:id:okazuki:20170919211037p:plain

次の作業環境は Mac です。以下のコマンドをターミナルでうちます。

xcrun coremlcompiler compile さっきダウンロードしたファイル.mlmodel 出力先フォルダ

出来上がったファイルをどうにかして Windows にもっていきましょう。

Xamarin.iOS で頑張る

とりあえず、Xamarin.Forms でプロジェクトを作って iOS プロジェクトだけで動くように作りたいと思います。先ほどのコマンドでできた一連のファイルを iOS プロジェクトの Resources フォルダに移動させます。

f:id:okazuki:20170919223451p:plain

ClreML を使って認識をしてくれるであろう処理のためのインターフェースを PCL プロジェクトに作ります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CoreMLLabApp
{
    public interface IFriesOrNotFriesService
    {
        Task<string> DetectAsync(byte[] image);
    }
}

そして、MainPage.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"
             xmlns:local="clr-namespace:CoreMLLabApp"
             x:Class="CoreMLLabApp.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS">0,20,0,0</On>
        </OnPlatform>
    </ContentPage.Padding>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        
        <Button Text="Take picture"
            Clicked="Button_Clicked" />

        <Image x:Name="image"
               HorizontalOptions="Fill"
               VerticalOptions="Fill"
               Grid.Row="1" />
    </Grid>
</ContentPage>

そして、カメラから画像をとりたいので Xam.Plugin.Media を導入して表示される readme.txt の内容に従って info.plist に設定を追加したら MainPage.xaml.cs を以下のようにします。

using Plugin.Media;
using Plugin.Media.Abstractions;
using System;
using System.IO;
using Xamarin.Forms;

namespace CoreMLLabApp
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

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

            var file = await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions());
            if (file == null) { return; }

            this.image.Source = ImageSource.FromStream(() => file.GetStream());

            using (var fs = file.GetStream())
            using (var ms = new MemoryStream())
            {
                await fs.CopyToAsync(ms);
                var d = DependencyService.Get<IFriesOrNotFriesService>();
                var result = await d.DetectAsync(ms.ToArray());
                await this.DisplayAlert("Result", result, "OK");
            }
        }
    }
}

ここまでは、ただの Xamarin.Forms ですね。

CoreML を使ってみよう

では、iOS プロジェクトに FriesOrNotFriesService.cs を追加して処理を書いていきます。サンプルプロジェクトとかを参考に以下のように書いてみました。

using System;
using System.Linq;
using System.Threading.Tasks;
using Foundation;
using Vision;
using CoreML;
using CoreImage;
using CoreFoundation;

[assembly: Xamarin.Forms.Dependency(typeof(CoreMLLabApp.iOS.FriesOrNotFriesService))]
namespace CoreMLLabApp.iOS
{
    public class FriesOrNotFriesService : IFriesOrNotFriesService
    {
        private static VNCoreMLModel VModel { get; }

        static FriesOrNotFriesService()
        {
            // Load the ML model
            var assetPath = NSBundle.MainBundle.GetUrlForResource("e3e4e645c0944c6ca84f9a000e501b22", "mlmodelc");
            var friedOrNotFriedModel = MLModel.Create(assetPath, out _);
            VModel = VNCoreMLModel.FromMLModel(friedOrNotFriedModel, out _);
        }

        public Task<string> DetectAsync(byte[] image)
        {
            var taskSource = new TaskCompletionSource<string>();
            void handleClassification(VNRequest request, NSError error)
            {
                var observations = request.GetResults<VNClassificationObservation>();
                if (observations == null)
                {
                    taskSource.SetException(new Exception("Unexpected result type from VNCoreMLRequest"));
                    return;
                }

                if (observations.Length == 0)
                {
                    taskSource.SetResult(null);
                    return;
                }

                var best = observations.First();
                taskSource.SetResult(best.Identifier);
            }

            using (var data = NSData.FromArray(image))
            {
                var ciImage = new CIImage(data);
                var handler = new VNImageRequestHandler(ciImage, new VNImageOptions());
                DispatchQueue.DefaultGlobalQueue.DispatchAsync(() =>
                {
                    handler.Perform(new VNRequest[] { new VNCoreMLRequest(VModel, handleClassification) }, out _);
                });
            }

            return taskSource.Task;
        }

    }
}

byte[] から CoreML の入力として渡すための CIImage に変換して認識処理を呼び出しています。

動かして動作確認

ふむ。とりあえず動くっぽい。

f:id:okazuki:20170919224454p:plain

f:id:okazuki:20170919224507p:plain

あとは、DependencyService で Android や UWP の時には、CustomVision の API をたたくように仕込めば iOS だけ CoreML で他は REST API みたいなことが出来ますね。

ソースコードは以下に置いています。

github.com