LUISを使って時間を止める

# 経緯

「スタンド能力が欲しい。」

JOJOの奇妙な冒険を知っている方なら、必ず一度は思ったことがあるはず。
自分もそうなのだから他の方もそうに違いない。

そんな思想背景を踏まえて、まずはこちらを見てほしい。

1.とある男が「スタープラチナ」と叫ぶと

![01_starplutina.png](https://qiita-image-store.s3.amazonaws.com/0/42445/de2623fa-ff4c-a26c-8f32-ee7b697776a0.png)

2.別の男が現れて「ザワールド」と叫びつつ時間を止める

![02_standing.png](https://qiita-image-store.s3.amazonaws.com/0/42445/ba10e003-0185-493b-adbd-d682973c664f.png)

3.最初の男が「そして時は動き出す」と言うと時間が動き始める

![03_原状復帰.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b979f22d-1b72-6c5e-938a-38cbe6d0cb55.png)

これやりたい。今の俺ならできるはず。
そう、LUISでね。

# 結果

https://youtu.be/zSZTq8VCUfk

注意) 音声は撮れてないけどしゃべってます

~~コードは後でGitに上げる予定。~~
[ココ](https://github.com/miyatama/tokiwotomeru)にコードアップしました。

# ざっくりとした仕様

 + 「スタープラチナ」を認識
 + 別の男を画面に出す
 + 「ザワールド」を認識
 + 時間を止める
 + 「そして時は動き出す」を認識
 + 時間を動かす

こんな感じだ。細かいところは気にしない。
時間止めたりするんだからそんなこと気にしてられない。

# 詳細

## AzureからLUISアプリを作る

### LUISを使う準備をする

ここまででLUISに関する説明してなかった事に気づく。

LUISとは、Microsoftが作った
`ある文が何を意味しているかいい感じに判断する機能`
である。
まぁ、今から使ってゆくので何となくそんなもんだと思ってれば大丈夫なはず。

では早速行動開始。
Azureアカウントを作ったらポータルへ飛ぶ。

ポータルへの入り方は覚えておいたほうが良い。
毎回毎回存在しないログインボタンを探す羽目になる(なった)ので、ここでしっかり書いておく
[トップ](https://azure.microsoft.com/ja-jp/)からポータルをクリック

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/defab8ee-b1c8-4d5e-97f8-0ada94ba4b16.png)

早速LUISのリソースを追加する。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/252f2049-608c-b6e5-2e8c-9fd08c9f360f.png)

これがLUISだ！

![buff2.png](https://qiita-image-store.s3.amazonaws.com/0/42445/88af6f0d-9c09-3fb6-a815-e38454d9d042.png)

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/e4e3d376-f1a0-6fa3-3cc3-03f466ee816b.png)

名前とかをいい感じに入力。
初めての方は損を考えずにとりあえずやってみる事をお勧めする。
なぜなら自分も他のCloud環境で損した事があるから。
ただ一つ注意してほしいのが、場所。これから先色々いじる予定のある方は場所を合わせとくといいらしい。
俺はかっこいいから米国西部か米国東部にしてる。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/a109d290-bf9a-88b3-c2c8-b7d8cc4091cf.png)

リソースが追加出来たら早速LUISのコンソールを開きにかかる

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/296a5344-b318-28b2-e031-9d897d2e6540.png)

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b45af6c8-fadd-2971-ec64-c9af4eeddfd1.png)

さらにログインが必要。簡単には使わせてもらえない。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/f69e5c08-2e44-0f91-02e3-bacac81417a5.png)


### LUISアプリを作ってスタープラチナとかを学習させる

#### アプリの新規作成

Create New Appをクリックしてアプリを作る。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b9617e3a-51c4-52ae-986d-fbfc4ebf4c1b.png)

japaneseを選択しないと日本語を認識しないのでそこだけ注意。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/d8207f05-efa5-8eb4-ea65-c841148916ad.png)

いざ作成開始

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/8440e48f-a48f-a425-840a-56d2e50d258f.png)

#### Intentを作成する

