隨機抽樣一致性算法(RANSAC)示例及源代碼


作者:王先榮

    大約在兩年前翻譯了《隨機抽樣一致性算法RANSAC》,在文章的最后承諾寫該算法的C#示例程序。可惜光陰似箭,轉眼許久才寫出來,實在抱歉。本文將使用隨機抽樣一致性算法來來檢測直線和圓,並提供源代碼下載。

一、RANSAC檢測流程

    在這里復述下RANSAC的檢測流程,詳細的過程見上一篇翻譯文章:

    RANSAC算法的輸入是一組觀測數據,一個可以解釋或者適應於觀測數據的參數化模型,一些可信的參數。

    RANSAC通過反復選擇數據中的一組隨機子集來達成目標。被選取的子集被假設為局內點,並用下述方法進行驗證:
    1.有一個模型適應於假設的局內點,即所有的未知參數都能從假設的局內點計算得出。
    2.用1中得到的模型去測試所有的其它數據,如果某個點適用於估計的模型,認為它也是局內點。
    3.如果有足夠多的點被歸類為假設的局內點,那么估計的模型就足夠合理。
    4.然后,用所有假設的局內點去重新估計模型,因為它僅僅被初始的假設局內點估計過。
    5.最后,通過估計局內點與模型的錯誤率來評估模型。
    這個過程被重復執行固定的次數,每次產生的模型要么因為局內點太少而被舍棄,要么因為比現有的模型更好而被選用。

二、得到觀測數據

    我們沒有實驗(測試)數據,這里用手工輸入的數據來替代——記錄您在PictureBox中的點擊坐標,作為觀測數據。

        /// <summary>
        /// 得到樣本點
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pbSample_Click(object sender, EventArgs e)
        {
            MouseEventArgs me=(MouseEventArgs)e;
            txtRandomPoints.Text += string.Format("({0},{1}),", me.X, me.Y);
            DrawPoint(new Point(me.X, me.Y));
        }
得到樣本點

 

三、檢測直線

    3.1 直線的相關知識

    (1)平面上的任意兩點可以確定一條直線;

    (2)直線的通用數學表達形式為:ax+by+c=0。這種表達形式有三個未知數,需要提供三個點才能解出a,b,c三個參數。由於隨機選擇的三個點不一定在一條直線上,所以程序中放棄這種方式。

    (3)直線可以用y=ax+b及x=c這兩個式子來表示。這兩種形式只有一個或者兩個未知數,只需兩個點就能解出a,b,c三個參數。隨機選擇的兩個點即可得到直線,我們采用這種形式。

    3.2 直線類

    直線類(Line)封裝了跟直線相關的一些屬性及方法,列表如下:

    (1)屬性

    A——y=ax+b中的a

    B——y=ax+b中的b

    C——x=c中的c

    (2)構造函數

    public Line(PointF p1, PointF p2)

    提供兩個點p1及p2,計算出直線的屬性A,B,C。

    (3)方法

    GetDistance——獲取點到直線之間的距離;

    GetY——根據x坐標,獲取直線上點的y坐標;

    ToString——獲取直線的方程式。

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace Ransac
{
    /// <summary>
    /// 直線類:采用y=a*x+b或者x=c的形式表示直線。
    /// </summary>
    public class Line
    {
        /// <summary>
        /// y=ax+b中的a
        /// </summary>
        public double A { get; set; }
        /// <summary>
        /// y=ax+b中的b
        /// </summary>
        public double B { get; set; }
        /// <summary>
        /// x=c中的c
        /// </summary>
        public double C { get; set; }
        
        /// <summary>
        /// 構造函數(如果直線為y=ax+b形式,則C為Nan;如果直線為x=c形式,則A和B為Nan)
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        public Line(double a, double b, double c)
        {
            if (!((double.IsNaN(a) && double.IsNaN(b) && !double.IsNaN(c)) || (!double.IsNaN(a) && !double.IsNaN(b) && double.IsNaN(c))))
                throw new ArgumentException("參數錯誤,無效的直線參數。");
            A = a;
            B = b;
            C = c;
        }

        /// <summary>
        /// 構造函數,由兩個點確定直線
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        public Line(PointF p1, PointF p2)
        {
            if (p1.X == p2.X)
            {
                A = double.NaN;
                B = double.NaN;
                C = p1.X;
            }
            else
            {
                A = 1d * (p1.Y - p2.Y) / (p1.X - p2.X);
                B = p1.Y - A * p1.X;
                C = double.NaN;
            }
        }

        /// <summary>
        /// 構造函數,由兩個點確定直線
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        public Line(Point p1, Point p2) :
            this(new PointF(p1.X, p1.Y), new PointF(p2.X, p2.Y))
        {
        }

        /// <summary>
        /// 生成一條隨機的直線
        /// </summary>
        /// <returns></returns>
        public static Line GetRandomLine()
        {
            Random random = new Random();
            int a = random.Next(-10, 10);
            int b = random.Next(-10, 10);
            return new Line(a, b, double.NaN);
        }

        /// <summary>
        /// 獲取點到直線的距離
        /// </summary>
        /// <param name="p"></param>
        /// <returns>返回點到直線的距離;如果直線通過點,返回0。</returns>
        public double GetDistance(Point p)
        {
            return GetDistance(new PointF(p.X, p.Y));
        }

        /// <summary>
        /// 獲取點到直線的距離
        /// </summary>
        /// <param name="p"></param>
        /// <returns>返回點到直線的距離;如果直線通過點,返回0。</returns>
        public double GetDistance(PointF p)
        {
            double d = 0d;
            if (double.IsNaN(C))
            {
                //y=ax+b相當於ax-y+b=0
                d = Math.Abs(1d * (A * p.X - p.Y + B) / Math.Sqrt(A * A + 1));
            }
            else
            {
                d = Math.Abs(C - p.X);
            }
            return d;
        }

        /// <summary>
        /// 根據x坐標,得到直線上點的y坐標 
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        public double GetY(double x)
        {
            double y;
            if (double.IsNaN(C))
                y = A * x + B;
            else
                y = double.NaN;
            return y;
        }

        /// <summary>
        /// 返回直線方程
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            string formula = "";
            if (double.IsNaN(C))
                formula = string.Format("y={0}{1}", A != 0 ? string.Format("{0:F02}x", A) : "", B != 0 ? (B > 0 ? string.Format("+{0:F02}", B) : string.Format("{0:F02}", B)) : "");
            else
                formula = string.Format("x={0:F02}", C);
            return formula;
        }
    }
}
Line類

 

    3.3 檢測直線的過程

    (1)隨機從觀測點中選擇兩個點,得到通過該點的直線;

    (2)用(1)中的直線去測試其他觀測點,由點到直線的距離確定觀測點是否為局內點或者局外點;

    (3)如果局內點足夠多,並且局內點多於原有“最佳”直線的局內點,那么將這次迭代的直線設為“最佳”直線;

    (4)重復(1)~(3)步直到找到最佳直線。

    細心的您估計已經發現我省略了標准RANSAC檢測過程中重新估計模型的步驟,我是故意的,我覺得麻煩且沒什么用處,所以咔嚓了,O(∩_∩)O~。

