OpenCV for Unityで画像処理100本ノック 1~10
経緯
OpenCV for Unity を使う機会があり、その練習がてら画像処理100本ノックをやってみます。
最初の10本はなるべくライブラリを使わずに画像処理の部分を実装します。
リポジトリはこちら
準備
OpenCVで画像の加工をする際には、
テクスチャを入力→Matという形式に変換→Matを加工→テクスチャへ変換
という流れになるようです。
この部分の処理は以下のクラスを用意して使いまわしました。
using OpenCVForUnity.CoreModule; using OpenCVForUnity.UnityUtils; using UnityEngine; namespace OneHundredKnock { public class Util { public static Mat LoadTexture(string path) { var srcTex = Resources.Load(path) as Texture2D; var srcMat = new Mat(srcTex.height, srcTex.width, CvType.CV_8UC4); Utils.texture2DToMat(srcTex, srcMat); return srcMat; } public static Texture2D MatToTexture2D(Mat imgMat) { var texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false); Utils.matToTexture2D(imgMat, texture); return texture; } } }
Q1 チャネル入れ替え
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { public class Q1 : MonoBehaviour { private void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var col = new byte[4]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { srcMat.get(x, y, col); var tmp = col[0]; col[0] = col[2]; col[2] = tmp; dstMat.put(x, y, col); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q2 グレースケール化
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// グレースケール化 /// 画像の輝度表現方法の一種で下式で計算される /// Y = 0.2126R + 0.7151G + 0.0722B /// </summary> public class Q2 : MonoBehaviour { void Start() { var imgMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC4); //Imgproc.cvtColor(imgMat, dstMat, Imgproc.COLOR_RGBA2GRAY); var rgba = new byte[4]; for (var x = 0; x < imgMat.width(); x++) { for (var y = 0; y < imgMat.height(); y++) { imgMat.get(x, y, rgba); var gr = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2]; rgba[0] = (byte)gr; rgba[1] = (byte)gr; rgba[2] = (byte)gr; dstMat.put(x, y, rgba); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q3 2値化
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// 2値化 /// 画像を黒と白の2値で表現する方法. /// ここではグレースケールにおいて閾値を128に設定して2値化する /// </summary> public class Q3 : MonoBehaviour { private void Start() { var imgMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC4); //Imgproc.cvtColor(imgMat, dstMat, Imgproc.COLOR_RGBA2GRAY); //Imgproc.threshold(dstMat, dstMat, 128, 255, Imgproc.THRESH_BINARY); var rgba = new byte[4]; for (var x = 0; x < imgMat.width(); x++) { for (var y = 0; y < imgMat.height(); y++) { imgMat.get(x, y, rgba); var gray = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2]; var col = (byte)(gray < 128 ? 0 : 255); rgba[0] = col; rgba[1] = col; rgba[2] = col; dstMat.put(x, y, rgba); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q4 大津の2値化
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// 大津の2値化 /// 2値化における分離の閾値を自動決定するアルゴリズム. クラス内分散とクラス間分散の比から計算される /// </summary> public class Q4 : MonoBehaviour { private void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = BinarizeByOtsu(srcMat); GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } private float[,] ToGrayScale(Mat srcMat) { var rgba = new byte[4]; var grs = new float[srcMat.rows(), srcMat.cols()]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { srcMat.get(x, y, rgba); grs[x, y] = .2126f * rgba[0] + .7152f * rgba[1] + .0722f * rgba[2]; } } return grs; } private int GetThreshByOtsu(float[,] grs, int pNum) { var (thresh, max) = (1, 0f); for (var t = 1; t < 255; t++) { var m0 = 0f; // 各クラス内の画素の輝度の平均 var m1 = 0f; var p0 = 0; // 各クラスに含まれる画素数 var p1 = 0; foreach (var gr in grs) { if (gr < t) { // class 0 p0++; m0 += gr; } else { // class 1 p1++; m1 += gr; } } m0 /= p0; m1 /= p1; var r0 = p0 / (float) pNum; var r1 = p1 / (float) pNum; var sbsb = r0 * r1 * (m0 - m1) * (m0 - m1); // クラス間分散 if (sbsb < max) continue; thresh = t; max = sbsb; } return thresh; } private Mat BinarizeByOtsu(Mat srcMat) { var grs = ToGrayScale(srcMat); var pNum = srcMat.rows() * srcMat.cols(); var thresh = GetThreshByOtsu(grs, pNum); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var rgba = new byte[4]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { var col = (byte) (grs[x, y] < thresh ? 0 : 255); rgba[0] = col; rgba[1] = col; rgba[2] = col; dstMat.put(x, y, rgba); } } return dstMat; } } }
Q5 HSV変換
using System; using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// HSV変換 /// 色相Hを反転(180を加算)してRGBになおす /// </summary> public class Q5 : MonoBehaviour { private void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var rgba = new byte[4]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { srcMat.get(x, y, rgba); var hsv = Rgb2Hsv(new Rgb(rgba[0], rgba[1], rgba[2])); hsv.h += 180; var rgb = Hsv2Rgb(hsv); rgba[0] = (byte) rgb.r; rgba[1] = (byte) rgb.g; rgba[2] = (byte) rgb.b; dstMat.put(x, y, rgba); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } private Hsv Rgb2Hsv(Rgb rgb) { var r = rgb.r / 255f; var g = rgb.g / 255f; var b = rgb.b / 255f; var max = Math.Max(r, Math.Max(g, b)); var min = Math.Min(r, Math.Min(g, b)); var hsv = new Hsv(); hsv.h = (int) ( Math.Abs(min - max) < float.Epsilon ? 0f : Math.Abs(min - b) < float.Epsilon ? 60f * ((g - r) / (max - min)) + 60f : Math.Abs(min - r) < float.Epsilon ? 60f * ((b - g) / (max - min)) + 180f : 60f * ((r - b) / (max - min)) + 300f); hsv.h %= 360; hsv.v = (int) (max * 100); hsv.s = (int) ((max - min) / max * 100); return hsv; } private Rgb Hsv2Rgb(Hsv hsv) { var v = hsv.v * .01f; if (hsv.s == 0) return new Rgb((int) (v * 255)); hsv.h %= 360; var s = hsv.s * .01f; var hi = hsv.h / 60; var ha = hsv.h / 60f % 1; var a = (int) (v * 255f); var b = (int) (v * (1f - s) * 255f); var c = (int) (v * (1f - s * ha) * 255f); var d = (int) (v * (1f - s * (1f - ha)) * 255f); Rgb rgb; switch (hi) { case 0: rgb = new Rgb(a, d, b); break; case 1: rgb = new Rgb(c, a, b); break; case 2: rgb = new Rgb(b, a, d); break; case 3: rgb = new Rgb(b, c, a); break; case 4: rgb = new Rgb(d, b, a); break; case 5: rgb = new Rgb(a, b, c); break; default: rgb = new Rgb(a); break; } return rgb; } struct Rgb { public Rgb(byte col) { r = col; g = col; b = col; } public Rgb(int col) { r = col; g = col; b = col; } public Rgb(byte r, byte g, byte b) { this.r = r; this.g = g; this.b = b; } public Rgb(int r, int g, int b) { this.r = r; this.g = g; this.b = b; } public int r; public int g; public int b; } struct Hsv { public int h; public int s; public int v; } } }
Q6 減色処理
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// 画像の値を256^3から4^3, すなわちR,G,B in {32, 96, 160, 224}の各4値に減色する.(量子化操作) /// </summary> public class Q6 : MonoBehaviour { void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var color = new byte[4]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { srcMat.get(x, y, color); for (var i = 0; i < color.Length; i++) { switch (color[i] / 64) { case 0: color[i] = 32; break; case 1: color[i] = 96; break; case 2: color[i] = 160; break; case 3: color[i] = 224; break; } dstMat.put(x, y, color); } } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q7 平均プーリング
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// 平均プーリング /// 画像をグリッド分割し. 各領域内の平均値でその領域内の値を埋める. /// </summary> public class Q7 : MonoBehaviour { [SerializeField] private int _grid = 16; private void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var color = new byte[4]; for (var x = 0; x < srcMat.width(); x += _grid) { for (var y = 0; y < srcMat.height(); y += _grid) { // グリッド内の領域の色の平均を求める var additive = new int[4]; for (var dx = 0; dx < _grid; dx++) { for (var dy = 0; dy < _grid; dy++) { srcMat.get(x + dx, y + dy, color); for (var i = 0; i < 3; i++) { additive[i] += color[i]; } } } for (var i = 0; i < 3; i++) { color[i] = (byte) (additive[i] / (_grid * _grid)); } for (var dx = 0; dx < _grid; dx++) { for (var dy = 0; dy < _grid; dy++) { dstMat.put(x + dx, y + dy, color); } } } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q8 Maxプーリング
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// Maxプーリング /// 平均値でなく最大値でプーリングする /// </summary> public class Q8 : MonoBehaviour { [SerializeField] private int _grid = 16; void Start() { var srcMat = Util.LoadTexture("imori_256x256"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); for (var x = 0; x < srcMat.width(); x += _grid) { for (var y = 0; y < srcMat.height(); y += _grid) { var col = new byte[4]; var maxCol = new byte[4]; for (var dx = 0; dx < _grid; dx++) { for (var dy = 0; dy < _grid; dy++) { srcMat.get(x+dx, y+dy, col); for (var i = 0; i < col.Length; i++) { maxCol[i] = col[i] > maxCol[i] ? col[i] : maxCol[i]; } } } for (var dx = 0; dx < _grid; dx++) { for (var dy = 0; dy < _grid; dy++) { dstMat.put(x+dx, y+dy, maxCol); } } } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q9 ガウシアンフィルタ
今度はこのノイズ混じりの画像を入力に使います.
using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// ガウシアンフィルタ /// 画像の平滑化を行うフィルタの一種であり, ノイズ除去にも使われる. /// ガウシアンフィルタは注目がその周辺画素を, ガウス分布による重み付けで平滑化する. /// /// </summary> public class Q9 : MonoBehaviour { private void Start() { var srcMat = Util.LoadTexture("imori_noise"); var dstMat = new Mat(srcMat.rows(), srcMat.cols(), CvType.CV_8UC4); var kernel = new double[,] { {1d / 16d, 2d / 16d, 1d / 16d,}, {2d / 16d, 4d / 16d, 2d / 16d,}, {1d / 16d, 2d / 16d, 1d / 16d,}, }; var col = new byte[4]; var src = new byte[srcMat.width(), srcMat.height(), 3]; for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { srcMat.get(x, y, col); for (var i = 0; i < src.GetLength(2); i++) { src[x, y, i] = col[i]; } } } for (var x = 0; x < srcMat.width(); x++) { for (var y = 0; y < srcMat.height(); y++) { var additive = new byte[4]; for (var dx = -kernel.GetLength(0) / 2; dx <= kernel.GetLength(0) / 2; dx++) { for (var dy = -kernel.GetLength(0) / 2; dy <= kernel.GetLength(0) / 2; dy++) { if (x + dx < 0 || x + dx >= srcMat.width() || y + dy < 0 || y + dy >= srcMat.height()) continue; for (var i = 0; i < additive.Length - 1; i++) { var tmp1 = src[x + dx, y + dy, i]; var tmp2 = kernel[dx + kernel.GetLength(0) / 2, dy + kernel.GetLength(0) / 2]; additive[i] += (byte) (tmp1 * tmp2); } } } dstMat.put(x, y, additive); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstMat); } } }
Q10 メディアンフィルタ
using System.Collections.Generic; using OpenCVForUnity.CoreModule; using UnityEngine; namespace OneHundredKnock.B { /// <summary> /// メディアンフィルター /// 注目画素の3x3の領域内の、メディアン値(中央値)を出力するフィルタ /// </summary> public class Q10 : MonoBehaviour { private void Start() { var srcImg = Util.LoadTexture("imori_noise"); var dstImg = new Mat(srcImg.rows(), srcImg.cols(), CvType.CV_8UC4); var color = new byte[4]; const int grid = 3; var srcRgb = new byte[srcImg.width(), srcImg.height(), 4]; for (var x = 0; x < srcImg.width(); x++) { for (var y = 0; y < srcImg.height(); y++) { srcImg.get(x, y, color); for (var i = 0; i < 3; i++) { srcRgb[x, y, i] = color[i]; } } } for (var x = 0; x < srcImg.width(); x++) { for (var y = 0; y < srcImg.height(); y++) { var medRgb = new List<List<byte>> { new List<byte>(), new List<byte>(), new List<byte>(), }; for (var dx = -grid / 2; dx <= grid / 2; dx++) { for (var dy = -grid / 2; dy <= grid / 2; dy++) { if (x + dx < 0 || x + dx >= srcImg.width() || y + dy < 0 || y + dy >= srcImg.height()) continue; for (var i = 0; i < medRgb.Count; i++) { medRgb[i].Add(srcRgb[x + dx, y + dy, i]); } } } for (var i = 0; i < medRgb.Count; i++) { medRgb[i].Sort(); color[i] = medRgb[i][medRgb[i].Count / 2]; } dstImg.put(x, y, color); } } GetComponent<Renderer>().material.mainTexture = Util.MatToTexture2D(dstImg); } } }
Q11以降はOpenCVのライブラリをガンガン使っていきます
参考記事
Documentation – OpenCV for Unity Gasyori100knock/Question_01_10 at master · yoyoyo-yo/Gasyori100knock · GitHub 画像処理100本ノック!!(001 - 010)丁寧にじっくりと - Qiita OpenCV plus Unityを使ってみる(セットアップ、画像処理100本ノック1~10編) - Qiita 大津の二値化ってなんだ…ってなった. - Qiita HSV色空間 - Wikipedia ツール|色の変換(RGB・HSV・HSL) | 矢野ヒロタ 【C#】ガウシアンフィルタで画像のぼかし(ノイズ除去) | 西住工房 3x3のメディアンフィルタの高速化をC#で書いてみた - 午後わてんのブログ