Intentとは
'文の意味を定義するもの'
だ。
要は今回で言う所の「スタープラチナ！」とか「時は動き出す」とかを表すもの。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/3897c523-a84f-7051-3c64-8f2764c888b4.png)

「スタープラチナ」を意味するので「sutapura」と名付けた。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/bf3f33dc-7eed-8c60-2705-509e6828b8e8.png)

こんな感じでスタープラチナを学ばせる。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/c69411cb-7482-c789-0c5d-b9843663f44f.png)

「スタープラチナ」として扱ってほしい文字列が複数になった場合はEntityを作成する。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/d7c8d608-2c26-7922-b054-c88ce43aeb8b.png)

#### Entityを作成する

Entityとは
`揺らぎのある単語をまとめるためのもの`
だ。
これ以上の詳しい説明が欲しいなら、本を読めばいいと思う。

今回のように「すたーぷらちな」や「スター・プラチナ」も「スタープラチナ」とみなしたい場合に利用する。

Intentと同じく新規作成する。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/5b8dcd1e-1afc-a5a8-1907-fba591864a9c.png)

#### IntentとEntityを結ぶ

再びsutapura Intentを開き、どこからどこまでがstar_plutinum Entityなのかを指定する。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/4a216866-2de9-a2b3-9002-ed38ee67a3c1.png)

これやり方が非常にわかりづらいのだが、
例文の「スター」をクリックした状態でマウスを右にずらし、「プラチナ」の右側に"]"が出た状態で再度クリックする。
分かりづらいだけあって文にしてもわかりづらい。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/af0c663c-628f-7184-9cd2-bba6b3af840f.png)

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/592f3828-3fe1-e458-c179-9d48f105bab9.png)

あらかた例文を打ちまくったら、右上のTrainボタンを押す。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/6341ea01-f620-d1e7-b2ca-73e93b0f75b7.png)

#### 学習 & テスト

学習が終わったら早速Testする。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/b25a5ab2-3f31-be9a-6d41-b12766d4522c.png)

似たような感じで「ザワールド」と「時は動き出す」も作る。

作り終わったらpublishしておく。





## クライアントからLUISを呼ぶ

### まずはLUISを呼ぶだけ呼ぶ

LUISは下記SDKを用意しているので、好きなものを使えばよい(2018.04.15現在)。今回はC#にした。

 + Android
 + Windows
 + Node.js
 + Python

まずはVisual Studio 2017を起動して新しくプロジェクトを作る。
男を出したりするので、フォームアプリとしてプロジェクトを作成する。
JSONを使うのでパッケージを入れる。
```
Install-Package Newtonsoft.Json
```

まずはシンプルに"スタープラチナ！"を呼んで何のインテントとみなされるか判断してみる。

``` CSharp:Form1
LuisRequest request = new LuisRequest();
Task<LuisResult> task = Task.Run(() => request.MakeRequest("スタープラチナ！"));
Task.WaitAll(task);
LuisResult result = task.Result;
Console.WriteLine(result.topScoringIntent.intent);
```

LUISを呼び出すクラスを作る。
注意としてはURLのVerboseをfalseにする事。

``` CSharp:LuisRequest
class LuisRequest
{

    public async Task<LuisResult> MakeRequest(string query)
    {
        var client = new HttpClient();
        var uri = "<Luis Endpoint>" + query;
        var response = await client.GetAsync(uri);

        var strResponseContent = await response.Content.ReadAsStringAsync();

        LuisResult result = JsonConvert.DeserializeObject<LuisResult>(strResponseContent.ToString());
        return result;
    }
}
```

LUISのEndpointは下記の場所

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/3578c275-f844-0b86-858e-905ad258ea81.png)

LUISの戻り値を定義する

``` CSharp:LuisResult
class LuisResult
{

  public string query{get; set;}
  public TopScoringIntent topScoringIntent{get; set;}
  public Entities[] entities {get; set;}
}
```

``` CSharp:TopScroringIntent
class TopScoringIntent
{
    public string intent {get; set; }
    public double score {get; set;}
}
```

