UnityでOpenCV その4

さて、なんやかんや2か月以上も過ぎていましたがorz
いよいよ前回の実装に入ります。
OpenCVUnity_findcontour1.png

抽出処理については、基本的にこちらの記事を参考にしました。
OpenCVで輪郭抽出から隣接領域の切り出し(その1)輪郭抽出まで

では、抽出処理の流れに沿って、それぞれの処理を見てみましょう。
なお、ここから先は画像処理に関係する処理を主に抜粋して載せています。
追記に当方の作成した全ソースコードを載せているため、先にそちらをご覧いただいてもかまいません。

【前処理】
まずは、元画像、および画像処理とマスク用のMat型インスタンスをそれぞれ用意しておきます。
画像処理用のMatには、元画像のグレースケールを格納します。

//元画像を、リソースフォルダからTexture2Dとして読み込む
Texture2D texture_src = Resources.Load(texturePath) as Texture2D;

//元画像のMatを用意
Mat imgMat= new Mat(src.height, src.width, CvType.CV_8UC4);
//Texture2DをMatに変換(OpenCVUnityのUtilクラス内メソッド)
Utils.texture2DToMat(src, imgMat);

//画像処理用Mat画像
Mat mat_proc = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);
//元画像のグレースケールを入れておく
Imgproc.cvtColor(imgMat, mat_proc, Imgproc.COLOR_RGBA2GRAY);
//マスク用Mat画像
Mat maskMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);


また、二値化処理をかけて輪郭抽出に適した画像(モノクロ画像)にしておきます。
二値化処理はいくつか方法があり、まず手動で調整するとこのようになります。

//mat_procは画像処理の対象(Mat)、thresは二値化の閾値(0~255)
//Imgproc.THRESH_BINARY_INV: 閾値より大きい値は0に,それ以外はmaxValに
Imgproc.threshold(mat_proc, mat_proc, thres, 255, Imgproc.THRESH_BINARY_INV);


ただ、閾値は画像の状態によって大きく左右されるため、いちいち手動で値を設定するのは面倒です。
そこで、画像に適した閾値を自動で計算してくれるアルゴリズムを用いた方法もあるため、下で紹介していきます。
まずは大津の手法による二値化処理です。

//大津の手法による、単純2値化処理
Imgproc.threshold(mat_proc, mat_proc, 0, 255, Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);


ただし、先ほどの手法でも画素内の明るさが大きく異なる部分があると、処理の結果が芳しくないことがあります。
そこで、それぞれの場所に応じた閾値を設定できる、適応的閾値処理を使うことも可能です(引数の詳しい説明などは省きます)。

//GaussianCによる適応的閾値処理
//adaptiveMethod: Imgproc.ADAPTIVE_THRESH_MEAN_C と分けて使う
Imgproc.adaptiveThreshold(mat_proc, mat_proc, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY_INV, 201, 11);


【輪郭抽出】
ここまで、輪郭抽出に必要な二値化画像が手に入った段階です。
いよいよ輪郭の抽出に入っていきます。

まずは全体の流れから見ていきましょう。

//procMode: 輪郭抽出の手法を決める列挙型ExtractMode
//ThresArea: 輪郭を抽出したい領域の最小面積

//輪郭
List<MatOfPoint> contours = new List<MatOfPoint>();

//処理状態によってマスク処理に使うMat画像を切り替える
//2値化画像を直接使う
if (procMode == (int)ExtractMode.BINARY)
{
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//コピー
mat_proc.copyTo(maskMat);
}
//2値化画像の輪郭を切り取って使う
else if (procMode == (int)ExtractMode.CONTOUR)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE); //輪郭内にある輪郭もツリー構造で抽出

//領域(面積)の割合が一定以上の輪郭を探す: getCertainAreaContour
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
contours.Add(point);
}

//マスク領域の生成
//Imgproc.drawContours の線の太さを負の値にすると、内部の領域も塗りつぶす
Imgproc.drawContours(maskMat, contours, -1, new Scalar(255), -1);
}
//2値化画像の輪郭を直線近似して領域を求める
else if (procMode == (int)ExtractMode.POLYLINE)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)の割合が一定以上の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint approxf = getLineApproxContour(point, 0.001);
contours.Add(approxf);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}
//2値化画像の輪郭を凸包して領域を求める
else if (procMode == (int)ExtractMode.HULL)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)の割合が一定以上の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint mopOut = getHullContour(point);
contours.Add(mopOut);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}


