こんそーるすーぷのレシピはこちら

いちおう仕事なのに趣味の人に劣って焦り

人工知能についてのメモ②「Python環境の設定」

人工知能についてのメモ①では人工知能についてを軽く知り、それを実現するためには機械学習または深層学習が方法としてあり、いろいろな言語があるけどPythonが一番人気というところまでやりました。
今回はPythonの環境を整えて少し触るところまでです。

Anacondaのインストール

Pythonを調べていると便利便利と書いてあるのでAnacondaというものを使用していきたいと思います。
Anacondaは「Python本体と、Pythonでよく利用されるライブラリ」がまとまっているパッケージらしいです。(開発ツールみたいなものと理解しておきました)
www.anaconda.com

Anacondaのダウンロードページから私はPython 3.6 version (64bit) のインストーラーをダウンロードしました。
途中に選択やチェックボックス等ありましたが一応そのまま「Next > Next > Install」といった感じでインストーラーに従いました。

※注意:インストール途中にcmd.exeが開きますが閉じてしまうとインストールできないものが出てきたりするので放置しましょう。

対話型実行環境 Jupyter Notebook

Pythonのコードを含んだWebページを作成できる機能(つまりGitをcmd.exeでやるかSourcetreeのようなGUIツール使うかみたいなものと認識しました)
これを使って可視化しながら簡単なものからやっていきたいと思います。

インストールしたツールはAnaconda-Navigatorから開けるようです。
f:id:nanokanato:20180228121348p:plain:w100f:id:nanokanato:20180228121829p:plain:w300

またJupyter Notebookの直接リンクがありますがブラウザの方が開かなかったことがあるのでAnaconda-Navigatorから手順を踏んで開いた方がよさそうです。

Jupyter Notebookを開くとJupyter NotebookのWindowとブラウザが立ち上がります。
f:id:nanokanato:20180228122544p:plain:w300

「Documents\Python Jupyter Note」のフォルダを作りそこで作業することにしました。

  1. Documentsをクリックして階層を移動。
  2. 右上のNewをクリックし「Folder」を選びリストにUntitled Folderを追加。
  3. Untitled Folderをチェックし、左上に出てくるRenameを押す。
  4. フォルダの名前が変更できますので適宜変更してください。

エクスプローラーでフォルダを作り、再度ブラウザを開くことでもOKです。

Pythonの基礎

作業フォルダの作成

Python Jupyter Noteのブラウザで先ほど作った「Documents\Python Jupyter Note」を開きます。
右上の「New」から「Folder」を選択し、Untitled Folderを作成します。
Untitled Folderを選択して、「Rename」でHello Pythonという名前に変更します。
今回は「Documents\Python Jupyter Note\Hello Python」を作業フォルダとして利用します。

Python3の作成

右上の「New」から「Python3」を選択します。
f:id:nanokanato:20180228133514p:plain:w300
作業フォルダにUntitled.ipynbが作成され、別のブラウザでチャットのような画面が開きます。
f:id:nanokanato:20180228133535p:plain:w300

Hello Python

作成したUntitled.ipynbの「In [ ]:」の横の入力欄に以下のコードを打って「Shift」+「Enter」を押してください。

print('Hello Python!!')

これで入力に足して「Hello Python!!」と出力されました。
f:id:nanokanato:20180228133835p:plain:w300

コードを入力し「Shift」+「Enter」で実行して出力という作業が、Jupyter NotebookでのPythonプログラミングの基本となります。

ちなみに一度入力した場所のコードを修正して実行することで出力結果も更新されます。

変数を用いた計算

変数を用いて計算なども可能です。

a = 12
b = 35
c = 53
a + b + c

aに12、bに35、cに53を代入してその3つを足した数、100を出力します。
f:id:nanokanato:20180228134726p:plain:w300

ファイルの読み込み

機械学習にはデータの入力が必要です。
いちいちデータを手で入力していてはキリがないのでファイルを読み込む機能を使用します。

まずは読み込むためのファイルを作成します。
作業フォルダにて右上の「New」から「Text File」を選択します。
その後作成されたUntitled.txtをチェックして左上の「Rename」でData.csvに名前を変更します。
Data.csvを開いてcsvのコンマ区切り形式で適当にデータを作ります。
f:id:nanokanato:20180228141432p:plain:w300

作成したファイルをUntitled.ipynbと同じフォルダに配置します。
f:id:nanokanato:20180228140100p:plain:w300

以下のコードを実行することでファイルを読み込んで出力することができます。

import pandas as pd
data = pd.read_csv('Data.csv')
data

f:id:nanokanato:20180228141704p:plain:w300
一番左の太字の0,1,2は何個目のデータかを表しており、その右側はファイルに入力したものと一致しています。
全て空白の行があった場合はスルーされます。

また、ファイルの拡張子がtxtの場合でもコンマ区切りになっていれば問題ないようです。
※しかし一般的には拡張子はcsvなので合わせた方が良いでしょう。
f:id:nanokanato:20180228142337p:plain:w300

「import pandas as pd」はpdにpandasというデータ分析に特化したライブラリをインポートしており、そこからデータを読み込むことでエクセルやCSVデータを簡単に扱うことが可能になっています。

ファイルの保存

画面上の「Untitled」という部分を任意の名前(HelloPython)に変更し、左上の「File」の下にある保存ボタン(フロッピーディスクのアイコン)を押して完了です。

保存したファイルをPythonとして書き出す

画面左上の「File > Download as > Python(.py)」を押すことで.pyファイルとして書き出せます。

人工知能についてのメモ③では教師なし学習を行います。

人工知能についてのメモ①「基礎知識の確認」

ほぼ個人メモなので雑です。理解していくために書き殴ります。
人工知能などに関しての技術進歩が速いので今のうちに基礎を理解しておかないとチンプンカンプンになると思ったのではじめました。(使用だけだと簡易ライブラリなどは出てきていますが...原理的な部分)
定期で何かわかったことがあれば追記したりします。
解釈の違いなど当然あると思いますのでご指摘いただけると嬉しいです。

