Bigdra.Log

備忘録

OpenCV for Unityで画像処理100本ノック 1~10

f:id:bigdra50:20210214201613p:plain

経緯

OpenCV for Unity を使う機会があり、その練習がてら画像処理100本ノックをやってみます。

assetstore.unity.com

最初の10本はなるべくライブラリを使わずに画像処理の部分を実装します。

リポジトリはこちら

github.com

準備

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;
        }
    }
}

f:id:bigdra50:20210214201958p:plain
imori_256x256

Q1 チャネル入れ替え

f:id:bigdra50:20210214202101p:plain
チャネル入れ替え

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 グレースケール化

f:id:bigdra50:20210214202133p:plain
グレースケール化

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値化

f:id:bigdra50:20210214202209p:plain
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値化

f:id:bigdra50:20210214202245p:plain
大津の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変換

f:id:bigdra50:20210214202328p:plain
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 減色処理

f:id:bigdra50:20210214202434p:plain
減色処理

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 平均プーリング

f:id:bigdra50:20210214202520p:plain
平均プーリング

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プーリング

f:id:bigdra50:20210214202550p:plain
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 ガウシアンフィルタ

今度はこのノイズ混じりの画像を入力に使います.

f:id:bigdra50:20210214202829p:plain
imori_noise

f:id:bigdra50:20210214202627p:plain
ガウシアンフィルタ

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 メディアンフィルタ

f:id:bigdra50:20210214202701p:plain
メディアンフィルタ

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#で書いてみた - 午後わてんのブログ