実は、上記のスクリプトでは一部、自作のメソッドを用いていたのですが、
各メソッドの実装内容は以下の通りです。

/// <summary>
/// 抽出した輪郭リストから一定以上の面積を持つ輪郭を返す
/// </summary>
/// <param name="contours">輪郭のリスト</param>
/// <param name="thresArea">面積の閾値</param>
/// <returns>面積が閾値以上の輪郭すべて</returns>
List<MatOfPoint> getCertainAreaContour(List<MatOfPoint> contours, double thresArea = 10)
{
List<MatOfPoint> result = new List<MatOfPoint>();

foreach (var each in contours)
{
//面積を求める
double area = Imgproc.contourArea(each);
if (area > thresArea) result.Add(each);
}

return result;
}

/// <summary>
/// 輪郭(MatOfPoint)から直線近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <param name="approxRate">カーブの細かさ</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getLineApproxContour(MatOfPoint mopIn, double approxRate = 0.001)
{
MatOfPoint approxf1 = new MatOfPoint();
MatOfPoint2f curveContour = new MatOfPoint2f(mopIn.toArray());
MatOfPoint2f approxContour = new MatOfPoint2f();
Imgproc.approxPolyDP(curveContour, approxContour, approxRate * Imgproc.arcLength(curveContour, true), true);

//直線近似した輪郭を変換
approxContour.convertTo(approxf1, CvType.CV_32S);

return approxf1;
}

/// <summary>
/// 輪郭(MatOfPoint)から凸包近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getHullContour(MatOfPoint mopIn)
{
MatOfPoint mopOut = new MatOfPoint();

//輪郭を凸包
MatOfInt hull = new MatOfInt();
Imgproc.convexHull(mopIn, hull);

//凸包した輪郭を変換
int size = (int)hull.size().height;
mopOut.create(size, 1, CvType.CV_32SC2);
for (int i = 0; i < size; i++)
{
int index = (int)hull.get(i, 0)[0];
double[] point = new double[] { mopIn.get(index, 0)[0], mopIn.get(index, 0)[1] };
mopOut.put(i, 0, point);
}

return mopOut;
}


【仕上げ】
ここまで、画像の特定の領域に対する輪郭抽出ができました。
ですが、わずかに残るノイズを処理するため、以下の処理を加えて不要な部分を除去します。