/// <summary>
        /// 嘗試獲取直線
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnGetLine_Click(object sender, EventArgs e)
        {
            //用RANSAC方法獲取最佳直線
            points = GetSamplePoints();
            Line bestLine = null;           //最佳直線
            double bestInliersCount = 0;    //最佳模型的局內點數目
            Random random = new Random();
            for (int idx = 0; idx < nudIterCount.Value; idx++)
            {
                int idx1, idx2;
                GetRandomInliersPoints(random, out idx1, out idx2);
                int inliersCount = 2;
                Line line = new Line(points[idx1], points[idx2]);
                for (int i = 0; i < points.Count; i++)
                {
                    if (i != idx1 && i != idx2)
                    {
                        if (line.GetDistance(points[i]) <= (double)nudMinDistance.Value)
                            inliersCount++;
                    }
                }
                if (inliersCount >= nudMinPointCount.Value)
                {
                    if (inliersCount > bestInliersCount)
                    {
                        bestLine = line;
                        bestInliersCount = inliersCount;
                    }
                }
            }
            //顯示最佳直線
            if (bestLine != null)
            {
                lblFormula.Text = string.Format("方程:{0}\r\nA:{1}\r\nB:{2}\r\nC:{3}\r\n局內點數目:{4}",
                    bestLine.ToString(), bestLine.A, bestLine.B, bestLine.C, bestInliersCount);
                DrawLine(bestLine);
            }
            else
                lblFormula.Text = "沒有獲取到最佳直線。";
        }
獲取直線

 

四、檢測圓

    4.1 圓的相關知識

    (1)平面內不在同一直線上的三個點可以確定一個圓;

    (2)圓的數學表達形式為:(x-a)2+(y-b)2=r2

    其中,(a,b)為圓心,r為半徑。

    4.2 圓類

    圓類(Circle)封裝了跟圓有關的屬性及方法,列表如下:

    (1)屬性

    A——圓心的x坐標

    B——圓心的y坐標

     R——圓的半徑

    (2)構造函數

    public Circle(PointF p1, PointF p2, PointF p3)

    提供三個點p1,p2和p3,計算出圓的屬性A,B,R。

    (3)方法

    GetDistance——獲取點到圓(周)之間的距離,表示點接近或者遠離圓;

    ToString——獲取圓的方程式。

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace Ransac
{
    /// <summary>
    /// 圓類:用(x-a)**2+(y-b)**2=r**2形式表示
    /// </summary>b
    public class Circle
    {
        /// <summary>
        /// 圓心的X坐標
        /// </summary>
        public double A { get; set; }
        /// <summary>
        /// 圓心的Y坐標
        /// </summary>
        public double B { get; set; }
        /// <summary>
        /// 半徑
        /// </summary>
        public double R { get; set; }

        /// <summary>
        /// 構造函數,提供圓心和半徑。
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="r"></param>
        public Circle(double a, double b, double r)
        {
            A = a;
            B = b;
            if (r < 0)
                throw new ArgumentOutOfRangeException("r", "圓的半徑必須大於0。");
            R = r;
        }

        /// <summary>
        /// 構造函數,提供三個點。
        /// 該算法來自csdn論壇,帖子地址是:http://bbs.csdn.net/topics/50383586,在此感謝5樓的privet。
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        /// <param name="p3"></param>
        public Circle(PointF p1, PointF p2, PointF p3)
        {
            float xMove = p1.X;
            float yMove = p1.Y;
            p1.X = 0;
            p1.Y = 0;
            p2.X = p2.X - xMove;
            p2.Y = p2.Y - yMove;
            p3.X = p3.X - xMove;
            p3.Y = p3.Y - yMove;
            float x1 = p2.X, y1 = p2.Y, x2 = p3.X, y2 = p3.Y;
            double m = 2.0 * (x1 * y2 - y1 * x2);
            if (m == 0)
                throw new ArgumentException("參數錯誤,提供的三點不能構成圓。");
            double x0 = (x1 * x1 * y2 - x2 * x2 * y1 + y1 * y2 * (y1 - y2)) / m;
            double y0 = (x1 * x2 * (x2 - x1) - y1 * y1 * x2 + x1 * y2 * y2) / m;
            R = Math.Sqrt(x0 * x0 + y0 * y0);
            A = x0 + xMove;
            B = y0 + yMove;
        }

        /// <summary>
        /// 構造函數,提供三個點。
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        /// <param name="p3"></param>
        public Circle(Point p1, Point p2, Point p3) :
            this(new PointF(p1.X, p1.Y), new PointF(p2.X, p2.Y), new PointF(p3.X, p3.Y))
        {
        }

        /// <summary>
        /// 獲取點到圓的距離(圓周,不是圓心)
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public double GetDistance(PointF p)
        {
            return Math.Abs(R - Math.Sqrt(Math.Pow(p.X - A, 2) + Math.Pow(p.Y - B, 2)));
        }

        /// <summary>
        /// 返回圓方程
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0}**2+{1}**2={2}",
                A == 0 ? "x" : (A > 0 ? string.Format("(x-{0:F02})", A) : string.Format("(x+{0:F02})", Math.Abs(A))),
                B == 0 ? "y" : (B > 0 ? string.Format("(y-{0:F02})", B) : string.Format("(y+{0:F02})", Math.Abs(B))),
                R == 0 ? "0" : string.Format("{0:F02}**2", Math.Abs(R)));
        }
    }
}
Circle類

 

    3.3 檢測圓的過程

    (1)隨機從觀測點中選擇三個點,嘗試得到通過這三個點的圓;

    (2)用(1)中的圓去測試其他觀測點,由點到圓的距離確定觀測點是否為局內點或者局外點;

    (3)如果局內點足夠多,並且局內點多於原有“最佳”圓的局內點,那么將這次迭代的圓設為“最佳”圓;

    (4)重復(1)~(3)步直到找到最佳圓。

/// <summary>
        /// 嘗試獲取圓
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnGetCircle_Click(object sender, EventArgs e)
        {
            //用RANSAC方法獲取最佳直線
            points = GetSamplePoints();
            Circle bestCircle = null;       //最佳圓
            double bestInliersCount = 0;    //最佳模型的局內點數目
            Random random = new Random();
            for (int idx = 0; idx < nudIterCount.Value; idx++)
            {
                int idx1, idx2, idx3;
                GetRandomInliersPoints(random, out idx1, out idx2, out idx3);
                int inliersCount = 3;
                Circle circle;
                try
                {
                    circle = new Circle(points[idx1], points[idx2], points[idx3]);
                }
                catch
                {
                    continue;
                }
                for (int i = 0; i < points.Count; i++)
                {
                    if (i != idx1 && i != idx2 && i!=idx3)
                    {
                        if (circle.GetDistance(points[i]) <= (double)nudMinDistance.Value)
                            inliersCount++;
                    }
                }
                if (inliersCount >= nudMinPointCount.Value)
                {
                    if (inliersCount > bestInliersCount)
                    {
                        bestCircle = circle;
                        bestInliersCount = inliersCount;
                    }
                }
            }
            //顯示最佳圓
            if (bestCircle != null)
            {
                lblFormula.Text = string.Format("方程:{0}\r\nA:{1}\r\nB:{2}\r\nR:{3}\r\n局內點數目:{4}",
                    bestCircle.ToString(), bestCircle.A, bestCircle.B, bestCircle.R, bestInliersCount);
                DrawCircle(bestCircle);
            }
            else
                lblFormula.Text = "沒有獲取到最佳圓。";
        }
獲取圓

 

五、本文源代碼

    點擊這里下載本文源代碼

    感謝您閱讀本文,希望對您有所幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM