#はじめに
OpenCVとUnity組み合わせて遊ぶと楽しいぞという気持ちをツイッターで頻繁に目にしていたある日、OpenCV plus Unityが無料になっていることに気づいてダウンロードしてみました。OpenCV#、OpenCVfor、OpenCVplusとOpenCV系のアセット結構多いし価格帯も違ってて意味わからんので誰かまとめてください…学生に全部買うお金はないです。
↓選べる松竹梅みたくなってるけど何が違うのかは正直謎です。ジェネリック医薬品的なやつなのかな…?
OpenCVplusに関してはあんまり知見がないのでとりあえず画像処理100本ノックの最初の方の問題でもやりながら使い勝手を確認してみます。
#セットアップ編
##パッケージのインポート
まず、OpenCV plus Unityをダウンロードしてプロジェクトにインポートします。
##UnsafeC#使用の許可
パッケージをUnityに入れると「Unsafeコードは使えないよ!!!」的なエラーが出てくると思います。C#でもポインタをいじいじできるようにUnsafeなコードを許可してあげましょう。最近はC#でもポインタを使おう界隈が強いですね。(多分ECS、Jobシステムとかを最適に活用するのにポインタの方がいいみたいなアレだと思います。知らんけど)
File>BuildSettings>PlayerSettingsを開いてOtherSettingsの"Allow 'unsafe' Code"の部分にチェックを入れます。
コレでエラーも消えるハズです。
##ドキュメントを探す
OpenCV plus Unityで検索しても公式のリファレンス以外で良さげな知見が少なく、非常に困りました。
OpenCV plus Unity自体がOpenCVをC#で使えるようにしたOpenCVSharpを踏まえて作っているみたいなのでこちらのワードで検索すると知見が結構見つかります。参考にしてください。
#画像処理100本ノック(1~10)
早速やっていきましょう。詳しい情報を知りたいときは画像処理100本ノックを見ましょう。いい問題集を作って戴きありがとうございます。
下に書いてあるプログラムはこちらに公開しておきます。
##下準備
まずは入力と出力の方法を決めましょう。
###入力
インスペクタからTexture2Dを取得してそれをOpenCVのMatに変換します。
public Texture2D texture;
Mat mat = Unity.TextureToMat(this.texture);
###出力
MatからTexture2Dに変換してUGUIのRawImageのtextureにブチ込みます。
Texture2D changedTex = Unity.MatToTexture(changedMat);
GetComponent<RawImage>().texture = changedTex;
デフォルトでRGBの各要素を入れ替える関数があるのでそれを利用しましょう。Cv2.
Cv2.CvtColor(変換対象,変換後の代入先,変換方法)
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
public class q1 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Mat changedMat = new Mat();
Cv2.CvtColor(mat, changedMat,ColorConversionCodes.BGR2RGB );
Texture2D changedTex = Unity.MatToTexture(changedMat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
Q01と同じ手法でRGBをグレースケールに変換する関数があるのですが、それに頼ってばっかりなのもナンセンスなのでちゃんと1ピクセルごとに処理してみます。各ピクセルのRGB値を取得してグレースケールの変換ルールにしたがって変換します。グレースケールなのでR=G=Bになります。処理する際にvector3やらvec3fやらを使うとUnityが爆発するので注意(多分メモリリークか何か)。vec3bを使ってbytes型で処理するとうまくいきます。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q2 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
for(int yi = 0; yi < mat.Height; yi++)
{
for(int xi = 0; xi < mat.Width; xi++)
{
Vec3b v = mat.At<Vec3b>(yi,xi);
float gr = 0.2126f * v[2] + 0.7152f * v[1] + 0.0722f * v[0];
v[0] = (byte)gr;
v[1] = (byte)gr;
v[2] = (byte)gr;
mat.Set<Vec3b>(yi, xi, v);
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
注目して欲しいのはこの部分です。
Vec3b v = mat.At<Vec3b>(yi,xi);
float gr = 0.2126f * v[2] + 0.7152f * v[1] + 0.0722f * v[0];
v[0] = (byte)gr;
v[1] = (byte)gr;
v[2] = (byte)gr;
mat.Set<Vec3b>(yi, xi, v);
vec3b v = mat.At<Vec3b>(yi,xi)
で該当ピクセルのBGR(この順番なことに注意!)をbytes型3次元ベクトルで取得しています。
mat.set<Vec3b>(yi,xi,v)
で該当ピクセルのBGRをbytes型3次元ベクトルvと設定することができます。代入時にbytesを処理することを忘れないようにしましょう。
RGBをグレースケールで取得した後に閾値より大きいか小さいかで黒にするか白にするか決めます。Q2ができれば特に問題はないはず。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q3 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
for (int yi = 0; yi < mat.Height; yi++)
{
for (int xi = 0; xi < mat.Width; xi++)
{
Vec3b v = mat.At<Vec3b>(yi, xi);
Debug.Log(v[0]);
float gr = 0.2126f * v[2] + 0.7152f * v[1] + 0.0722f * v[0];
if(gr < 128)
{
gr = 0;
}
else
{
gr = 255;
}
v[0] = (byte)gr;
v[1] = (byte)gr;
v[2] = (byte)gr;
mat.Set<Vec3b>(yi, xi, v);
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
##Q4.大津の二値化
Q03では閾値が一定に定められていましたが、本問題では閾値が画像の明暗の度合いからいい感じに定められます。つまり暗めの画像だからって真っ黒になったり白めだからって真っ白になったりしません。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
using System.Linq;
public class q4 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
float[] results = new float[256];
float[,] grs = new float[mat.Height,mat.Width];
for(int yi = 0; yi < mat.Height; yi++)
{
for(int xi = 0; xi < mat.Width; xi++)
{
Vec3b v = mat.At<Vec3b>(yi, xi);
float gr = 0.2126f * v[2] + 0.7152f * v[1] + 0.0722f * v[0];
grs[yi, xi] = gr;
}
}
for(int thi = 1; thi < 255; thi++)
{
int w0 = 0;
int w1 = 0;
float M0 = 0;
float M1 = 0;
foreach(float gr in grs)
{
if(gr < thi)
{
w0++;
M0 += gr;
}
else
{
w1++;
M1 += gr;
}
}
Debug.Log(w0 + w1);
float tmp0 = w0 == 0 ? 0 : M0 / w0;
float tmp1 = w1 == 0 ? 0 : M1 / w1;
results[thi] = ((float)w0 / (mat.Height * mat.Width)) * ((float)w1 / (mat.Height * mat.Width)) * Mathf.Pow(tmp0 - tmp1 , 2);
}
int z = 0;
for(int i = 1; i < 255; i++)
{
if (results[i] > results[z]) z = i;
}
for(int yi = 0; yi < mat.Height; yi++)
{
for(int xi = 0; xi < mat.Width; xi++)
{
if(grs[yi,xi] < z)
{
Vec3b v = new Vec3b();
v[0] = (byte)0;v[1] = (byte)0;v[2] = (byte)0;
mat.Set<Vec3b>(yi, xi, v);
}
else
{
Vec3b v = new Vec3b();
v[0] = (byte)255; v[1] = (byte)255; v[2] = (byte)255;
mat.Set<Vec3b>(yi, xi, v);
}
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
##Q5.HSV変換
RBGをHue,Saturation,Valueに変換してHueを変更してRGBに戻すことをします。感覚としてはデカルト座標→極座標→デカルト座標って感じのノリな気がします。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
public class q5 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Mat changedMat = new Mat();
Mat changedMat1 = new Mat();
Cv2.CvtColor(mat, changedMat, ColorConversionCodes.BGR2HSV);
for(int yi = 0; yi < mat.Height; yi++)
{
for(int xi = 0; xi < mat.Width; xi++)
{
Vec3b v = changedMat.At<Vec3b>(yi, xi);
Debug.Log(v[0]);
v[0] = (byte)((v[0] - 180) % 360);
changedMat.Set<Vec3b>(yi, xi, v);
}
}
Cv2.CvtColor(changedMat,changedMat1, ColorConversionCodes.HSV2BGR);
Texture2D changedTex = Unity.MatToTexture(changedMat1);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
色の解像度を変更します。RGB各要素について一定の区間内の数値をある数値を一つの数値にします。
今回はRGBそれぞれ256の数値を取れていたのを、4種類の数値で表すように変更する処理を施しています。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q6 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
for (int yi = 0; yi < mat.Height; yi++)
{
for (int xi = 0; xi < mat.Width; xi++)
{
Vec3b v = mat.At<Vec3b>(yi, xi);
v[0] = (byte)(ReduceColor(v[0]));
v[1] = (byte)(ReduceColor(v[1]));
v[2] = (byte)(ReduceColor(v[2]));
mat.Set<Vec3b>(yi, xi, v);
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
public float ReduceColor (float val)
{
if(val < 63)
{
return 32;
}else if(val <127)
{
return 96;
}else if(val < 191)
{
return 160;
}else if(val < 255)
{
return 224;
}
return -1;
}
// Update is called once per frame
void Update()
{
}
}
}
ピクセルをある程度のチャンクに固めてチャンク内の色の平均値で塗りつぶします。ドット絵っぽくなります。(ドット絵ではない(ドット絵警察))
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q7 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
for(int yi = 0; yi < 16; yi++)
{
for(int xi = 0; xi < 16; xi++)
{
Vector3 sum = new Vector3();
for(int yj = 0; yj < 8; yj++)
{
for(int xj = 0; xj < 8; xj++)
{
Vec3b v = mat.At<Vec3b>(yi * 8 + yj,xi * 8 + xj);
sum[0] += v[0];
sum[1] += v[1];
sum[2] += v[2];
}
}
Vec3b ave = new Vec3b();
ave[0] = (byte)(sum[0] / 64);
ave[1] = (byte)(sum[1] / 64);
ave[2] = (byte)(sum[2] / 64);
for (int yj = 0; yj < 8; yj++)
{
for (int xj = 0; xj < 8; xj++)
{
mat.Set<Vec3b>(yi * 8 + yj, xi * 8 + xj, ave);
}
}
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
塗りつぶす色をチャンク内平均じゃなくてチャンク内の最高値を用いるプーリングです。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q8 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
for (int yi = 0; yi < 16; yi++)
{
for (int xi = 0; xi < 16; xi++)
{
Vec3b max = new Vec3b();
for (int yj = 0; yj < 8; yj++)
{
for (int xj = 0; xj < 8; xj++)
{
Vec3b v = mat.At<Vec3b>(yi * 8 + yj, xi * 8 + xj);
if (max[0] < v[0]) max[0] = v[0];
if (max[1] < v[1]) max[1] = v[1];
if (max[2] < v[2]) max[2] = v[2];
}
}
for (int yj = 0; yj < 8; yj++)
{
for (int xj = 0; xj < 8; xj++)
{
mat.Set<Vec3b>(yi * 8 + yj, xi * 8 + xj, max);
}
}
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
##Q9.ガウシアンフィルタ
画像のノイズを除去する手法の一つで、周辺のピクセルの色の重み付き平均を用いて該当ピクセルを塗りつぶします。
本問題のように周辺ピクセルの情報を利用する処理を用いる際は端っこのピクセルを例外的に処理することを忘れないようにしましょう。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q9 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Vector3[,] v = new Vector3[mat.Height, mat.Width];
for (int yi = 0; yi < mat.Height; yi++)
{
for (int xi = 0; xi < mat.Width; xi++)
{
Vec3b vyx = mat.At<Vec3b>(yi, xi);
v[yi, xi][0] = vyx[0];
v[yi, xi][1] = vyx[1];
v[yi, xi][2] = vyx[2];
}
}
v = Gaussian(v, mat.Height, mat.Width);
for(int yi = 0; yi < mat.Height; yi++)
{
for(int xi = 0; xi < mat.Width; xi++)
{
Vec3b vyx = new Vec3b();
vyx[0] = (byte)v[yi, xi][0];
vyx[1] = (byte)v[yi, xi][1];
vyx[2] = (byte)v[yi, xi][2];
mat.Set<Vec3b>(yi, xi, vyx);
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
public Vector3[,] Gaussian (Vector3[,] target,int height,int width)
{
Vector3[,] result = target;
for(int yi = 0; yi < height; yi++)
{
for(int xi = 0; xi < width; xi++)
{
Vector3 sumColor = new Vector3();
int[,] multiply = { { 1, 2, 1 }, { 2, 4, 2 }, { 1, 2, 1 } };
if (xi == 0)
{
multiply[0,0] = 0;
multiply[1,0] = 0;
multiply[2,0] = 0;
}
else if (xi == width-1)
{
multiply[0, 2] = 0;
multiply[1, 2] = 0;
multiply[2, 2] = 0;
}
if (yi == 0)
{
multiply[0, 0] = 0;
multiply[0, 1] = 0;
multiply[0, 2] = 0;
}else if(yi == height-1)
{
multiply[2, 0] = 0;
multiply[2, 1] = 0;
multiply[2, 2] = 0;
}
int sum = 0;
foreach (int i in multiply)
{
sum += i;
}
for(int yj = -1; yj < 2; yj++)
{
for(int xj = -1; xj < 2; xj++)
{
if(multiply[yj+1,xj+1] != 0)
{
sumColor += multiply[yj+1,xj+1] * target[yi + yj,xi + xj];
}
}
}
sumColor /= sum;
result[yi,xi] = sumColor;
}
}
return result;
}
// Update is called once per frame
void Update()
{
}
}
}
重たい…処理に10秒ぐらいかかります。愚直計算したのは誤りだったみたいです。どこがボトルネックなんだろう…
因みにGaussianフィルタはOpenCVにデフォルトで実装されています。面倒な実装が一行で済む上に処理速度もこっちの方が早いです。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q9_another : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Mat changedMat = new Mat();
Cv2.GaussianBlur(mat, changedMat, new Size(3,3),1.3,1.3);
Texture2D changedTex = Unity.MatToTexture(changedMat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
↓比較画像。左が自前実装、右がデフォルト関数です。若干ブラーの入り方に違いがあるのがわかります。
##Q10.メディアンフィルタ
画像のノイズを除去する手法の一つで、周辺のピクセルの色の中央値を用いて該当ピクセルを塗りつぶします。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
public class q10 : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Vector3[,] v = new Vector3[mat.Height, mat.Width];
for (int yi = 0; yi < mat.Height; yi++)
{
for (int xi = 0; xi < mat.Width; xi++)
{
Vec3b vyx = mat.At<Vec3b>(yi, xi);
v[yi, xi][0] = vyx[0];
v[yi, xi][1] = vyx[1];
v[yi, xi][2] = vyx[2];
}
}
v = Median(v, mat.Height, mat.Width);
for (int yi = 0; yi < mat.Height; yi++)
{
for (int xi = 0; xi < mat.Width; xi++)
{
Vec3b vyx = new Vec3b();
vyx[0] = (byte)v[yi, xi][0];
vyx[1] = (byte)v[yi, xi][1];
vyx[2] = (byte)v[yi, xi][2];
mat.Set<Vec3b>(yi, xi, vyx);
}
}
Texture2D changedTex = Unity.MatToTexture(mat);
GetComponent<RawImage>().texture = changedTex;
}
public Vector3[,] Median(Vector3[,] target, int height, int width)
{
Vector3[,] result = target;
for (int yi = 0; yi < height; yi++)
{
for (int xi = 0; xi < width; xi++)
{
int[,] multiply = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };
if (xi == 0)
{
multiply[0, 0] = 0;
multiply[1, 0] = 0;
multiply[2, 0] = 0;
}
else if (xi == width - 1)
{
multiply[0, 2] = 0;
multiply[1, 2] = 0;
multiply[2, 2] = 0;
}
if (yi == 0)
{
multiply[0, 0] = 0;
multiply[0, 1] = 0;
multiply[0, 2] = 0;
}
else if (yi == height - 1)
{
multiply[2, 0] = 0;
multiply[2, 1] = 0;
multiply[2, 2] = 0;
}
List<float> tmp_x = new List<float>();
List<float> tmp_y = new List<float>();
List<float> tmp_z = new List<float>();
for (int yj = -1; yj < 2; yj++)
{
for (int xj = -1; xj < 2; xj++)
{
if (multiply[yj + 1, xj + 1] != 0)
{
tmp_x.Add(target[yi + yj, xi + xj][0]);
tmp_y.Add(target[yi + yj, xi + xj][1]);
tmp_z.Add(target[yi + yj, xi + xj][2]);
}
}
}
tmp_x.Sort();
tmp_y.Sort();
tmp_z.Sort();
if(tmp_x.Count % 2 == 0) {
result[yi, xi][0] = (tmp_x[tmp_x.Count / 2] + tmp_x[(tmp_x.Count / 2) - 1])/2;
result[yi, xi][1] = (tmp_y[tmp_y.Count / 2] + tmp_y[(tmp_y.Count / 2) - 1])/2;
result[yi, xi][2] = (tmp_z[tmp_z.Count / 2] + tmp_z[(tmp_z.Count / 2) - 1])/2;
}
else
{
result[yi, xi][0] = tmp_x[(tmp_x.Count - 1) / 2];
result[yi, xi][1] = tmp_y[(tmp_y.Count - 1) / 2];
result[yi, xi][2] = tmp_z[(tmp_z.Count - 1) / 2];
}
}
}
return result;
}
// Update is called once per frame
void Update()
{
}
}
}
コレも重たいですね…デフォルトで実装されている関数があるのでこれを利用させていただきましょう。
namespace OpenCvSharp
{
using UnityEngine;
using System.Collections;
using OpenCvSharp;
using UnityEngine.UI;
using System;
public class q10_another : MonoBehaviour
{
public Texture2D texture;
// Use this for initialization
void Start()
{
Mat mat = Unity.TextureToMat(this.texture);
Mat changedMat = new Mat();
Cv2.MedianBlur(mat, changedMat, 3);
Texture2D changedTex = Unity.MatToTexture(changedMat);
GetComponent<RawImage>().texture = changedTex;
}
// Update is called once per frame
void Update()
{
}
}
}
#最後に
わかりやすいドキュメントがなくて大変でした。Unityで画像処理をやってみたい人は触ってみる価値がありそうです。今なら無料ですし。
Unityでやる意味あった?