//ノイズ除去
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//膨張処理(dilate)
Imgproc.dilate(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//オープニング
Imgproc.morphologyEx(maskMat, maskMat, Imgproc.MORPH_OPEN, new Mat());


【マスク処理】
あとは抽出できた輪郭を使い、画像の一部分だけを取り出す処理を行ってみましょう。

//マスク処理
Mat imgResult = new Mat(src.height, src.width, CvType.CV_8UC4, new Scalar(255, 255, 255));
imgMat.copyTo(imgResult, maskMat); //元画像のマットにマスクを当てはめたうえで結果画像(Mat)にコピー


こうして、輪郭抽出の大まかな流れは終わりです。
以上の処理をもとに輪郭抽出した結果はこちらです。
OpenCVUnity_findcontour2.png

Unityで画像処理をする機会は少ないかもしれませんが、当方の記事が少しでも参考になれば幸いです。
ではまた。
【ソースコード】
本当は本文中に載せたかったのですが、長すぎたのでこちらに移動しました。
なお、ここでは本文中の処理に加え、元画像をトリミングする処理なども実行されています。

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

public class FindContourScript : MonoBehaviour {

/// <summary>
/// 左右、および上下のトリミング幅を表す構造体
/// </summary>
[System.Serializable]
public struct CropArea
{
//画像切り取りの左端
[Range(0f, 1f)]
public float Left;
//画像切り取りの上端
[Range(0f, 1f)]
public float Top;
//画像切り取りの幅
[SerializeField, Range(0f, 1f)]
public float width;
//画像切り取りの高さ
[SerializeField, Range(0f, 1f)]
public float height;

/// <summary>
/// 画像サイズを考慮した切り取り幅
/// </summary>
public float CropWidth
{
get
{
if (Left + width > 1f || width <= 0f) return (1f - Left);
return width;
}
}
/// <summary>
/// 画像サイズを考慮した切り取り高さ
/// </summary>
public float CropHeight
{
get
{
if (Top + height > 1f || height <= 0f) return (1f - Top);
return height;
}
}
}

//モード
public enum ExtractMode { BINARY = 0, CONTOUR, POLYLINE, HULL }

//抽出モード
[SerializeField]
ExtractMode ex_mode;

/// <summary>
/// リソースの読み込み先
/// </summary>
public string texturePath = "test001";

/// <summary>
/// 二値化の閾値
/// </summary>
[Range(0, 256)]
public int ThresBinary = 128;

/// <summary>
/// 面積の閾値
/// </summary>
public double ThresArea = 100;

/// <summary>
/// 閾値反転をさせるか
/// </summary>
public bool BinInv = false;

/// <summary>
/// トリミングの領域
/// </summary>
[SerializeField]
CropArea cropArea;

//高速モード
[SerializeField]
bool fastMode = false;
//抽出した輪郭の表示
[SerializeField]
bool showContour = false;

// Use this for initialization
void Start ()
{
Texture2D texture_src = Resources.Load(texturePath) as Texture2D;

//テクスチャ貼り付け
SetTexture(texture_src);
}

/// <summary>
/// 貼り付けるテクスチャをセットする
/// </summary>
/// <param name="texture_src">テクスチャ画像</param>
public void SetTexture(Texture2D texture_src)
{
#if UNITY_EDITOR
Debug.LogFormat("Texture Size: W{0}, H{1}", texture_src.width, texture_src.height);
#endif

//テクスチャ解像度
int w = texture_src.width, h = texture_src.height;
//マスク処理
var roi = new OpenCVForUnity.Rect((int)(w * cropArea.Left), (int)(h * cropArea.Top), (int)(w * cropArea.CropWidth), (int)(h * cropArea.CropHeight));
var imgMat = ExtractDrawing(texture_src, roi, (int)ex_mode, ThresBinary, showContour, fastMode);

//加工したMat画像をテクスチャに変換
Texture2D texture_out = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);

if (fastMode)
{
Utils.fastMatToTexture2D(imgMat, texture_out);
}
else
{
Utils.matToTexture2D(imgMat, texture_out);
}

//メモリ解放
imgMat = null;

//テクスチャを適用する
GetComponent<Renderer>().material.mainTexture = texture_out;
}

/// <summary>
/// テクスチャから描画された絵の部分を抽出するメソッド
/// </summary>
/// <param name="src">元画像</param>
/// <param name="procMode">抽出モード</param>
/// <param name="thres">2値化の閾値</param>
/// <param name="outline">抽出した部分の表示</param>
/// <param name="fast">高速処理モード</param>
/// <returns>画像処理を施したMat</returns>
public Mat ExtractDrawing(Texture2D src, OpenCVForUnity.Rect roi, int procMode, int thres, bool outline = false, bool fast = false)
{
//元のテクスチャ素材をMatに
Mat origin = new Mat(src.height, src.width, CvType.CV_8UC4);

//素材をMatに変換
if (fast)
{
Utils.fastTexture2DToMat(src, origin);
//Debug.Log("FastMode imgMat dst ToString " + mat_proc.ToString());
}
else
{
Utils.texture2DToMat(src, origin);
//Debug.Log("imgMat dst ToString " + mat_proc.ToString());
}

//元素材のMat画像
Mat imgMat = new Mat(origin, roi); //ROI領域を指定してトリミング

//画像処理用Mat画像
Mat mat_proc = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);
Imgproc.cvtColor(imgMat, mat_proc, Imgproc.COLOR_RGBA2GRAY);
//マスク用Mat画像
Mat maskMat = new Mat(imgMat.rows(), imgMat.cols(), CvType.CV_8UC1);

//2値化処理(反転する場合は Imgproc.THRESH_BINARY_INVを使う)
//Imgproc.THRESH_OTSUを指定すると、thresの値に関係なく自動で閾値を設定する
int type = (BinInv) ? Imgproc.THRESH_BINARY_INV : Imgproc.THRESH_BINARY;
if (thres < 0 || thres > 255)
{
//GaussianCによる適応的閾値処理
//adaptiveMethod: Imgproc.ADAPTIVE_THRESH_MEAN_C と分けて使う
//thresholdType: Imgproc.THRESH_BINARY と分けて使う
Imgproc.adaptiveThreshold(mat_proc, mat_proc, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, type, 201, 11); //201, 11

//大津の手法による、単純2値化処理
//Imgproc.threshold(mat_proc, mat_proc, 0, 255, Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
}
else
{
Imgproc.threshold(mat_proc, mat_proc, thres, 255, type);
}

//輪郭
List<MatOfPoint> contours = new List<MatOfPoint>();

//処理状態によってマスク処理に使うMat画像を切り替える
//2値化画像を直接使う
if (procMode == (int)ExtractMode.BINARY)
{
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//コピー
mat_proc.copyTo(maskMat);
}
//2値化画像の輪郭を切り取って使う
else if (procMode == (int)ExtractMode.CONTOUR)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE); //輪郭内にある輪郭もツリー構造で抽出