``` CSharp:Entities
class Entities
{
    public string entity {get; set;}
    public string type {get; set;}
    public int startIndex {get; set;}
    public int endIndex {get; set;}
    public double score {get; set;}
}
```

よし、sutapuraって出てる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/1754777f-4597-47ae-ec3b-4a726ed5c1f4.png)

### 音声経由で呼ぶ

やはりテキストでLUIS呼んでも物足りない。ここはVUIの出番だ。
「スタープラチナ」って言いたい。

まずはLUISよろしくazure上でSpeech APIリソースを追加する。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/e70255a5-f6d8-214d-b1cc-74c580196a31.png)

後は[ココ](https://github.com/Azure-Samples/Cognitive-Speech-STT-Windows)を見つつ組み込んで行く。

まずはnugetで必要なものを入れる
```
Microsoft.ProjectOxford.SpeechRecognition-x64
Microsoft.ProjectOxford.SpeechRecognition-x86
```

必要な情報をメモっておく
必要なのはサブスクリプションキー(subscriptionkey)。
コレ超不親切で、Azureのポータル上「ココがサブスクリプションキーですよ！」なんて書いてなかったりする。

![buff.png](https://qiita-image-store.s3.amazonaws.com/0/42445/3f92460a-59a7-7050-a159-cfb48084a8ea.png)

AuthentificationURIなるものも必要になるが、これはsampleを追ってゆくと書いてあったりする。

では早速フォームにボタンとラベルを追加してプログラムを修正。
自分で考えるところは特にない。ほぼサンプルのまま。

``` CSharp:Form1
public partial class Form1 : Form
{

    private delegate void InvokeDelegate();
    private MicrophoneRecognitionClient micClient;

    public Form1()
    {
        InitializeComponent();


        LuisRequest request = new LuisRequest();
        Task<LuisResult> task = Task.Run(() => request.MakeRequest("スタープラチナ！"));
        Task.WaitAll(task);
        LuisResult result = task.Result;
        Console.WriteLine(result.topScoringIntent.intent);
        // request.MakeRequest("ザ・ワールド！");
        // request.MakeRequest("そして時は動き出す");

        if (this.micClient == null) { 
            // this.CreateMicrophoneRecoClientWithIntent();
            this.CreateMicrophoneRecoClient();
        }
    }

    private void btnSutapura_Click(object sender, EventArgs e)
    {
        if (this.micClient != null)
        {
            this.micClient.EndMicAndRecognition();
            this.micClient = null;
        }
        if (this.micClient == null) { 
            // this.CreateMicrophoneRecoClientWithIntent();
            this.CreateMicrophoneRecoClient();
        }
        this.micClient.StartMicAndRecognition();
    }

    private SpeechRecognitionMode Mode
    {
        get
        {
            return SpeechRecognitionMode.LongDictation;
            // return SpeechRecognitionMode.ShortPhrase;
        }
    }

    private string DefaultLocale
    {
        get { return "ja_JP"; }
    }
    

    public string SubscriptionKey
    {
        get
        {
            return "サブスクリプションキー";
        }
    }


    private string AuthenticationUri
    {
        get
        {
            return "https://api.cognitive.microsoft.com/sts/v1.0/issueToken";
        }
    }

    private string LuisEndpointUrl
    {
        get {
            return "";
        }
    }

    private void CreateMicrophoneRecoClient()
    {
        this.micClient = SpeechRecognitionServiceFactory.CreateMicrophoneClient(
            this.Mode,
            this.DefaultLocale,
            this.SubscriptionKey);
        this.micClient.AuthenticationUri = this.AuthenticationUri;

        // Event handlers for speech recognition results
        this.micClient.OnMicrophoneStatus += this.OnMicrophoneStatus;
        this.micClient.OnPartialResponseReceived += this.OnPartialResponseReceivedHandler;
        if (this.Mode == SpeechRecognitionMode.ShortPhrase)
        {
            this.micClient.OnResponseReceived += this.OnMicShortPhraseResponseReceivedHandler;
        }
        else if (this.Mode == SpeechRecognitionMode.LongDictation)
        {
            this.micClient.OnResponseReceived += this.OnMicDictationResponseReceivedHandler;
        }

        this.micClient.OnConversationError += this.OnConversationErrorHandler;
    }


    private void OnMicDictationResponseReceivedHandler(object sender, SpeechResponseEventArgs e)
    {
        Console.WriteLine("status: " + e.PhraseResponse.RecognitionStatus);
        if (e.PhraseResponse.RecognitionStatus == RecognitionStatus.EndOfDictation ||
            e.PhraseResponse.RecognitionStatus == RecognitionStatus.RecognitionSuccess ||
            e.PhraseResponse.RecognitionStatus == RecognitionStatus.DictationEndSilenceTimeout)
        {
            BeginInvoke(new InvokeDelegate(() =>
            {
                this.Text = "stop Sutapura";
    	            if (e.PhraseResponse.Results.Length == 0)
    	            {
    	                Console.WriteLine("No phrase response is available.");
    	            }
    	            else
    	            {
    	                Console.WriteLine("********* Final n-BEST Results *********");
    	                for (int i = 0; i < e.PhraseResponse.Results.Length; i++)
    	                {
    	                    Console.WriteLine(
    	                        string.Format("[{0}] Confidence={1}, Text=\"{2}\"", 
    	                        i, 
    	                        e.PhraseResponse.Results[i].Confidence,
    	                        e.PhraseResponse.Results[i].DisplayText));
    	                }
    	                if (e.PhraseResponse.Results.Length > 0)
    	                {
    	                    lblStapura.Text = e.PhraseResponse.Results[0].DisplayText;
    	                }
    	
    	            }
                this.micClient.EndMicAndRecognition();
            }));
        }

    }

    private void CreateMicrophoneRecoClientWithIntent()
    {
        this.micClient =
            SpeechRecognitionServiceFactory.CreateMicrophoneClientWithIntentUsingEndpointUrl(
                this.DefaultLocale,
                this.SubscriptionKey,
                this.LuisEndpointUrl);
        this.micClient.AuthenticationUri = this.AuthenticationUri;
        this.micClient.OnIntent += this.OnIntentHandler;

        // Event handlers for speech recognition results
        this.micClient.OnMicrophoneStatus += this.OnMicrophoneStatus;
        this.micClient.OnPartialResponseReceived += this.OnPartialResponseReceivedHandler;
        this.micClient.OnResponseReceived += this.OnMicShortPhraseResponseReceivedHandler;
        this.micClient.OnConversationError += this.OnConversationErrorHandler;
    }

    private void OnMicShortPhraseResponseReceivedHandler(object sender, SpeechResponseEventArgs e)
    {
        BeginInvoke(new InvokeDelegate(() => {
            this.micClient.EndMicAndRecognition();
            Console.WriteLine(e);
        }));
    }

    private void OnMicrophoneStatus(object sender, MicrophoneEventArgs e)
    {
        if (e.Recording) {
            BeginInvoke(new InvokeDelegate(() => {
                this.Text = "Go Sutapura";
                Console.WriteLine("start recording");
            }));


        }
    }

    private void OnIntentHandler(object sender, SpeechIntentEventArgs e)
    {
        Console.WriteLine(e.Payload);
    }

    private void OnPartialResponseReceivedHandler(object sender, PartialSpeechResponseEventArgs e)
    {
        Console.WriteLine(e.PartialResult);
    }

    private void OnConversationErrorHandler(object sender, SpeechErrorEventArgs e)
    {
        Console.WriteLine( "Error code: " + e.SpeechErrorCode.ToString() + ", Error text: " + e.SpeechErrorText);
    }
}
```

いざ実行。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/de53e916-688c-8736-47fc-e34c1e769a93.png)

おぉ、いい感じ。

ちなみに、プログラム中に"WithIntent"なる文言があったと思う。
実はSpeech APIのSDK上、LUISとの連動もできる。
が、今回はやらない。
やりたい人はぜひともやり方をQiitaに上げてもらえると嬉しい。

## 男出したり時間とめたりする

さぁ、ここまでで「言葉を認識する」所ができた。
後は「認識した内容に応じてエフェクトを掛ける」部分だ。

### 画面にカメラ画像を出す

画面、カメラと言えばOpenCVだ。というわけで`OpenCvSharp3-AnyCPU`をNugetで取得。
下記コントロールを追加する

 + PictureBox
 + Timer

プログラムをこそこそ書いて、カメラが映るか確認。

``` CSharp:Form1
using Microsoft.CognitiveServices.SpeechRecognition;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace TokiwotomeruApp
{
    public partial class Form1 : Form
    {

        private VideoCapture video = new VideoCapture(0);
        private Mat bgr;

        public Form1()
        {
            InitializeComponent();

        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            bgr = video.RetrieveMat();
            bgr = bgr.Flip(FlipMode.Y);
            pictureBox1.BackgroundImage = bgr.ToBitmap();
            bgr.Release();
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            timer1.Start(); 
        }

    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/027b80e5-5e97-be9f-bec0-de36c4d46250.png)

いい感じだ。

### 「スタープラチナ」で男を出す

さて、いよいよスタープラチナを組み込んでゆく。
IT業界そこそこ長いが、スタープラチナを実装するのは初めてだ。

まずは好きなスタープラチナの画像を用意してプロジェクトのbin配下(Debug/Release)にコピーする。
尚、スタープラチナは背景透過で作成する事。

カメラ画像にかぶせるのはアルファブレンドを使う。
細かな説明は[OpenCVでアルファチャンネル付きpngを表示する](https://qiita.com/qinojp/items/f0e2e62a7c4935dffe7c)を見てもらったほうが良いかもしれない。
実行するとやたらめったらメモリ食ってるが仕事でもないので無視することにした。

``` CSharp:Form1
Mat frame = video.RetrieveMat();
Mat bgr = frame.Flip(FlipMode.Y);
Mat bgrTemp = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
bgr.ConvertTo(bgrTemp, MatType.CV_32FC3);
if (alphaBackground == null)
{
    // create front end Image and resize
    Mat stapura = Cv2.ImRead(".\\stapura.png", ImreadModes.Unchanged);
    Mat resizeStapura = resizeStapura = Mat.Zeros(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC4);
    Point2f[] src =
    {
        new Point2f(0,0),
        new Point2f(stapura.Cols, 0),
        new Point2f(stapura.Cols, stapura.Rows),
        new Point2f(0, stapura.Rows)
    };
    /*
    float startY = bgrTemp.Rows / 3.0f;
    float startX = bgrTemp.Cols / 3.0f;
    float height = bgrTemp.Rows - startY;
    float width = bgrTemp.Cols - startX;
    */
    float height = 290;
    float width = 300;
    float startY = bgrTemp.Rows - height;
    float startX = bgrTemp.Cols - width;
    Point2f[] dst =
    {
        new Point2f(startX, startY),
        new Point2f(startX + width, startY),
        new Point2f(startX + width, startY + height),
        new Point2f(startX, startY + height),
    };
    Mat resizeMat = Cv2.GetPerspectiveTransform(src, dst);
    Cv2.WarpPerspective(stapura, resizeStapura, resizeMat ,resizeStapura.Size(), InterpolationFlags.Cubic, BorderTypes.Transparent);
    stapura.Release();
    stapura = null;
    resizeMat.Release();
    resizeMat = null;

    // split channel 
    Mat[] chanelSplitedImage = Cv2.Split(resizeStapura);
    Mat alphaStapura1C = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_8UC1);
    Mat alphaUntiStapura1C = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_8UC1);
    chanelSplitedImage[3].ConvertTo(alphaStapura1C, MatType.CV_8UC1);
    Cv2.BitwiseNot(alphaStapura1C, alphaUntiStapura1C);
    Cv2.Normalize(alphaStapura1C, alphaStapura1C, 0.0f, 1.0f, NormTypes.MinMax);
    Cv2.Normalize(alphaUntiStapura1C, alphaUntiStapura1C, 0.0f, 1.0f, NormTypes.MinMax);

    Mat rgbStapuraTemp = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    Mat alphaStapuraTemp = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    
    Cv2.Merge( new Mat[]
            {
                    chanelSplitedImage[0],
                    chanelSplitedImage[1],
                    chanelSplitedImage[2]
            }
        , rgbStapuraTemp);
    Cv2.Merge( new Mat[]
            {
                    alphaStapura1C,
                    alphaStapura1C,
                    alphaStapura1C
            }
        , alphaStapuraTemp);

    Mat alphaBackgroundTemp = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    Cv2.Merge( new Mat[]
            {
                    alphaUntiStapura1C,
                    alphaUntiStapura1C,
                    alphaUntiStapura1C
            }
        , alphaBackgroundTemp);

    rgbStapura = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    alphaStapura = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    alphaBackground = new Mat(bgrTemp.Rows, bgrTemp.Cols, MatType.CV_32FC3);
    rgbStapuraTemp.ConvertTo(rgbStapura ,MatType.CV_32FC3);
    alphaStapuraTemp.ConvertTo(alphaStapura, MatType.CV_32FC3);
    alphaBackgroundTemp.ConvertTo(alphaBackground, MatType.CV_32FC3);

    resizeStapura.Release();
    rgbStapuraTemp.Release();
    alphaStapuraTemp.Release();
    alphaBackgroundTemp.Release();
    chanelSplitedImage[0].Release();
    chanelSplitedImage[1].Release();
    chanelSplitedImage[2].Release();
    chanelSplitedImage[3].Release();
    alphaStapura1C.Release();
    src = null;
    dst = null;
    rgbStapuraTemp = null;
    alphaStapuraTemp = null;
    alphaBackgroundTemp = null;
    resizeStapura = null;
    chanelSplitedImage[0] = null;;
    chanelSplitedImage[1] = null;;
    chanelSplitedImage[2] = null;;
    chanelSplitedImage[3] = null;;
    chanelSplitedImage = null;
    alphaStapura1C = null;
}
Mat tmp = rgbStapura.Mul(alphaStapura) + bgrTemp.Mul(alphaBackground);
tmp.ConvertTo(bgr, MatType.CV_8UC3);
bgrTemp.Release();
tmp.Release();
bgrTemp = null;
tmp = null;

