【C#】用 EmguCV 繪製各種輪廓線

  • 12474
  • 0
  • 2019-09-04

EmguCV 內的函數常會隨著版本更新而有所變動。
每次都要重新查詢實在很麻煩,那就把它們記下來吧!

版本概要:
EmguCV 版本:3.2.0.2682
編譯器版本: Visual Studio 2017 Community 
方案平台: x64  (許多導致程式無法執行的原因是因為沒有改執行平台!)

正文開始。
首先藝術家夏恩用小畫家畫了一張圖來作為範本 — 一朵雲。
因為形狀奇特,非常適合用來說明。

1. BoundingBox: 可以框住全部範圍的矩形。

這是沒有經過旋轉地矩形,有經過旋轉的矩形在後面討論。

using System;
using System.Windows.Forms;
using System.Drawing;

using Emgu.CV;
using Emgu.CV.Structure;
using Emgu.CV.CvEnum;
using Emgu.CV.Util;


namespace Test
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // 準備兩張圖,一張用來找輪廓,一張用來繪製。
            Image<Gray, byte> I = new Image<Gray, byte>(@"D:\Test\1.jpg");
            Image<Bgr, byte> DrawI = I.Convert<Bgr, byte>();

            Image<Gray, byte> CannyImage = I.Clone();
            CvInvoke.Canny(I, CannyImage, 255, 255, 5, true);
            
            MyCV.BoundingBox(CannyImage, DrawI);
            pictureBox1.Image = DrawI.Bitmap;
        }
    }

    public class MyCV
    {
        public static void BoundingBox(Image<Gray, byte> src, Image<Bgr, byte> draw)
        {
            // 使用 VectorOfVectorOfPoint 類別一次取得多個輪廓。
            using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
            {
                // 在這版本請使用FindContours,早期版本有cvFindContours等等,在這版都無法使用,
                // 由於這邊是要取得最外層的輪廓,所以第三個參數給 null,第四個參數則用 RetrType.External。
                CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

                int count = contours.Size;
                for (int i = 0; i < count; i++)
                {
                    using (VectorOfPoint contour = contours[i])
                    {
                        // 使用 BoundingRectangle 取得框選矩形
                        Rectangle BoundingBox = CvInvoke.BoundingRectangle(contour);
                        CvInvoke.Rectangle(draw, BoundingBox, new MCvScalar(255, 0, 255, 255), 3);
                    }
                }
            }
        }
    }
}

以下這張圖是程式執行結果。

註:後面的程式碼僅寫出操作的函數,將省略主視窗及名稱空間,請自行代換主視窗的程式碼。

接著,在這邊常有看到一些範例程式會建議使用 ApproxPolyDP 這個方法,取得近似的形狀,
連EmguCV自己附帶的範例檔案都是這樣寫的,原因是可以提升程式執行效率,只是經過測試,
若是在一些精度需求不高的情況下可以這麼做,若以目前這個雲形的例子而言,夏恩並不建議這樣做。
請看以下,這是採用 ApproxPolyDP 函數的程式碼與結果。

public static void ApproxBoundingBox(Image<Gray, byte> src, Image<Bgr, byte> draw)
{
    using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
    {
        CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

        int count = contours.Size;
        for (int i = 0; i < count; i++)
        {
            using (VectorOfPoint contour = contours[i])
            using (VectorOfPoint approxContour = new VectorOfPoint())
            {
                // 使用 ApproxPolyDP 做近似,其中第三個參數是近似的限制,像是用總長度乘以0.05
                // 表示近似後的圖形總長度不得低於原本的 95% 的意思。
                CvInvoke.ApproxPolyDP(contour, approxContour, CvInvoke.ArcLength(contour, true) * 0.05, true);
                Rectangle BoundingBox = CvInvoke.BoundingRectangle(approxContour);
                CvInvoke.Rectangle(draw, BoundingBox, new MCvScalar(255, 0, 255, 255), 3);
            }
        }
    }
}

以下這張圖是程式執行結果。

從這張圖可以看到有許多捲捲的地方都被近似掉了,以至於框選出來的範圍會失真。
若目標是長方形或三角形這種比較規則的形狀,使用近似的方法可以提升執行的效率,
那是因為不是所有的圖像都是這麼乾淨,有些二值影像會有毛邊,這時用近似法可以得到不錯的結果。

其實若是直接把輪廓線畫出來就可以看得更清楚,近似後許多細節會消失。
以下是程式碼與執行結果。