そもそも人工知能(AI)とは

膨大なデータを元に分析して分析・判断し、検出・抽出を行う機能、仕組み。
今回のような分析・判断するAIを強いAIと呼び、
プログラムで書かれたような知能の低いAIのことを弱いAIと呼ぶそうです。

よく出てくる単語

  • アルゴリズム
    • 学習したデータから検出・抽出する方法のこと。
  • データ
    • 主に学習の材料になるデータのこと。
      画像解析なら同じ対象が映った複数画像の集合や、Excelのような表など。
  • 入力
    • 機械学習のプログラムに対してデータを入力すること。
  • 出力
    • 入力から機械学習のプログラムが導き出した答えのこと。
  • ラベル
    • 入力とそれに対応すべき出力を人間が例として示した見本のこと
  • データマイニング
    • 様々なデータ解析の技法を大量のデータに網羅的に適用することで知識を取り出す技術のこと。
      通常では想像が及びにくい、発見ができる可能が多い。

人工知能を実現する方法

  • 機械学習
    機械学習を一言で表すと「明示的にプログラムしなくても学習する能力をコンピュータに与える研究分野」
    • 教師あり学習(Supervised Learning)
      • 入力と出力の関係を学習する方法。
        ラベルを提示することでそこから解釈されるアルゴリズムを生成する。
    • 教師なし学習(Unsupervised Learning)
    • 強化学習(Reinforcement Learning
      • 価値・評価を最大化するような行動を学習する方法。
        人間が与えた環境を観測し、どう行動すべきかを学習する。行動によって環境に影響が出て、環境から報酬という形でフィードバックを得ることで学習アルゴリズムのガイドとする。
        例として「将棋をして、より勝率の高い戦略を学習させる」などという方が個人的にはわかりやすい。
  • 深層学習(ディープラーニング
    ディープラーニング機械学習をさらに発展させたもの。主にデータを分析する際に使う枠組みが異なっていて、人間の神経を真似て作った「ニューラルネットワーク」で、コンピューターによるデータの分析と学習を強化している。
    機械学習ではデータに対して「入力と出力の関係」や「データの構造」「価値・評価を最大化する」など重点を人間が用意したのに対し、その部分もディープラーニングに学習させ判断させようとしているらしい....。(相手が何を求めているかを考えることでより人間に近くなっている!)

人工知能を作るにあたって選べる言語

  1. Python
  2. Java
  3. R
  4. C++
  5. C
  6. JavaScript
  7. Scala
  8. Julia

個人的にはJava,Js,C++あたりがうれしいのですが、一般的にはPythonが多く、記事も多いのでまずはPythonではじめるのが無難な感じでした。

人工知能についてのメモ②ではPythonの環境を整えます。

【Unity】WindowsPCのスクリーンをキャプチャして再生する

以下のほうが高機能で高速です。
tips.hecomi.com

しかし...GTX10XX系を搭載したゲーミングPCでは動かないそうで...??
実際、GTX1070を積んだノートで動作しなかった...

なので動作としては遅いが何となくPC画面をUnity内に描画したくてやってみました。
他に解決策があればぜひ教えていただきたいです。

ソースはこちら、サンプルをいくつか用意したのでどうぞ
github.com

実際に動かしてみるとこんな感じです。
exeでもWindowsであればもちろん動きます。
・Windows10
・Unity2017.3.1

【Unity】UnityEditor上でScriptのUpdate()などを動作させる

スクリプトを Edit モードで実行します
簡潔に「[ExecuteInEditMode]」をつけるだけです。

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class ExampleClass : MonoBehaviour {
    public Transform target;
    void Update() {
        if (target)
            transform.LookAt(target);
        
    }
}

Editor上でログを出力したり、UIを動的に変更するのに使いました。

  • Update はシーンの何かが変更されたときのみ呼び出しされます。
  • OnGUI はゲームビューが Event を受け取った時のみ呼び出しされます。
  • OnRenderObject および他のレンダリング コールバック関数はシーンビューまたはゲームビューの再描画の都度、呼び出しされます。

と公式に書いてますので、他にもOnGUIやOnRenderObjectも動作するようです。

docs.unity3d.com

【Unity】Windows, Macで画像を印刷する方法

Windows, Macからデフォルトのプリンター設定を呼び出して印刷をやったのでメモとして残します。

検証環境
Unity : 2017.3.0f3
Mac : Sierra 10.12.6
Windows : Windows10, Windows7

はじめに

WindowsでもMacでも画像を印刷する際にパスの指定が必要になるため、画像の保存が必要です。
念のため記載しておきます。

Texture2Dをタイムスタンプをファイル名としてpngで保存します。
保存先はアクセス可能な場所である必要があります。

using UnityEngine;
using System.IO;

public class hogehoge
{
    public string SaveTexture2D(Texture2D texture2D)
    {
        byte[] bytes = texture2D.EncodeToPNG();
        string fileName = "image_" + (System.DateTime.Now.Ticks / 10000000) + ".png";
        string filePath = "hogehoge/hoge/"+fileName;
        if (bytes != null && 0 < bytes.Length) File.WriteAllBytes(filePath, bytes);
        else filePath = null;
        
        return filePath;
    }
}

Macで印刷する方法

Macだとlprコマンドで印刷するのが楽です。

using UnityEngine;
using System.Diagnostics;

public class hogehoge
{
    public delegate void PrintCallBack(string message);

    public void Print(string filePath, PrintCallBack callback)
    {
        if (callback == null) return;
        if (filePath == null)
        {
            callback("filePath is null");
            return;
        }

        string cmd = "lpr "+filePath;
        
        var process = new Process();
        process.StartInfo.FileName = "/bin/bash";
        process.StartInfo.Arguments = "-c \" " + cmd + " \"";
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardOutput = true;
        process.Start();

        string output = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        process.Close();
        
        callback();
    }
}

Windows

Windowsだとlprコマンドの場合、ネットワークプリンタのIPと名前が必要だったりと厄介なのでSystem.Drawing.dllを使用します。

以下はdllを使用する上での設定です。

  • Assets以下の階層にPluginsフォルダを作成し、System.Drawing.dllを複製します。
    dllはだいたいここにあります。
    「C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll」
  • File>Build Setting>PlayerSettingsのOther SettingsでOptimizationのApi Compatibility Levelを「.NET2.0」に変更。

もし、VisualStudioが「using System.Drawing」などでエラーを出す場合

ソリューションエクスプローラーのプロジェクト名の下にある参照を開く。
Boo.LangやSystem.hogehogeなどを右クリックし、オブジェクトブラウザーで表示を押す。
オブジェクトブラウザーでSystem.Drawingを選択。
オブジェクトブラウザーの上方にある、+ボタン(ソリューションエクスプローラー内の選択されたプロジェクトに参照を追加)を押す。

using UnityEngine;
using System.Drawing;
using System.Drawing.Printing;

public class hogehoge
{
    public delegate void PrintCallBack(string message);
    private string imagePath = null;
    private PrintCallBack printCallback;

    public void Print(string filePath, PrintCallBack callback)
    {
        if (callback == null) return;
        if (filePath == null)
        {
            callback("filePath is null");
            return;
        }

        imagePath = filePath;
        printCallback = callback;

        //PrintDocumentオブジェクトの作成
        PrintDocument pd = new PrintDocument();

        //PrintPageイベントハンドラの追加
        pd.PrintPage += new PrintPageEventHandler(pd_PrintPage);

        //Print
        pd.EndPrint += new PrintEventHandler(pd_PrintEnd);

        //PrintControllerプロパティをStandardPrintController
        pd.PrintController = new StandardPrintController();
        try
        {
            //印刷を開始する
            pd.Print();
        }
        catch (System.Exception ex)
        {
            if (printCallback != null) printCallback(ex.Message);
            printCallback = null;
        }
    }
    
    //プリントが終了した
    private static void pd_PrintEnd(object sender, PrintEventArgs e)
    {
        if (printCallback != null) printCallback(imagePath);
        printCallback = null;
    }

    //プリント画像を指定する
    private static void pd_PrintPage(object sender, PrintPageEventArgs e)
    {
        //印刷画像を読み込む
        System.Drawing.Image printImg = null;
        if (System.IO.File.Exists(imagePath))
        {
            printImg = System.Drawing.Image.FromFile(imagePath);
            if (printImg != null)
            {
                DrawAspectFillImage(e.Graphics, printImg, new RectangleF(0,//e.PageSettings.PrintableArea.Left,
                                                                         0,//e.PageSettings.PrintableArea.Top, 
                                                                         e.PageSettings.PrintableArea.Width,
                                                                         e.PageSettings.PrintableArea.Height), true);
                printImg.Dispose();
            }
        }
        //次のページがないことを通知する
        e.HasMorePages = false;
    }

    //プリント画像を描画する
    public static void DrawAspectFillImage(System.Drawing.Graphics graphics, Image image, RectangleF rectangle, bool isKeepAspectRatio)
    {
        if (graphics == null) throw new System.ArgumentNullException();
        if (rectangle.Width <= 0 || rectangle.Height <= 0) throw new System.ArgumentOutOfRangeException();
        if (image == null) return;

        Debug.Log("("+rectangle.Left+","+rectangle.Top+") - ("+rectangle.Width+","+rectangle.Height+")");
        var l = rectangle.Left;
        var t = rectangle.Top;
        var w = (float)image.Width;
        var h = (float)image.Height;
        if (isKeepAspectRatio)
        {
            var r = h / w;
            w = rectangle.Height / r;
            h = rectangle.Height;
            if (w > rectangle.Width)
            {
                w = rectangle.Width;
                h = rectangle.Width * r;
            }
            l += (rectangle.Width - w) / 2;
            t += (rectangle.Height - h) / 2;
        }
        graphics.DrawImage(image, l, t, w, h);
    }
}

注意としてはUnityEditorでは動くけど、exeにすると動かない場合は「.NET2.0」の設定を見直してみてください。

【Maya】Pythonで線を描画する

MayaをPythonでいじれるということなので手始めに検証です。
今回は線を引いてみました。

import maya.cmds as cmds

transform = cmds.createNode('transform', n='curve1')
nurbsCurve = cmds.createNode('nurbsCurve',n='curveShape1', p=transform)
cmds.curve(nurbsCurve, p=[(0, 0, 0), (3, 5, 6), (10, 12, 14), (9, 9, 9)])
  1. transformのcurve1を作成
  2. curve1を親にしてnurbsCurveのcurveShape1を作成
  3. curveShape1にカーブの値を設定

表示結果はこんな感じ
nurbsCurveで指定したので綺麗な曲線になるかと思ったのですがカクカクになってますね....
f:id:nanokanato:20180118155418p:plain:w400

【Unity】OpenCVで手の判別

UnityでARKitなど画像から何かを認識していく便利な機能がどんどん増えているなか、いまさらOpenCVをやります。
過去にやったもののまとめ + αです。
(まだ、ソースがごちゃっとしてますが自分の備忘録として記載します)

OpenCVについて

今更の今更ですが、OpenCVについて
やっている人が多いので記事が多すぎて公式の方が埋もれ気味です...

公式Webはここ
opencv.org

公式Gitはここ
github.com

Wikiのコピペですが「画像処理・画像解析および機械学習等の機能を持つC/C++JavaPythonMATLAB用ライブラリ」だそうです。
今、主流になっている技術の詰め合わせという感じですね。
OpenCV自体は無料でDLできるのでガリガリ書けば最新技術がガリガリできるわけなんです。

前はOpenCVforUnityでやってましたが...

過去に使っていたOpenCVforUnity...
https://www.assetstore.unity3d.com/jp/#!/content/21088www.assetstore.unity3d.com

こういう記事を書いてましたが...
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp

iOS11で久々にビルドすると通らない...AssetStoreの更新分があったので更新しましょう...。

MatとUnity形式の画像の変換

OpenCVではcv::matという形式を使って画像を加工したりします。
UnityではTextureなど別の形式なのでその辺を補ってくれる部分をHGMatとしてまとめました。

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public class HGMat : MonoBehaviour {

		[SerializeField] private Image TextureImage;
		[HideInInspector] public Mat rgbaMat;
		[HideInInspector] public Color32[] colors;
		private Texture2D _convertTexture;

		/*======================================
		* Default Method
		======================================*/
		/// <summary>
		/// Raises the destroy event.
		/// </summary>
		void OnDestroy()
		{
			Dispose();
		}

		/*======================================
		* Override Method
		======================================*/
		// Use this for initialization
		protected virtual void Start() {
			
		}

		// Update is called once per frame
		protected virtual void Update() {
			
		}

		/// <summary>
		/// Mat the retouch.
		/// </summary>
		/// <param name="rgbaMat">Mat.</param>
		protected virtual void MatRetouch(Mat _rgbaMat) 
		{

		}

		/// <summary>
		/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
		/// </summary>
		/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
		/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
		/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
		/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
		protected virtual void Dispose()
		{
			if (_texture2D != null) 
				_texture2D = null;
			if (colors != null)
				colors = null;
			if (rgbaMat != null)
			{
				rgbaMat.Dispose();
				rgbaMat = null;
			}
		}

		/*======================================
	    * Private Method
		======================================*/
		/// <summary>
		/// Sprites from _texture2D.
		/// </summary>
		/// <returns>The from _texture2D.</returns>
		/// <param name="texture">Texture2D.</param>
		private Sprite _spriteFromTexture2D(Texture2D texture)
		{
			//Texture2DからSprite作成
			Sprite sprite = null;
			if (texture != null)
				sprite = Sprite.Create(texture, new UnityEngine.Rect(0, 0, texture.width, texture.height), Vector2.zero);
			return sprite;
		}

		/*======================================
	    * Convert Method
		======================================*/
		/// <summary>
		/// Shows the retouch Mat.
		/// </summary>
		private void _showRetouchMat() 
		{
			MatRetouch(rgbaMat);
			if (TextureImage != null)
			{
				Utils.matToTexture2D(rgbaMat, _convertTexture, colors);
				Sprite _sprite = _spriteFromTexture2D(_convertTexture);
				TextureImage.sprite = _sprite;
			}
		}

		/// <summary>
		/// Texture2D to Mat.
		/// </summary>
		private Texture2D _texture2D = null;
		public Texture2D texture2D 
		{
			get { return _texture2D; }
			set {
				_texture2D = value;
				_texture2DToMat();
			}
		}
		private void _texture2DToMat() 
		{
			if (_texture2D != null)
			{
				//Texture2DをMatに変換する
				if (colors == null || colors.Length != _texture2D.width * _texture2D.height)
					colors = new Color32[_texture2D.width * _texture2D.height];
				if (_convertTexture == null || _convertTexture.width != _texture2D.width || _convertTexture.height != _texture2D.height)
					_convertTexture = new Texture2D (_texture2D.width, _texture2D.height, TextureFormat.RGBA32, false);
				rgbaMat = new Mat(_texture2D.height, _texture2D.width, CvType.CV_8UC4);
				Utils.texture2DToMat(_texture2D, rgbaMat);

				//Matを修正して画面に描画
				_showRetouchMat();
			}
		}

		/// <summary>
		/// WebCamTexture to Mat.
		/// </summary>
		private WebCamTexture _webCamTexture = null;
		public WebCamTexture webCamTexture 
		{
			get { return _webCamTexture; }
			set {
				_webCamTexture = value;
				_webCamTextureToMat();
			}
		}
		private void _webCamTextureToMat()
		{
			if (_webCamTexture != null && _webCamTexture.isPlaying && _webCamTexture.didUpdateThisFrame)
			{
				//WebCamTextureをMatに変換する
				if (colors == null || colors.Length != _webCamTexture.width * _webCamTexture.height)
					colors = new Color32[_webCamTexture.width * _webCamTexture.height];
				if (_convertTexture == null || _convertTexture.width != _webCamTexture.width || _convertTexture.height != _webCamTexture.height)
					_convertTexture = new Texture2D (_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false);
				rgbaMat = new Mat(_webCamTexture.height, _webCamTexture.width, CvType.CV_8UC4);
				Utils.webCamTextureToMat(_webCamTexture, rgbaMat, colors);

				//Matを修正して画面に描画
				_showRetouchMat();
			}
		}
	}
}

TextureImageにInspectorからUI.Imageをセットすることで加工後のMatを描画できます。
webCamTexture, texture2DのどちらかにUnityで取得した画像を入れることでcv::mat形式に変換されます。
その後「MatRetouch(Mat _rgbaMat) 」が呼ばれてそこで編集したMatがUI.Imageに描画されます。

このHGMatをベースとして画像からカメラからの入力を行います。

画像からMatに変換して加工

HGMatの使い方説明になります。
継承クラスを用意してInspectorからTexture2Dが入力できるようにします。
この時、入力する画像の設定がAdvanced > Read/Write Enabledがtrueになっていないとエラーがでます。
f:id:nanokanato:20171218104017p:plain:w300

Start, Update, MatRetouch, Disposeをoverrideしており、
Startで初期化、
UpdateなどでHGMatへ画像の入力、
MatRetouchでMat画像の加工、
Disposeで解放
を行います。

using UnityEngine;
using OpenCVForUnity;
using HGHandGesture;

public class ImageTracking : HGMat {

	public Texture2D HandImage;

	/*======================================
    * Override Method
	======================================*/
	// Use this for initialization
	protected override void Start()
	{
		base.Start();
	}

	// Update is called once per frame
	protected override void Update()
	{
		base.Update();

		//加工用のTexture2Dを送りMatへの変換を待つ
		base.texture2D = HandImage;
	}

	/// <summary>
	/// Mat the retouch.
	/// </summary>
	/// <param name="rgbaMat">Mat.</param>
	protected override void MatRetouch(Mat _rgbaMat)
	{
		//変換されたMatを加工する
		base.MatRetouch(_rgbaMat);
	}

	/// <summary>
	/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
	/// </summary>
	/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
	/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
	/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
	/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
	protected override void Dispose()
	{
		//破棄処理を記載
		base.Dispose();
	}
}

カメラ映像をMatに変換して加工

Webカメラもかなり使う機能なのでまとめました。
HGMatのWebカメラ特化クラスです。

using System.Collections;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public class HGCamera : HGMat {

		[System.Serializable] public class WebCameraData 
		{
			public string DeviceName = null;
			public Size Size = new Size(1136, 640);
			public bool IsFrontFacing = false;
		}
		[SerializeField] private WebCameraData _webCameraData = new WebCameraData();

		private WebCamTexture _webCamTexture;
		private WebCamDevice _webCamDevice;

		private bool _isInitWaiting = false;
		private bool _hasInitDone = false;

		/*======================================
	    * Override Method
		======================================*/
		// Use this for initialization
		protected override void Start() {
			base.Start();
			Initialize();
		}

		// Update is called once per frame
		protected override void Update() {
			base.Update();
			if (_hasInitDone) base.webCamTexture = _webCamTexture;
		}

		/// <summary>
		/// Mat the retouch.
		/// </summary>
		/// <param name="rgbaMat">Mat.</param>
		protected override void MatRetouch(Mat _rgbaMat)
		{
			base.MatRetouch(_rgbaMat);
		}

		/// <summary>
		/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
		/// </summary>
		/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
		/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
		/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
		/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
		protected override void Dispose()
		{
			_isInitWaiting = false;
			_hasInitDone = false;

			if (_webCamTexture != null) 
			{
				_webCamTexture.Stop ();
				_webCamTexture = null;
			}

			base.Dispose();
		}

		/*======================================
	    * Public Method
		======================================*/

		public void Play()
		{
			if (_hasInitDone) webCamTexture.Play();
		}

		public void Pause()
		{
			if (_hasInitDone) webCamTexture.Pause();
		}

		public void Stop()
		{
			if (_hasInitDone) webCamTexture.Stop();
		}

		public void ChangeCamera()
		{
			if (_hasInitDone && _webCameraData != null)
			{
				_webCameraData.IsFrontFacing = !_webCameraData.IsFrontFacing;
				Initialize();
			}
		}

		/*======================================
	    * Private Method
		======================================*/

		private void Initialize()
		{
			if (_isInitWaiting) return;
			StartCoroutine(_Initialize());
		}

		private IEnumerator _Initialize ()
		{
			if (_webCameraData == null)
				yield return null;
			
			if (_hasInitDone) Dispose();
			_isInitWaiting = true;

			if (!string.IsNullOrEmpty(_webCameraData.DeviceName))
				_webCamTexture = new WebCamTexture(_webCameraData.DeviceName, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);

			if (_webCamTexture == null)
			{
				for (int cameraIndex = 0; cameraIndex < WebCamTexture.devices.Length; cameraIndex++) 
				{
					if (WebCamTexture.devices[cameraIndex].isFrontFacing == _webCameraData.IsFrontFacing)
					{
						_webCamDevice = WebCamTexture.devices[cameraIndex];
						_webCamTexture = new WebCamTexture (_webCamDevice.name, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
						break;
					}
				}
			}

			if (_webCamTexture == null) 
			{
				if (WebCamTexture.devices.Length > 0) 
				{
					_webCamDevice = WebCamTexture.devices [0];
					_webCamTexture = new WebCamTexture(_webCamDevice.name, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
				} 
				else _webCamTexture = new WebCamTexture((int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
			}

			if (_webCamTexture != null)
			{
				_webCamTexture.Play();

				while (true) 
				{
					if (_webCamTexture.didUpdateThisFrame)
					{
						_isInitWaiting = false;
						_hasInitDone = true;
						OnInited();
						break;
					} 
					else yield return 0;
				}
			}
		}

		private void OnInited ()
		{
			float width = _webCamTexture.height;
			float height = _webCamTexture.width;

			float widthScale = (float)Screen.width/width;
			float heightScale = (float)Screen.height/height;
			if (widthScale < heightScale)
				Camera.main.orthographicSize = (width*(float)Screen.height/(float)Screen.width)/2;
			else Camera.main.orthographicSize = height/2;
		}
	}
}

HGCameraを継承することで簡単にWebカメラの加工と描画ができるようになりました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;
using HGHandGesture;

public class CameraTracking : HGCamera {

	/*======================================
    * Override Method
	======================================*/
	// Use this for initialization
	protected override void Start()
	{
		base.Start();
	}
	
	// Update is called once per frame
	protected override void Update () {
		base.Update();
	}

	protected override void MatRetouch(Mat _rgbaMat)
	{
		base.MatRetouch(_rgbaMat);
	}

	protected override void Dispose()
	{
		base.Dispose();
	}
}

OpenCVで手の判別

やっと本題、上の機能を使ってOpenCVで手の判別をします。
使うメソッドはMatRetouchだけなので今後ここのみ記載します。

protected override void MatRetouch(Mat _rgbaMat)
{
	base.MatRetouch(_rgbaMat);
	//ここで処理して手を判別する
}

HGColorSpuiter.csはタップした位置からColorを取得したり、ColorとScalarの変換を行うクラスです。
ScalarはOpenCVで色を表すクラスと思っています()

using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public static class HGColorSpuiter
	{
		public static Point storedTouchPoint = null;

		//ColorをScalarに変換
		public static Scalar ColorToScalar(Color color) 
		{
			//   0,   0,   0, 255:黒
			// 255,   0,   0, 255:赤
			//   0, 255,   0, 255:緑
			//   0,   0, 255, 255:青
			// 255, 255, 255, 255:白
			return new Scalar(color.r*255f, color.g*255f, color.b*255f, color.a*255f);
		}

		//ScalarをColorに変換
		public static Color ScalarToColor(Scalar scalar) 
		{
			float r = 0;
			if (0 < scalar.val.Length) r = (float)scalar.val[0]/255f;
			float g = 0;
			if (1 < scalar.val.Length) g = (float)scalar.val[1]/255f;
			float b = 0;
			if (2 < scalar.val.Length) b = (float)scalar.val[2]/255f;
			float a = 0;
			if (3 < scalar.val.Length) a = (float)scalar.val[3]/255f;
			return new Color(r, g, b, a);
		}

		//タップした位置の色を返す
		public static Color GetTapPointColor(Mat rgbaMat)
		{
			Color tapColor = new Color(0.031f, 0.326f, 0.852f, 1f);

			//タップ座標の取得
#if ((UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR)
			//Touch
			int touchCount = Input.touchCount;
			if (touchCount == 1)
			{
				Touch t = Input.GetTouch(0);
				if(t.phase == TouchPhase.Ended && !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(t.fingerId))
					storedTouchPoint = new Point (t.position.x, t.position.y);
			}
#else
			//Mouse
			if (Input.GetMouseButtonUp(0) && !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
				storedTouchPoint = new Point(Input.mousePosition.x, Input.mousePosition.y);
#endif

			//タップされていればタップされている場所を取得
			if(storedTouchPoint != null)
			{
				Point touchPoint = _convertScreenPoint(rgbaMat, storedTouchPoint);

				//タップされている場所から色情報を取得
				Scalar blobColorHsv = _onTouch(rgbaMat, touchPoint);
				if (blobColorHsv != null) tapColor = ScalarToColor(blobColorHsv);
			}

			return tapColor;
		}

		//タップ座標を取得
		private static Point _convertScreenPoint(Mat rgbaMat, Point screenPoint)
		{
			//台形補正を行いタップされた位置の座標を正確に取得する
			Vector2 tl = Camera.main.WorldToScreenPoint(new Vector3(-rgbaMat.width()/2,  rgbaMat.height()/2));
			Vector2 tr = Camera.main.WorldToScreenPoint(new Vector3( rgbaMat.width()/2,  rgbaMat.height()/2));
			Vector2 br = Camera.main.WorldToScreenPoint(new Vector3( rgbaMat.width()/2, -rgbaMat.height()/2));
			Vector2 bl = Camera.main.WorldToScreenPoint(new Vector3(-rgbaMat.width()/2, -rgbaMat.height()/2));

			Mat srcRectMat = new Mat(4, 1, CvType.CV_32FC2);
			Mat dstRectMat = new Mat(4, 1, CvType.CV_32FC2);

			srcRectMat.put(0, 0, tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y);
			dstRectMat.put(0, 0, 0.0, 0.0, rgbaMat.width(), 0.0, rgbaMat.width(), rgbaMat.height(), 0.0, rgbaMat.height());

			Mat perspectiveTransform = Imgproc.getPerspectiveTransform(srcRectMat, dstRectMat);

			MatOfPoint2f srcPointMat = new MatOfPoint2f(screenPoint);
			MatOfPoint2f dstPointMat = new MatOfPoint2f();

			Core.perspectiveTransform (srcPointMat, dstPointMat, perspectiveTransform);

			return dstPointMat.toArray()[0];
		}

		//タッチ座標からその場所の平均色を取得
		private static Scalar _onTouch(Mat rgbaMat, Point touchPoint)
		{
			int cols = rgbaMat.cols();
			int rows = rgbaMat.rows();
			int x = (int)touchPoint.x;
			int y = (int)touchPoint.y;
			if ((x < 0) || (y < 0) || (x > cols) || (y > rows)) return null;

			OpenCVForUnity.Rect touchedRect = new OpenCVForUnity.Rect();

			touchedRect.x = (x > 5) ? x - 5 : 0;
			touchedRect.y = (y > 5) ? y - 5 : 0;

			touchedRect.width = (x + 5 < cols) ? x + 5 - touchedRect.x : cols - touchedRect.x;
			touchedRect.height = (y + 5 < rows) ? y + 5 - touchedRect.y : rows - touchedRect.y;

			//タップ座標のみを切り抜く
			Mat touchedRegionRgba = rgbaMat.submat(touchedRect);

			Mat touchedRegionHsv = new Mat();
			Imgproc.cvtColor(touchedRegionRgba, touchedRegionHsv, Imgproc.COLOR_RGB2HSV_FULL);

			//タップされた位置の平均色を計算する
			Scalar blobColorHsv = Core.sumElems(touchedRegionHsv);
			int pointCount = touchedRect.width * touchedRect.height;
			for (int i = 0; i < blobColorHsv.val.Length; i++)
				blobColorHsv.val [i] /= pointCount;

			touchedRegionRgba.release();
			touchedRegionHsv.release();

			return blobColorHsv;
		}
	}
}

最終版のHGOrigin.csクラスです。(クラス名は仮のまま...)
手の識別を行い、その位置に描画を行います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	//手の認識を行う
	public static class HGOrigin
	{
		public static int depthThreashold = 9000; //検出の精度(0 ~ 30000)
		public static Color ContourRangeColor = Color.green;
		public static Color ArmRangeColor = Color.blue;
		public static Color HandRangeColor = Color.cyan;
		public static Color PalmRangeColor = Color.yellow;
		public static Color PalmCenterColor = Color.grey;
		public static Color FingerRangeColor = Color.red;

		//認識の開始
		public static void Cognition(Mat rgbaMat, Color handColor)
		{
			//指定色と同じ輪郭を取得する
			Mat mDilatedMask = new Mat();
			_makeColorMask(rgbaMat, handColor, mDilatedMask);

			//マスクの輪郭の頂点を取得する
			List<MatOfPoint> contours = new List<MatOfPoint> ();
			Imgproc.findContours(mDilatedMask, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

			//輪郭ごとの頂点を取得し、手を判別する
			foreach (MatOfPoint contour in contours)
				_contourToHandGesture(rgbaMat, contour);
		}

		/*=============================================*
		 * 画像から輪郭を取得するまで
		 *=============================================*/
		/// <summary>
		/// Makes the color mask.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="handColor">Hand color.</param>
		/// <param name="mDilatedMask">M dilated mask.</param>
		private static void _makeColorMask(Mat rgbaMat, Color handColor, Mat mDilatedMask) 
		{
			//色の範囲を指定する
			Scalar mLowerBound = new Scalar(0);
			Scalar mUpperBound = new Scalar(0);
			_getApproximateScalarFromColor(handColor, mLowerBound, mUpperBound);

			//ガウシアンピラミッドを利用して画像を周波数ごとに分解した(小さくした)HSV形式の画像を作成
			Mat mHsvMat = new Mat();
			_getGaussianPyramidHSVMat(rgbaMat, mHsvMat);

			//inRangeで色による探索を行い、mMaskに指定色だけが残った画像(マスク)を作成する
			Mat mMask = new Mat();
			Core.inRange (mHsvMat, mLowerBound, mUpperBound, mMask);

			//dilateで画像の膨張を行い、マスクのノイズ除去を行う
			Imgproc.dilate (mMask, mDilatedMask, new Mat ());
		}

		/// <summary>
		/// Gets the color of the approximate scalar from.
		/// </summary>
		/// <param name="handColor">Hand color.</param>
		/// <param name="mLowerBound">M lower bound.</param>
		/// <param name="mUpperBound">M upper bound.</param>
		private static void _getApproximateScalarFromColor(Color handColor, Scalar mLowerBound, Scalar mUpperBound) 
		{
			//色の範囲を指定する
			Scalar mColorRadius = new Scalar(25, 50, 50, 0);

			Scalar hsvColor = HGColorSpuiter.ColorToScalar(handColor);
			double minH = (hsvColor.val [0] >= mColorRadius.val [0]) ? hsvColor.val [0] - mColorRadius.val [0] : 0;
			double maxH = (hsvColor.val [0] + mColorRadius.val [0] <= 255) ? hsvColor.val [0] + mColorRadius.val [0] : 255;

			mLowerBound.val [0] = minH;
			mUpperBound.val [0] = maxH;

			mLowerBound.val [1] = hsvColor.val [1] - mColorRadius.val [1];
			mUpperBound.val [1] = hsvColor.val [1] + mColorRadius.val [1];

			mLowerBound.val [2] = hsvColor.val [2] - mColorRadius.val [2];
			mUpperBound.val [2] = hsvColor.val [2] + mColorRadius.val [2];

			mLowerBound.val [3] = 0;
			mUpperBound.val [3] = 255;
		}

		/// <summary>
		/// Gets the gaussian pyramid HSV mat.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="mHsvMat">M hsv mat.</param>
		private static void _getGaussianPyramidHSVMat(Mat rgbaMat, Mat mHsvMat) 
		{
			Mat mPyrDownMat = new Mat();
			Imgproc.pyrDown (rgbaMat, mPyrDownMat);
			Imgproc.pyrDown (mPyrDownMat, mPyrDownMat);
			Imgproc.cvtColor (mPyrDownMat, mHsvMat, Imgproc.COLOR_RGB2HSV_FULL);
		}

		/*=============================================*
		 * 輪郭ごとの頂点から手を判別するまで
		 *=============================================*/
		/// <summary>
		/// Contours to hand gesture.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="contour">Contour.</param>
		private static void _contourToHandGesture(Mat rgbaMat, MatOfPoint contour) 
		{
			try 
			{
				//頂点を調査する準備をする
				_pointOfVertices(rgbaMat, contour);

				//基準輪郭のサイズの取得と描画(長方形)
				OpenCVForUnity.Rect boundRect = Imgproc.boundingRect(new MatOfPoint(contour.toArray()));
				Imgproc.rectangle(rgbaMat, boundRect.tl(), boundRect.br(), HGColorSpuiter.ColorToScalar(ContourRangeColor), 2, 8, 0);

				/*=============================================*
				* 腕まで含んだ手の大きさを取得する
				**=============================================*/
				//腕まで含んだ手の大きさを識別する
				MatOfInt hull = new MatOfInt();
				Imgproc.convexHull(new MatOfPoint(contour.toArray()), hull);

				//腕まで含んだ手の範囲を取得
				List<Point> armPointList = new List<Point>();
				for (int j = 0; j < hull.toList().Count; j++)
				{
					Point armPoint = contour.toList()[hull.toList()[j]];
					bool addFlag = true;
					foreach (Point point in armPointList.ToArray()) 
					{
						//輪郭の1/10より近い頂点は誤差としてまとめる
						double distance = Mathf.Sqrt((float)((armPoint.x-point.x)*(armPoint.x-point.x)+(armPoint.y-point.y)*(armPoint.y-point.y)));
						if (distance <= Mathf.Min((float)boundRect.width, (float)boundRect.height)/10) 
						{
							addFlag = false;
							break;
						}
					}
					if (addFlag) armPointList.Add(armPoint);	
				}

				MatOfPoint armMatOfPoint = new MatOfPoint();
				armMatOfPoint.fromList(armPointList);
				List<MatOfPoint> armPoints = new List<MatOfPoint>();
				armPoints.Add(armMatOfPoint);

				//腕まで含んだ手の範囲を描画
				Imgproc.drawContours(rgbaMat, armPoints, -1, HGColorSpuiter.ColorToScalar(ArmRangeColor), 3);

				//腕まで含んだ手が三角形の場合はそれ以上の識別が難しい
				if (hull.toArray().Length < 3) return;

				/*=============================================*
				* 掌の大きさを取得する
				**=============================================*/
				//凸面の頂点から凹面の点のみを取得し、掌の範囲を取得する
				MatOfInt4 convexDefect = new MatOfInt4();
				Imgproc.convexityDefects(new MatOfPoint(contour.toArray()), hull, convexDefect);

				//凹面の点をフィルタリングして取得
				List<Point> palmPointList = new List<Point>();
				for (int j = 0; j < convexDefect.toList().Count; j = j+4) 
				{
					Point farPoint = contour.toList()[convexDefect.toList()[j+2]];
					int depth = convexDefect.toList()[j+3];
					if (depth > depthThreashold && farPoint.y < boundRect.br().y-boundRect.tl().y)
						palmPointList.Add(contour.toList()[convexDefect.toList()[j+2]]);
				}

				MatOfPoint palmMatOfPoint = new MatOfPoint();
				palmMatOfPoint.fromList(palmPointList);
				List<MatOfPoint> palmPoints = new List<MatOfPoint>();
				palmPoints.Add(palmMatOfPoint);

				//掌の範囲を描画
				Imgproc.drawContours(rgbaMat, palmPoints, -1, HGColorSpuiter.ColorToScalar(PalmRangeColor), 3);

				/*=============================================*
				* 掌+指先の大きさを取得する
				**=============================================*/
				//掌の位置を元に手首を除いた範囲を取得する
				List<Point> handPointList = new List<Point>();
				handPointList.AddRange(armPointList.ToArray());
				handPointList.Reverse();
				handPointList.RemoveAt(0);
				handPointList.Insert(0, palmPointList.ToArray()[0]);
				handPointList.RemoveAt(handPointList.Count-1);
				handPointList.Insert(handPointList.Count, palmPointList.ToArray()[palmPointList.Count-1]);

				MatOfPoint handMatOfPoint = new MatOfPoint();
				handMatOfPoint.fromList(handPointList);
				List<MatOfPoint> handPoints = new List<MatOfPoint>();
				handPoints.Add(handMatOfPoint);

				Imgproc.drawContours(rgbaMat, handPoints, -1, HGColorSpuiter.ColorToScalar(HandRangeColor), 3);

				/*=============================================*
				* 指先の位置を取得する
				**=============================================*/
				//掌の各頂点の中心を求める
				List<Point> palmCenterPoints = new List<Point>();
				for (int i = 0; i < palmPointList.Count; i++)
				{
					Point palmPoint = palmPointList.ToArray()[i];
					Point palmPointNext = new Point();
					if (i+1 < palmPointList.Count) 
						palmPointNext = palmPointList.ToArray()[i+1];
					else palmPointNext = palmPointList.ToArray()[0];
	
					Point palmCenterPoint = new Point((palmPoint.x+palmPointNext.x)/2, (palmPoint.y+palmPointNext.y)/2);
					palmCenterPoints.Add(palmCenterPoint);
				}
	
				//掌の頂点から最も近い手の頂点を求める
				for (int i = 0; i < palmCenterPoints.Count && i+1 < handPointList.Count && i < 5; i++) 
				{
					Point palmPoint = palmCenterPoints.ToArray()[i];


					List<Point> fingerList = new List<Point>();
					fingerList.Add(palmPoint);
					fingerList.Add(handPointList.ToArray()[i+1]);
	
					MatOfPoint fingerPoint = new MatOfPoint();
					fingerPoint.fromList(fingerList);
	
					List<MatOfPoint> fingerPoints = new List<MatOfPoint>();
					fingerPoints.Add(fingerPoint);
	
					Imgproc.drawContours(rgbaMat, fingerPoints, -1, HGColorSpuiter.ColorToScalar(FingerRangeColor), 3);
				}

//				Imgproc.putText(rgbaMat, "", new Point(2, rgbaMat.rows()-30), Core.FONT_HERSHEY_SIMPLEX, 1.0, HGColorSpuiter.ColorToScalar(Color.black), 2, Imgproc.LINE_AA, false);
			}
			catch (System.Exception e) 
			{
				Debug.Log(e.Message);
			}
		}

		/// <summary>
		/// Points the of vertices.
		/// </summary>
		/// <param name="contour">Contour.</param>
		private static void _pointOfVertices(Mat rgbaMat, MatOfPoint contour) 
		{
			//multiplyでガウシアンピラミッドで分解されたサイズを掛け算で実画像サイズに戻す
			Core.multiply(contour, new Scalar(4, 4), contour);

			//輪郭の頂点がまだらにあるので識別しやすいようにポリゴン近似でサンプリングする。
			MatOfPoint2f pointMat = new MatOfPoint2f();
			Imgproc.approxPolyDP(new MatOfPoint2f(contour.toArray()), pointMat, 3, true);
			contour = new MatOfPoint(pointMat.toArray());
		}
	}
}

使用するときはこんな感じ
MatRetouchでタップ位置の色を取得して手の位置に合わせて描画します。
タップ位置の色はデフォルトが肌色、タップ後はタップした位置の色になります。

protected override void MatRetouch(Mat _rgbaMat)
{
	//変換されたMatを加工する
	base.MatRetouch(_rgbaMat);

	//タップ位置の色を取得
	Color color = HGColorSpuiter.GetTapPointColor(_rgbaMat);

	//手の位置に合わせて描画
	HGOrigin.Cognition(_rgbaMat, color);
}

最終的に認識できたもの

最終的に上記の処理で手の画像から識別することが可能になりました。
緑 :肌色の輪郭の範囲
青 :肌色の範囲
水色:手の範囲
黄 :掌の範囲
赤 :指
f:id:nanokanato:20171219104740j:plain:w300

ただ、上記は認識しやすい綺麗な手の画像で、下記のような画像だとまだ認識の精度が低いことがわかります。
f:id:nanokanato:20171219105312p:plain:w300f:id:nanokanato:20171219105315p:plain:w300

いろいろ試行錯誤した手順は以下にまとめました。