if(bgr!= null)
{
    if (bitmap == null)
    {
        bitmap = bgr.ToBitmap();
    } else {
        OpenCvSharp.Extensions.BitmapConverter.ToBitmap(bgr, bitmap);
    }
    pictureBox1.Image = bitmap;
}
frame.Release();
frame = null;
bgr.Release();
bgr = null;
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/4ef778e5-1574-ad49-2baa-5eefff07d9db.png)

ちゃんとスタープラチナが出せた。


### 「ザ・ワールド」で時を止める

スタープラチナがだせたら次は時間を止める。
おそらくQiitaの読者の中でも時間を止めたことがある人は少ないんじゃなかろうかと思う。
時間を止めるとどうなるかと言うと、`画面中央から放射状に青色が広がる`のだ。
頭のいい人たちはこの説明でざわつくかもしれないが、Netflixでもそうなってたんだからしょうがない。

もう少し具体的に言うと、

 + 3秒ぐらいかけて青くなる
 + 背景はすでに止まってる

スタープラチナの応用で丸型のアルファブレンドを使って作成する。
もっといいやり方があると思うが、そう思う人がやればいいと思うし、できたら教えてほしい。
マネする。

下記コードをタイマーで回して描画する。。

``` bash
if (theWorldBase == null)
{
    theWorldBase = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat theWorldBaseTemp = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    bgr.ConvertTo(theWorldBaseTemp, MatType.CV_32FC3);
    Mat frontend = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat backend = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Multiply(rgbStapura, alphaStapura, frontend);
    Cv2.Multiply(theWorldBaseTemp, alphaBackground, backend);
    Cv2.Add(frontend, backend, theWorldBase);
    theWorldBaseTemp.Release();
    theWorldBaseTemp = null;
    frontend.Release();
    frontend = null;
    backend.Release();
    backend = null;
    centerPosition = new OpenCvSharp.Point(bgr.Cols / 2.0f, bgr.Rows / 2.0f);
    circleSize = 1;
    // magic 5 px
    circleMaxSize = (int)Math.Ceiling(Math.Sqrt(Math.Pow(centerPosition.X, 2) + Math.Pow(centerPosition.Y, 2))) - 5;

    theWorldComplete = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
};
if (circleSize < (circleMaxSize))
{
    circleSize += (int)Math.Floor(circleMaxSize / 30.0f);
    if (circleSize > circleMaxSize)
    {
        circleSize = circleMaxSize;
    }
    Console.WriteLine(circleSize);

    // create circle blue mask
    Mat blueMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
        Cv2.Circle(blueMask, centerPosition, circleSize, new Scalar(1.0d, 0.0d, 0.0d), -1);

    // create alpha mask
    Mat[] blueMaskArr = Cv2.Split(blueMask);
    Mat frontAlphaMask1C = blueMaskArr[0].Clone();

    Mat blueBackgroundMask = Mat.Ones(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
        Cv2.Circle(blueBackgroundMask, centerPosition, circleSize, new Scalar(0.0d, 1.0d, 1.0d), -1);
    Mat[] backgroundMaskArr = Cv2.Split(blueBackgroundMask);
    Mat backAlphaMask1C = backgroundMaskArr[0];
    
    Cv2.Normalize(frontAlphaMask1C, frontAlphaMask1C, 0.0f, 1.0f, NormTypes.MinMax);
    Cv2.Normalize(backAlphaMask1C, backAlphaMask1C, 0.0f, 1.0f, NormTypes.MinMax);

    Mat frontAlphaMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat frontAlphaMaskTemp= new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Merge(new Mat[]
        {
            frontAlphaMask1C,
            frontAlphaMask1C,
            frontAlphaMask1C
        }
        , frontAlphaMaskTemp);
    frontAlphaMaskTemp.ConvertTo(frontAlphaMask, MatType.CV_32FC3);

    Mat backAlphaMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat backAlphaMaskTemp = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Merge(new Mat[]
        {
            backAlphaMask1C,
            backAlphaMask1C,
            backAlphaMask1C
        }
        , backAlphaMaskTemp);
    backAlphaMaskTemp.ConvertTo(backAlphaMask, MatType.CV_32FC3);

    // create background
    Mat background = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Multiply(theWorldBase, backAlphaMask, background);

    // create frontend
    Mat frontend = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Multiply(theWorldBase, frontAlphaMask, frontend );
    Cv2.Multiply(frontend, blueMask, frontend );

    // create Image
    Cv2.Add(frontend, background, theWorldComplete);
    theWorldComplete .ConvertTo(bgr, MatType.CV_8UC3);

    blueMaskArr[0].Release();
    blueMaskArr[1].Release();
    blueMaskArr[2].Release();
    blueBackgroundMask.Release();
    backgroundMaskArr[0].Release();
    backgroundMaskArr[1].Release();
    backgroundMaskArr[2].Release();
    backgroundMaskArr = null;
    blueMaskArr = null;
    frontAlphaMaskTemp.Release();
    frontAlphaMaskTemp = null;
    backAlphaMask1C.Release();
    backAlphaMask1C = null;
    backAlphaMaskTemp.Release();
    backAlphaMaskTemp = null;
    frontAlphaMask1C.Release();
    frontAlphaMask1C = null;
    frontAlphaMask.Release();
    blueMask.Release();
    backAlphaMask.Release();
    background.Release();
    frontend.Release();
} else {
    Mat blueMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3, new Scalar(1,0f, 0.0f, 0.0f));
    Cv2.Multiply(theWorldBase, blueMask, theWorldComplete);
    theWorldComplete.ConvertTo(bgr, MatType.CV_8UC3);
    blueMask.Release();
    blueMask = null;
}
```