public static void DrawContour(Image<Gray, byte> src, Image<Bgr, byte> draw)
{
    using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
    {
        CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

        int count = contours.Size;
        for (int i = 0; i < count; i++)
        {
            using (VectorOfPoint contour = contours[i])
            using (VectorOfPoint approxContour = new VectorOfPoint())
            {
                // 原始輪廓線
                CvInvoke.DrawContours(draw, contours, i, new MCvScalar(255, 0, 255, 255), 3);
                
                // 近似後輪廓線
                CvInvoke.ApproxPolyDP(contour, approxContour, CvInvoke.ArcLength(contour, true) * 0.02, true);
                Point[] pts = approxContour.ToArray();
                for(int j=0; j<pts.Length; j++)
                {
                    Point p1 = new Point(pts[j].X, pts[j].Y);
                    Point p2;

                    if (j == pts.Length - 1)
                        p2 = new Point(pts[0].X, pts[0].Y);
                    else
                        p2 = new Point(pts[j+1].X, pts[j+1].Y);

                    CvInvoke.Line(draw, p1, p2, new MCvScalar(255, 0, 0, 0), 3);
                }
            }
        }
    }
}

以下這張圖是程式執行結果。
粉紅色邊框是原本的輪廓線,藍色邊框則是近似後的輪廓線。
若是把上述參數 0.02 調成 0.05 則輪廓會差異更大!有興趣可以自已玩玩看。

2. ConvexHull: 可以框住區塊的最小多邊形。

並不是所有時候我們都需要整個矩形範圍,若只是要可以框住區塊的話,那就試試 ConvexHull 吧!

public static void ConvexHull(Image<Gray, byte> src, Image<Bgr, byte> draw)
{
    using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
    {
        CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

        int count = contours.Size;
        for (int i = 0; i < count; i++)
        {
            using (VectorOfPoint contour = contours[i])
            {
                // 這邊說明一下,因為 ConvexHull 執行時表示僅接受 PointF 類別,但是從 FindContours 得到
                // 的結果卻是 Point 類別,因此需要先做個轉換。
                // Point2PointF 夏恩寫的轉換函數,說不定有更迅捷的方法,可以自行 google 一下。
                PointF[] temp = Array.ConvertAll(contour.ToArray(), new Converter<Point, PointF>(Point2PointF));
                PointF[] pts = CvInvoke.ConvexHull(temp, true);

                for (int j = 0; j < pts.Length; j++)
                {
                    Point p1 = new Point((int)pts[j].X, (int)pts[j].Y);
                    Point p2;

                    if (j == pts.Length - 1)
                        p2 = new Point((int)pts[0].X, (int)pts[0].Y);
                    else
                        p2 = new Point((int)pts[j + 1].X, (int)pts[j + 1].Y);

                    CvInvoke.Line(draw, p1, p2, new MCvScalar(255, 0, 255, 255), 3);
                }
            }
        }
    }
}

private static PointF Point2PointF(Point P)
{
    PointF PF = new PointF
    {
        X = P.X,
        Y = P.Y
    };
    return PF;
}

以下這張圖是程式執行結果。

3. MinAreaBoundingBox: 可框住區域的最小矩形。

這是可旋轉的矩形,意即找到面積最小,又可以框住該區域的矩形。

public static void MinAreaBoundingBox(Image<Gray, byte> src, Image<Bgr, byte> draw)
{
    using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
    {
        CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

        int count = contours.Size;
        for (int i = 0; i < count; i++)
        {
            using (VectorOfPoint contour = contours[i])
            {
                // MinAreaRect 是此版本找尋最小面積矩形的方法。
                RotatedRect BoundingBox = CvInvoke.MinAreaRect(contour);
                CvInvoke.Polylines(draw, Array.ConvertAll(BoundingBox.GetVertices(), Point.Round), true, new Bgr(Color.DeepPink).MCvScalar, 3);
            }
        }
    }
}

以下這張圖是程式執行結果。

4. MinAreaCircle:可框住區域的最小圓形。

public static void MinAreaCircle(Image<Gray, byte> src, Image<Bgr, byte> draw)
{
    using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
    {
        CvInvoke.FindContours(src, contours, null, RetrType.External, ChainApproxMethod.ChainApproxSimple);

        int count = contours.Size;
        for (int i = 0; i < count; i++)
        {
            using (VectorOfPoint contour = contours[i])
            {
                CircleF circle = CvInvoke.MinEnclosingCircle(contour);       
                CvInvoke.Circle(draw, new Point((int)circle.Center.X, (int) circle.Center.Y), (int)circle.Radius, new MCvScalar(255, 0, 255, 255), 3);
            }
        }
    }
}

以下這張圖是程式執行結果。

在 EmguCV 內一種輪廓線就一種畫法。
真的是要足夠熟練才能夠駕馭這些函數唉!

像是可憐的夏恩已經陷在這些函數內好幾天了,真是頭昏眼花,臨表泣涕,不知所云。

參考資料

1. 特徵(moment、contourArea、arcLength)
2. EmguCV Image Process: Extracting Lines, Contours, and Components part 6
3. Comparing two fingerprint image using Emgu in C#
4. 使用EmguCv計算包圍物體的最小圓與最小可旋轉矩形和不可選擇矩形
5. 記錄, OpenCV 學習路徑, (2) 辨識多邊形 (OpenCV, Python)
6. [OpenCV] 計算輪廓面積 (Calculate Contour Area)
7. Structural Analysis and Shape Descriptors
8. EmgnCv進行輪廓尋找和計算物體凸包