UnityでOpenCVを使いたい場合には「OpenCV for Unity」と「OpenCV plus Unity」の2種類のアセットがあります。OpenCV for Unityは有償($104)でメンテナンスもこまめに行われている一方、APIが独特でpythonやC++のOpenCVに慣れている人には少し使いにくいイメージです。
一方、OpenCV plus Unityは オープンソースのOpenCVSharpをUnity用にカスタムした無償のアセットです。APIの形式も他言語のものと似ていて比較的使いやすいため、この記事ではOpenCV plus Unityを使ったサンプルを紹介します。
ここで紹介するOpenCVのサンプルは以下のとおりです。
- 下準備
- 画像の読み込み
- グレースケール化
- 画像を2値化する
- 画像から輪郭検出する
- 画像を射影変換する
- 座標の射影変換
- ガウシアンブラーでぼかす
- 画像の膨張処理
- Sovelフィルタを使う
- ハイパスフィルタを使う
- 論理演算を使ってマスクする
- 画像のレイヤを分ける
- 画像を左右反転する
- 図形を描画する
- Arucoマーカーを作成する
- Arucoマーカーを検出する
- OpenCVでピクセルにアクセスする
- 顔検出をする
- ROIを指定して画像をトリミングする
- 関連書籍
下準備
Asset StoreからUnity plus OpenCVをダウンロードして、プロジェクトにimportします。
インポート直後はエラーが表示されます。メニューバーからEdit→Project Settingsを選択して、「Allow unsafe code」のチェックボックスにチェックを入れて下さい。
OpenCVで処理した画像を表示するため、UIのRawImageを使います。ヒエラルキーウインドウで「+」→「UI」→「Raw Image」を選択して下さい。表示する画像を画面サイズに合わせるため、RawImageにCameraScalerスクリプトをアタッチしておきます。
CameraScalerスクリプトはAssets/OpenCV+Unity/Demo/Scriptsの中にあります。
これで下準備は完了です。次に新規でスクリプトを作成して、OpenCVで画像を表示していきましょう。
画像の読み込み
まずは画像をTexture2D型として読み込み、それを一旦OpenCVのMatに変換して、最後にもう一度Texture2D型に戻して表示するサンプルです。
次のTestスクリプトを作成して、上で作成したRawImageのオブジェクトにアタッチして下さい。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using OpenCvSharp.Demo; using OpenCvSharp; using OpenCvSharp.Aruco; public class Test : MonoBehaviour { public Texture2D texture; void Start() { // 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture; } }
スクリプトをRaw Imageにアタッチできたら、インスペクタからtexture変数に表示したい画像を指定します。このときOpenCVが画像のピクセルにアクセスできるように事前に設定する必要があります。表示したい画像を選択してインスペクタから「Read/Write Enable」にチェックを入れて下さい。
表示したい画像をインスペクタからTestスクリプトのtexture変数にセットしたら実行してみて下さい。ゲームビューに画像が表示されればOKです。
グレースケール化
カラー画像を読み込んで、グレースケール化するサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // 画像書き出し Texture2D outTexture = new Texture2D(gray.Width, gray.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(gray, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
画像をグレースケールに変換するにはCvtColorメソッドを使います。第三引数に指定する値によって変換する形式を選択できます。よく使う値は次のとおりです。
値 | 意味 |
---|---|
RGB2GRAY | RGB画像をグレースケールに変換 |
RGB2HSV | RGBをHSV形式に変換 |
RGBA2RGB | 透明チャネルを削除する |
RGB2GBR | RとBチャネルを入れ替える |
実行結果は次のようになります。
画像を2値化する
カラー画像を読み込んで、しきい値よりも明るいピクセルは白、暗いピクセルは黒にする2値化のサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // 2値化 Mat bin = new Mat(); Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(bin, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
画像を2値化するにはThresholdメソッドを使います。第3引数にしきい値、第5引数には変換形式を設定します。第5引数でよく使う値には次のものがあります。
値 | 意味 |
---|---|
Binary | 白黒に変換 |
Binary | 白黒反転して変換 |
Otsu | 大津の2値化アルゴリズムで変換 |
実行結果は次のようになります。
画像から輪郭検出する
2値化した画像から輪郭を抽出するサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // 2値化 Mat bin = new Mat(); Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary); // 輪郭抽出 Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(bin, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxNone, null); foreach (Point[] contour in contours) { // 面積 double area = Cv2.ContourArea(contour); // 重心 Moments m = Cv2.Moments(contour); int cx = (int)(m.M10 / m.M00); int cy = (int)(m.M01 / m.M00); Cv2.DrawContours(mat, new Point[][] { contour }, 0, new Scalar(0, 0, 255), 8); } // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
輪郭抽出をするには、画像を2値化してからFindContoursメソッドを使用します。第5引数にExternalを指定した場合は最外周の輪郭のみ検出します。一方、Treeを指定した場合は入れ子になった輪郭も抽出します。
値 | 意味 |
---|---|
External | 最外周の輪郭のみを検出 |
Tree | 内側の輪郭も検出 |
実行結果は次のようになります。
画像を射影変換する
画像を射影変換するサンプルです。射影変換とは四角形の頂点をつまんで好きな形に変形するイメージです。例えば撮影した画像に含まれるQRコード部分だけを取り出して、正方形に変換する場合などに使用します。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); Vector2[] srcData = new Vector2[] { new Vector2(0, 0), new Vector2(mat.Width, 0), new Vector2(mat.Width, mat.Height), new Vector2(0, mat.Height) }; Vector2[] dstData = new Vector2[] { new Vector2(100, 50), new Vector2(400, 200), new Vector2(440, 350), new Vector2(0, 500) }; Mat srcMat = new Mat(4, 1, MatType.CV_32FC2, srcData); Mat dstMat = new Mat(4, 1, MatType.CV_32FC2, dstData); Mat T = Cv2.GetPerspectiveTransform(srcMat, dstMat); Mat dst = new Mat(); Cv2.WarpPerspective(mat, dst, T, new Size(mat.Width, mat.Height)); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(dst, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
射影変換するにはGetPerspectiveTransformメソッドを使い、入力座標を出力座標に射影するための行列を求めます。次にWarpPerspectiveメソッドに計算した射影行列を指定して画像を変換します。
実行結果は次のようになります。
座標の射影変換
画像の射影変換では出力結果は射影変換した後の画像でした。座標自体を変換したい場合にはこちらのスクリプトを使用します。
Vector2[] srcData = new Vector2[] { new Vector2(100, 100), new Vector2(300, 120), new Vector2(300, 280), new Vector2(120, 300), }; Vector2[] dstData = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1), }; Mat srcMat = new Mat(4, 1, MatType.CV_32FC2, srcData); Mat dstMat = new Mat(4, 1, MatType.CV_32FC2, dstData); // 射影変換行列 Mat T = Cv2.GetPerspectiveTransform(srcMat, dstMat); Point2f[] srcPos = { new Point2f(300, 280) }; Point2f[] dstPos = Cv2.PerspectiveTransform(srcPos, T); Debug.Log(dstPos[0].X + "," + dstPos[0].Y);
GetPerspectiveTransformメソッドを使って射影変換行列を求めるところまでは同じです。座標自体を変換するにはPerspectiveTransformメソッドを使います。PerspectiveTransformには入力座標の配列と射影変換行列を指定し、戻り値として出力座標の配列を受け取ります。
ガウシアンブラーでぼかす
ガウシアンブラーを使って画像をぼかすサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // ぼかし Mat blur = new Mat(); Cv2.GaussianBlur(mat, blur, new Size(11, 11), 0); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(blur, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVでガウシアンブラーをかけるにはGaussianBlurメソッドを使います。第3引数に渡すフィルタのサイズを大きくすればするほど、よりボケた画像になります。
実行結果は次のようになります。
画像の膨張処理
白色のピクセルを周囲8方向に膨張させる処理を行うサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // 2値化 Mat bin = new Mat(); Cv2.Threshold(gray, bin, 127, 255, ThresholdTypes.Binary); // 膨張処理 Mat dst = new Mat(); Mat k = new Mat(); Cv2.Dilate(bin, dst, k, null, 3); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(dst, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
膨張処理を行うにはDilateメソッドを使用します。第5引数には何回膨張処理を繰り返すかを指定します。この値が大きければ大きいほど白色ピクセルの膨張量が大きくなります。また、逆に白色のピクセルを縮小させたい場合はCv2.Erodeメソッドを使います。引数の指定方法はDilateとほぼ同じです。
実行結果は次のようになります。
Sovelフィルタを使う
縦方向や横方向のエッジを検出できるSovelフィルタを使うサンプルです。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // Sovelフィルタ Mat sobel = new Mat(); Cv2.Sobel(gray, sobel, MatType.CV_8UC1, 0, 1); // 書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(sobel, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVでSobelフィルタを使うにはSobelメソッドを使います。第3引数には入力画像の形式を指定します。第4と第5引数で検出するエッジの方向を指定します。(1, 0)であれば縦方向、(0, 1)であれば横方向のエッジを検出します。
下図(左)は縦方向のエッジを検出、下図(右)は横方向のエッジを検出したものです。
ハイパスフィルタを使う
ハイパスフィルタを使って画像中から輝度値の変化が大きい領域のみを抽出するサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // ハイパスフィルタのカーネル double[] data = { -1, -1, -1, -1, 8, -1, -1, -1, -1 }; Mat kernel = new Mat(3, 3, MatType.CV_64FC1, data); // ハイパス Mat dst = new Mat(); Cv2.Filter2D(gray, dst, MatType.CV_8UC1, kernel); // 画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(dst, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVにはハイパスフィルタに使えるメソッドは用意されていません。そこでハイパスフィルタのカーネルを定義して、Filter2Dメソッドを使って畳み込み演算を行います。カーネルの値はマイナスの値も含まれるので、MatTypeにはCV_64FC1を指定していることに注意して下さい。
実行結果は次のようになります。
論理演算を使ってマスクする
OpenCVで論理演算をするサンプルです。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // 黒色に白丸のマスク(カラー画像の場合はmaskも3chぶん用意する) Mat mask = new Mat(mat.Width, mat.Height, MatType.CV_8UC3, new Scalar(0,0,0)); Cv2.Circle(mask, new Point(mat.Width / 2, mat.Height / 2), 200, new Scalar(255,255,255), -1); // 論理演算 Mat dst = new Mat(); Cv2.BitwiseAnd(mat, mask, dst); // 書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(dst, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVの論理演算にはBitwiseAnd、BitwiseOr、BitwiseNot、BitwiseXOrが用意されています。1chのグレースケール画像に対して論理演算をする場合はマスクも1chで大丈夫ですが、3chのカラー画像に対して論理演算をする場合はマスク画像も3chぶん用意する必要があります。カラー画像に対して1chのマスクを使うとRレイヤのみマスクされてしまいます。
実行結果は次のようになります。
画像のレイヤを分ける
複数チャネルを持つ画像をレイヤごとに分割するサンプルです。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // 各レイヤを取り出す Mat[] rgba = Cv2.Split(mat); // Rレイヤを書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(rgba[2], outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
複数レイヤを分割するにはSplitメソッドを使用します。戻り値はMatの配列になります。また、逆に各レイヤを1つの画像に合成する場合はMergeメソッドを使います。
実行結果は次のようになります。
画像を左右反転する
OpenCVで読み込んだ画像を左右反転するサンプルです。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // 左右反転 Mat dst = new Mat(); Cv2.Flip(mat, dst, FlipMode.Y); //画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(dst, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
画像を左右反転するにはFlipメソッドを使います。第3引数で縦方向に反転するか、横方向に反転するかを選ぶことができます。
実行結果は次のようになります。
図形を描画する
OpenCVで円や四角形を描画するサンプルです。
Mat mat = new Mat(512, 512, MatType.CV_8UC3, new Scalar(30, 30, 30)); // 円 Cv2.Circle(mat, new Point(100, 100), 50, new Scalar(255, 0, 0), 3, LineTypes.AntiAlias); // 円塗りつぶし Cv2.Circle(mat, new Point(300, 100), 50, new Scalar(255, 0, 0), -1, LineTypes.AntiAlias); // 四角形 Cv2.Rectangle(mat, new OpenCvSharp.Rect(50, 180, 300, 100), new Scalar(0, 0, 255), 3, LineTypes.AntiAlias); //画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVで円を描画するにはCircleメソッドを使用します。第2引数に円の中心座標、第3引数に半径、第5引数に線の太さを指定します。線の太さをマイナスの値にすると図形の内部が塗りつぶされます。また、第6引数のLineTypesにAntiAliasを指定することで、ギザギザのないなめらかな図形を描画できます。
OpenCVで四角形を描画するにはRectangleメソッドを使います。第2引数のRectメソッドで左上座標と幅、高さを指定します。塗りつぶしやLineTypesはCircleメソッドと同様です。
実行結果は次のようになります。
Arucoマーカーを作成する
OpenCVにはARマーカーを検出するためのArucoライブラリが付属しています(実際にはOpenCV Contribモジュールに含まれているものです)。次のスクリプトはArucoマーカーを作成するためのサンプルです。
Dictionary dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict6X6_250); int size = 150; Mat mat = new Mat(); CvAruco.DrawMarker(dictionary, 5, size, mat); Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
作成するARマーカーはGetPredefinedDictionaryの引数で指定します。ここではDict6X6_250を指定しているので6セルx6セルのサイズのマーカー(外側の黒枠も入れると8セルx8セル)を250個まで生成できます。実際に生成するにはDrawMarkerメソッドを使用します。このDrawMarkerメソッドの第2引数で250個のうち何番目のマーカーを生成するかを指定します。
作成したマーカーは次のようになります。
Arucoマーカーを検出する
上で生成したArucoマーカーを撮影した画像中から検出するサンプルです。
DetectorParameters detectorParameters = DetectorParameters.Create(); Dictionary dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict6X6_250); Point2f[][] corners; int[] ids; Point2f[][] rejectedImgPoints; Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール化 Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // Arucoマーカーの検出 CvAruco.DetectMarkers(gray, dictionary, out corners, out ids, detectorParameters, out rejectedImgPoints); CvAruco.DrawDetectedMarkers(mat, corners, ids); //画像書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
Arucoマーカーを検出するにはDetectMarkersメソッドを使用します。出力結果として、マーカーの四隅の座標とマーカーのIDを受け取ることができます。この情報を使ってマーカーの姿勢を推定することもできます。姿勢推定の手法はコチラの記事が詳しいです。
OpenCVでピクセルにアクセスする
ピクセルにアクセスして画像の左上だけ白色に塗りつぶすサンプルです。
Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); unsafe { byte* data = mat.DataPointer; int width = mat.Width; int height = mat.Height; int channel = mat.Channels(); for (int y = 0; y < height/4; y++) { for (int x = 0; x < width/4 ; x++) { int idx = (x + y * width) * channel; data[idx+0] = 255; data[idx+1] = 255; data[idx+2] = 255; } } } // 書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVのMatクラスにはAtメソッドが用意されていて、これでもピクセル(画素)にアクセスすることはできます。ただ、このメソッドは呼び出しに時間がかかるため、全ピクセルを処理するには向いていません。高速にピクセルにアクセスするためには、DataPointerで取得できるポインタを使います。ポインタを使う箇所はunsafeブロックで囲む必要があります。
ピクセルの座標を(x,y)、画像の幅と高さを(width, height)、チャネル数をchannelとすると、そのピクセルのインデックスは (x + y * width) * channelで調べられます。
実行結果は次のとおりです。
顔検出をする
画像中から顔を検出して、その位置を表示するサンプルです。
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // グレースケール Mat gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY); // カスケード分類器の準備 CascadeClassifier haarCascade = new CascadeClassifier("Assets/OpenCV+Unity/Demo/Face_Detector/haarcascade_frontalface_default.xml"); // 顔検出 OpenCvSharp.Rect[] faces = haarCascade.DetectMultiScale(gray); // 顔の位置を描画 foreach (OpenCvSharp.Rect face in faces) { Cv2.Rectangle(mat, face, new Scalar(0, 0, 255), 3); } // 書き出し Texture2D outTexture = new Texture2D(mat.Width, mat.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(mat, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
OpenCVで顔検出をするにはカスケード分類器を使います。カスケード分類器のパラメータはOpenCV plus Unityの/OpenCV+Unity/Demo/Face_Detector/に含まれているので、CascadeClassifierのコンストラクタでパスを指定して読み込みます。顔検出はDetectMultiScaleメソッドを使います。検出した顔の領域は、DetectMultiScaleメソッドの戻り値としてRect型の配列で受け取ります。
実行結果は次のようになります。
ROIを指定して画像をトリミングする
画像から特定の注目領域(ROI)だけを切り出す方法です
// 画像読み込み Mat mat = OpenCvSharp.Unity.TextureToMat(this.texture); // ROIの領域だけトリミングする // 左上座標(0,0)から縦横200pxぶんを切り出す Rect rect = new Rect(0, 0, 200, 200) Mat roi = new Mat(mat, rect); // 書き出し Texture2D outTexture = new Texture2D(roi.Width, roi.Height, TextureFormat.ARGB32, false); OpenCvSharp.Unity.MatToTexture(roi, outTexture); // 表示 GetComponent<RawImage>().texture = outTexture;
ROIの領域をトリミングするには、Rect型で領域を指定します。Rectには左上の座標と横幅・高さを指定します。