時間が止められるとこうなるはず。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/6b4d9354-4822-e948-aa86-09d821427355.png)


### 「時は動き出す」で時を動かす

スタープラチナも出せて時間も止めれた。あとは動かすだけ。ちょろい。
時間が動き出すと`画面中央に向かって青色の領域が小さくなる`だ。
ちなみに青色の領域がなくなると時間が動き出す。

特に書くことないのでタイマーで回すコードだけ

``` CSharp:Form1
if (circleSize >= (circleMaxSize)) { 
    circleSize = circleMaxSize;
    startTimeComplete = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3, new Scalar(1,0f, 0.0f, 0.0f));
}
if (circleSize > 5) {
    circleSize -= (int)Math.Floor(circleMaxSize / 30.0f);
    if (circleSize > circleMaxSize)
    {
        circleSize = circleMaxSize;
    }
    Console.WriteLine(circleSize);

    // create circle blue mask
    Mat blueMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
        Cv2.Circle(blueMask, centerPosition, circleSize, new Scalar(1.0d, 0.0d, 0.0d), -1);

    // create alpha mask
    Mat[] blueMaskArr = Cv2.Split(blueMask);
    Mat frontAlphaMask1C = blueMaskArr[0].Clone();

    Mat blueBackgroundMask = Mat.Ones(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
        Cv2.Circle(blueBackgroundMask, centerPosition, circleSize, new Scalar(0.0d, 1.0d, 1.0d), -1);
    Mat[] backgroundMaskArr = Cv2.Split(blueBackgroundMask);
    Mat backAlphaMask1C = backgroundMaskArr[0];
    
    Cv2.Normalize(frontAlphaMask1C, frontAlphaMask1C, 0.0f, 1.0f, NormTypes.MinMax);
    Cv2.Normalize(backAlphaMask1C, backAlphaMask1C, 0.0f, 1.0f, NormTypes.MinMax);

    Mat frontAlphaMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat frontAlphaMaskTemp= new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Merge(new Mat[]
        {
            frontAlphaMask1C,
            frontAlphaMask1C,
            frontAlphaMask1C
        }
        , frontAlphaMaskTemp);
    frontAlphaMaskTemp.ConvertTo(frontAlphaMask, MatType.CV_32FC3);

    Mat backAlphaMask = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Mat backAlphaMaskTemp = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Merge(new Mat[]
        {
            backAlphaMask1C,
            backAlphaMask1C,
            backAlphaMask1C
        }
        , backAlphaMaskTemp);
    backAlphaMaskTemp.ConvertTo(backAlphaMask, MatType.CV_32FC3);

    // create background
    Mat background = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Multiply(theWorldBase, backAlphaMask, background);

    // create frontend
    Mat frontend = new Mat(bgr.Rows, bgr.Cols, MatType.CV_32FC3);
    Cv2.Multiply(theWorldBase, frontAlphaMask, frontend );
    Cv2.Multiply(frontend, blueMask, frontend );

    // create Image
    Cv2.Add(frontend, background, startTimeComplete);
    startTimeComplete.ConvertTo(bgr, MatType.CV_8UC3);

    blueMaskArr[0].Release();
    blueMaskArr[1].Release();
    blueMaskArr[2].Release();
    blueBackgroundMask.Release();
    backgroundMaskArr[0].Release();
    backgroundMaskArr[1].Release();
    backgroundMaskArr[2].Release();
    backgroundMaskArr = null;
    blueMaskArr = null;
    frontAlphaMaskTemp.Release();
    frontAlphaMaskTemp = null;
    backAlphaMask1C.Release();
    backAlphaMask1C = null;
    backAlphaMaskTemp.Release();
    backAlphaMaskTemp = null;
    frontAlphaMask1C.Release();
    frontAlphaMask1C = null;
    frontAlphaMask.Release();
    blueMask.Release();
    backAlphaMask.Release();
    background.Release();
    frontend.Release();
} else {

    currentStage = stage.None;
}
```

# まとめ

調子に乗ってスタープラチナを呼び出したり、クラウドで色々やってみようとすると、
ちゃんと請求が来る。azure賢い。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/42445/8d5ba191-3f61-9792-d21f-c4fe6fbd6822.png)