//領域(面積)が最大の輪郭を探す: getMaxAreaContour
//MatOfPoint maxContour = getMaxAreaContour(contours);
//contours.Clear();
//contours.Add(maxContour);

//領域(面積)の割合が一定以上(0.1)の輪郭を探す: getCertainAreaContour
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
contours.Add(point);
}

//マスク領域の生成
//Imgproc.drawContours の線の太さを負の値にすると、内部の領域も塗りつぶす
Imgproc.drawContours(maskMat, contours, -1, new Scalar(255), -1);
}
//2値化画像の輪郭を直線近似して領域を求める
else if (procMode == (int)ExtractMode.POLYLINE)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)が最大の輪郭から、輪郭を直線近似
//MatOfPoint approxf1 = getLineApproxContour(getMaxAreaContour(contours), 0.001);
//contours.Clear();
//contours.Add(approxf1);

//領域(面積)の割合が一定以上(0.1)の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint approxf = getLineApproxContour(point, 0.001);
contours.Add(approxf);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}
//2値化画像の輪郭を凸包して領域を求める
else if (procMode == (int)ExtractMode.HULL)
{
//輪郭抽出の処理
Imgproc.findContours(mat_proc, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//領域(面積)が最大の輪郭から、輪郭を凸包
//MatOfPoint mopOut = getHullContour(getMaxAreaContour(contours));
//contours.Clear();
//contours.Add(mopOut);

//領域(面積)の割合が一定以上(0.1)の輪郭を直線近似
List<MatOfPoint> areas = getCertainAreaContour(contours, ThresArea);
contours.Clear();
foreach (MatOfPoint point in areas)
{
MatOfPoint mopOut = getHullContour(point);
contours.Add(mopOut);
}

//マスク領域の生成
Imgproc.fillPoly(maskMat, contours, new Scalar(255));
}

//ノイズ除去
//縮小処理(erode)
Imgproc.erode(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//膨張処理(dilate)
Imgproc.dilate(maskMat, maskMat, new Mat(), new Point(-1, 1), 1);
//オープニング
Imgproc.morphologyEx(maskMat, maskMat, Imgproc.MORPH_OPEN, new Mat());

//マスク処理
Mat imgResult = new Mat(src.height, src.width, CvType.CV_8UC4, new Scalar(255, 255, 255));
imgMat.copyTo(imgResult, maskMat);

//リソース解放
if (!mat_proc.IsDisposed) mat_proc.Dispose();
if (!imgMat.IsDisposed) imgMat.Dispose();
if (!maskMat.IsDisposed) maskMat.Dispose();

//輪郭の描画
if (outline && contours.Count > 0)
{
Imgproc.drawContours(imgResult, contours, -1, new Scalar(0, 255, 0, 255), 2);
}

return imgResult;
}

/// <summary>
/// 抽出した輪郭リストから最大の面積を持つ輪郭を返す
/// </summary>
/// <param name="contours">輪郭のリスト</param>
/// <returns>面積が最大の輪郭</returns>
MatOfPoint getMaxAreaContour(List<MatOfPoint> contours)
{
//最大の面積を持つ輪郭の番号
int idxMain = 0;
//最大の面積値を記憶する
double maxArea = 0;
//リスト内で最大の面積を探す
foreach (MatOfPoint each in contours)
{
//面積を求める
double area = Imgproc.contourArea(each);
if (area > maxArea)
{
maxArea = area;
//Linqでリストの同じ要素の番号を求める
idxMain = contours.FindIndex((t) => (t == each));
}
}

return contours[idxMain];
}

/// <summary>
/// 抽出した輪郭リストから一定以上の面積を持つ輪郭を返す
/// </summary>
/// <param name="contours">輪郭のリスト</param>
/// <param name="thresArea">面積の閾値</param>
/// <returns>面積が閾値以上の輪郭すべて</returns>
List<MatOfPoint> getCertainAreaContour(List<MatOfPoint> contours, double thresArea = 10)
{
List<MatOfPoint> result = new List<MatOfPoint>();

foreach (var each in contours)
{
//面積を求める
double area = Imgproc.contourArea(each);
if (area > thresArea) result.Add(each);
}

return result;
}

/// <summary>
/// 輪郭(MatOfPoint)から直線近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <param name="approxRate">カーブの細かさ</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getLineApproxContour(MatOfPoint mopIn, double approxRate = 0.001)
{
MatOfPoint approxf1 = new MatOfPoint();

MatOfPoint2f curveContour = new MatOfPoint2f(mopIn.toArray());
MatOfPoint2f approxContour = new MatOfPoint2f();
Imgproc.approxPolyDP(curveContour, approxContour, approxRate * Imgproc.arcLength(curveContour, true), true);

//直線近似した輪郭を変換
approxContour.convertTo(approxf1, CvType.CV_32S);

return approxf1;
}

/// <summary>
/// 輪郭(MatOfPoint)から凸包近似を施した輪郭を返す
/// </summary>
/// <param name="mopIn">変換元の輪郭(MatOfPoint)</param>
/// <returns>変換後の輪郭</returns>
MatOfPoint getHullContour(MatOfPoint mopIn)
{
MatOfPoint mopOut = new MatOfPoint();

//輪郭を凸包
MatOfInt hull = new MatOfInt();
Imgproc.convexHull(mopIn, hull);

//凸包した輪郭を変換
int size = (int)hull.size().height;
mopOut.create(size, 1, CvType.CV_32SC2);
for (int i = 0; i < size; i++)
{
int index = (int)hull.get(i, 0)[0];
double[] point = new double[] { mopIn.get(index, 0)[0], mopIn.get(index, 0)[1] };
mopOut.put(i, 0, point);
}

return mopOut;
}

/// <summary>
/// テクスチャ画像のトリミング処理(時間がかかる)
/// </summary>
/// <param name="src">元素材のテクスチャ</param>
/// <param name="dst">出力先のテクスチャ</param>
/// <param name="x">左上のx座標</param>
/// <param name="y">左上のy座標</param>
/// <param name="width">画像の横幅</param>
/// <param name="height">画像の縦幅</param>
void cropTexture(Texture2D src, out Texture2D dst, int x, int y, int width, int height)
{
//トリミングした領域のピクセル配列を取得
Color[] pix = src.GetPixels(x, y, width, height);

//出力先のテクスチャに割り当て
dst = new Texture2D(width, height);
dst.SetPixels(pix);
dst.Apply();
}
}

コメントの投稿

非公開コメント

プロフィール

Reveちゃん

Author:Reveちゃん
コンビでやってます。
夢担当と技術担当がいます。

大学院卒業 → ロボットベンチャー(漆黒)就職 → 1年で退職 → ベトナムで仕事中(今ここ) → メディアアーティスト(未来☆)

リンクフリーです。

最新記事
最新コメント
月別アーカイブ
カテゴリ
アクセス数
検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR