1. 程式人生 > >WPF實現KMEANS演算法

WPF實現KMEANS演算法

KMEANS演算法很簡單,用C實現整個演算法應該不到100行程式碼吧(不要寫介面),但是我擁有將100行程式碼進化為1000行程式碼的超能力(真是無力吐槽),所以寫這麼簡單的東西居然花了我差不多2天。其實主要還是介面程式設計不熟悉,我還得要多練練啊。

KMEANS演算法在這裡就不多做介紹了,下面簡要說一下程式碼:

1)XAML:

<Window Name="window" x:Class="KMEANS.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:k="clr-namespace:KMEANS"
        Title="MainWindow" Height="650" Width="700">
    <Grid Name="RootGd" Margin="0,0,0,0">
        <TextBlock HorizontalAlignment="Left" Margin="281,23,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="-0.849,0.384"><Run Language="zh-cn" Text="劃分數量"/></TextBlock>
        <TextBox x:Name="CommunityAmount" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Margin="365,19,0,0"/>
        <Button x:Name="BeginBt" Content="開始" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" RenderTransformOrigin="7.774,2.89" Margin="524,21,0,0" Click="BeginBt_Click"/>
        <k:MyCanvasClass x:Name="MyCanvas" HorizontalAlignment="Left" Height="550" Margin="28,58,0,0" VerticalAlignment="Top" Width="620">
            <Border BorderBrush="#FFD61B1B" BorderThickness="4" Height="550" Width="620" Canvas.Top="2"/>
        </k:MyCanvasClass>

    </Grid>
</Window>

介面相當簡單,因為我的資料是從硬碟上面讀取的所以就不做資料的輸入框了。那個聚落數量是要自己設定的,而且要小於等於10.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace KMEANS
{
    public partial class MainWindow : Window
    {
        private static int pointAmount = 0;
        private static int communityAmount = 0;
        // 所有點的起始座標
        private List<Point> points;
        // 聚集點座標
        private List<Point> communitys;

        // 所有點的自定義圖形
        private List<MyEllipse> pointsE;
        // 聚集點的自定義圖形
        private List<MyEllipse> communitysE;

        private SolidColorBrush pointBrush;
        private SolidColorBrush communityBrush;

        public delegate void InvokeSetTitle(string str);
        public delegate void InvokeDrawPoint();
        // 計算KMEANS完畢之後返回事件
        public delegate void InvokeSetResult(StateChangedEventArgs args);

        private Calculator calculator;

        public MainWindow()
        {
            InitializeComponent();
            pointBrush = new SolidColorBrush(Colors.Red);
            communityBrush = new SolidColorBrush(Colors.Black);
            pointsE = new List<MyEllipse>();
            communitysE = new List<MyEllipse>();
        }

        private void BeginBt_Click(object sender, RoutedEventArgs e)
        {
            //pointAmount = int.Parse(PointAmount.Text);
            communityAmount = int.Parse(CommunityAmount.Text);
            window.Title = "正在生成隨機點...";
            Thread t = new Thread(new ThreadStart(init));
            t.Start();
        }

        /// <summary>
        /// 生成隨機點的函式
        /// </summary>
        private void init()
        {
            points = new List<Point>();
            communitys = new List<Point>();
            Random ra = new Random();
            try
            {
                FileStream fs = new FileStream("Data.txt", FileMode.Open);
                StreamReader sr = new StreamReader(fs);
                String line = sr.ReadLine();
                while (line != null)
                {
                    pointAmount++;
                    Point p = new Point();
                    String[] point = line.Split(' ');        
                    p.X = float.Parse(point[0]);
                    p.Y = float.Parse(point[1]);
                    points.Add(p);
                    line = sr.ReadLine();
                }
                sr.Close();
            }
            catch(IOException e)
            {
                MessageBox.Show("檔案讀取失敗!"+e.Message);
                return;
            }

            for (int i = 0; i < communityAmount; i++)
            {
                int x = ra.Next(550);
                int y = ra.Next(620);
                Point p = new Point();
                p.X = x;
                p.Y = y;
                communitys.Add(p);
            }

            InvokeSetTitle m = new InvokeSetTitle(SetWindowTitle);
            Dispatcher.BeginInvoke(m, new object[] { "隨機點已生成,正在繪製..." });

            InvokeDrawPoint m2 = new InvokeDrawPoint(drawPoint);
            Dispatcher.BeginInvoke(m2, null);
        }

        /// <summary>
        /// 改變標題欄
        /// </summary>
        /// <param name="s">設定字串</param>
        private void SetWindowTitle(String s)
        {
            window.Title = s;
        }


        /// <summary>
        /// 根據傳入的p在Canvas上面畫點
        /// </summary>
        /// <param name="p"></param>
        private void drawPoint()
        {
            for (int i = 1; i <= points.Count; i++)
            {
                MyEllipse e = new MyEllipse();
                e.E = new Ellipse();
                e.E.Height = 3.0;
                e.E.Width = 3.0;
                e.ColorR = ColorResources.GetColor(1);
                e.CurPoint = points[i - 1];
                e.ID = i;
                e.BelongCommunity = 0;
                pointsE.Add(e);
                MyCanvas.drawPoint(e);
            }
            for (int i = 1; i <= communitys.Count; i++)
            {
                MyEllipse e = new MyEllipse();
                e.E = new Ellipse();
                e.E.Height = 8.0;
                e.E.Width = 8.0;
                e.ColorR = ColorResources.GetColor(11);
                e.CurPoint = points[i - 1];
                e.ID = i;
                e.BelongCommunity = 0;
                communitysE.Add(e);
                MyCanvas.drawPoint(e);
            }
            window.Title = "繪製完畢,正在計算...";

            StateChangedEventArgs args = new StateChangedEventArgs();
            args.communitysE = communitysE;
            args.pointsE = pointsE;
            Thread t = new Thread(delegate() { cal(args); });
            t.Start();
        }

        private void cal(StateChangedEventArgs args)
        {
            calculator = new Calculator();
            calculator.StateChangedEvent += afterCalculate;
            calculator.Calculate(args);
        }

        /// <summary>
        /// 計算完畢KMEANS之後把相應點的引數回傳,並且在此函式進行繪製
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void afterCalculate(Calculator sender, StateChangedEventArgs args)
        {
            InvokeSetResult m = new InvokeSetResult(SetResult);
            Dispatcher.BeginInvoke(m, new object[] { args });
        }

        private void SetResult(StateChangedEventArgs args)
        {
            var p = args.pointsE;
            foreach (MyEllipse e in p)
            {
                MyCanvas.ChangeEState(e, MyCanvasClass.POINT);
            }

            var c = args.communitysE;
            foreach (MyEllipse e in c)
            {
                MyCanvas.ChangeEState(e, MyCanvasClass.COMMUNITY);
            }
            window.Title = "計算完畢";
        }
    }
}

上面是CODE BEHIND,主要進行和UI的互動。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace KMEANS
{
    public class MyCanvasClass : Canvas
    {
        public static int POINT = 0;
        public static int COMMUNITY = 1;
        public void drawPoint(MyEllipse e)
        {    
            e.E.Visibility = Visibility.Visible;
            e.E.Fill = new SolidColorBrush(e.ColorR);
            e.E.SetValue(Canvas.TopProperty, e.CurPoint.X);
            e.E.SetValue(Canvas.LeftProperty, e.CurPoint.Y);
            this.Children.Add(e.E);
        }

        /// <summary>
        /// 改變橢圓的位置。當計算出KMEANS之後會呼叫
        /// </summary>
        /// <param name="e">橢圓引用</param>
        /// <param name="type">橢圓型別</param>
        public void ChangeEState(MyEllipse e, int type)
        {
            e.E.Fill = new SolidColorBrush(e.ColorR);
            if (type == COMMUNITY)
            {
                e.E.SetValue(Canvas.TopProperty, e.CurPoint.X);
                e.E.SetValue(Canvas.LeftProperty, e.CurPoint.Y);
            }
        }

    }
}

這裡是重寫的Canvas類,主要負責把橢圓新增進去介面去(圖形裡面我用橢圓來替代點)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace KMEANS
{
    /// <summary>
    /// 自定義控制元件
    /// </summary>
    public class MyEllipse : UIElement
    {
        public Ellipse E { get; set; }
        public int ID { get; set; }
        public Point CurPoint { get; set; }
        public Color ColorR { get; set; }
        
        // 只有Point才有的屬性,在迭代中屬於哪個Community
        public int BelongCommunity { get; set; }

        public MyEllipse()
        {
            E = new Ellipse();
            CurPoint = new Point();
        }
    }
}

這裡是我寫的橢圓類。裡面要關聯有一些關於這個點(橢圓)的相關資訊。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;

namespace KMEANS
{
    /// <summary>
    /// 儲存顏色資源
    /// </summary>
    public class ColorResources
    {
        private static Dictionary<int, Color> MyColorResources;
        private ColorResources() { }

        /// <summary>
        /// 只支援10種顏色的儲存
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public static Color GetColor(int index)
        {
            if (MyColorResources == null)
            {
                MyColorResources = new Dictionary<int, Color>();
                MyColorResources.Add(1, Colors.Red);
                MyColorResources.Add(2, Colors.DarkSeaGreen);
                MyColorResources.Add(3, Colors.Blue);
                MyColorResources.Add(4, Colors.Brown);
                MyColorResources.Add(5, Colors.Coral);
                MyColorResources.Add(6, Colors.DarkBlue);
                MyColorResources.Add(7, Colors.Green);
                MyColorResources.Add(8, Colors.Orange);
                MyColorResources.Add(9, Colors.Pink);
                MyColorResources.Add(10, Colors.Yellow);
                MyColorResources.Add(11, Colors.Black);
            }
            if (index > 11 && index < 1)
            {
                throw new IndexException();
            }
            return MyColorResources[index];
        }
    }

    public class IndexException : Exception
    {
        public String e = "下標溢位";
    }
}

這個是我定義的顏色資源類。只定義了11種顏色。其中普通點可以採用前面10種,聚集點採用最後一種也就是黑色。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KMEANS
{
    public class StateChangedEventArgs : EventArgs
    {
        // 所有點的自定義圖形
        public List<MyEllipse> pointsE;
        // 聚集點的自定義圖形
        public List<MyEllipse> communitysE;
    }
}

這是我自定義的傳引數類,在後臺計算引擎和MainWindow之間進行資料互動(事件需要)。

最後是最重要的演算法的資料引擎。整個演算法寫在裡面的,演算法真的差不多100行左右。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace KMEANS
{
    /// <summary>
    /// 負責計算KMEANS的主要邏輯程式碼
    /// </summary>
    public class Calculator
    {
        // 委託  
        public delegate void StateChanged(Calculator sender, StateChangedEventArgs args);
        // 定義事件  
        public event StateChanged StateChangedEvent;

        // 所有點的自定義圖形
        public List<MyEllipse> pointsE;
        // 聚集點的自定義圖形
        public List<MyEllipse> communitysE;

        private StateChangedEventArgs parm;

        private int countTime = 10000;
        private const double NORMAL = 0.1;


        /// <summary>
        /// 計算KMEANS的入口函式
        /// </summary>
        /// <param name="args"></param>
        public void Calculate(StateChangedEventArgs args)
        {
            pointsE = args.pointsE;
            communitysE = args.communitysE;
            parm = args;
            int c = 0;

            while (countTime > 0)
            {
                Debug.WriteLine(c++);
                findNearest();
                if (findCenter() == false)
                {
                    break;
                }
                countTime--;
            }
            StateChangedEvent(this, args);
        }

        /// <summary>
        /// 在一次迭代中為Point找到最近的Community,並且改變Point的顏色,修改其所屬Community
        /// </summary>
        private void findNearest()
        {
            foreach (MyEllipse e in pointsE)
            {
                Point a = e.CurPoint;
                int neareastId = 0;
                double distance = 9999999.9;
                foreach (MyEllipse c in communitysE)
                {
                    double d = getDistance(e.CurPoint, c.CurPoint);
                    if (d < distance)
                    {
                        neareastId = c.ID;
                        distance = d;
                    }
                }
                e.BelongCommunity = neareastId;
                if (neareastId > 0 && neareastId <= 10)
                {
                    e.ColorR = ColorResources.GetColor(neareastId);
                }
            }
        }

        /// <summary>
        /// 在一次迭代中(首先通過findNearest將Points分簇)將簇心重新定位,實際上改變該Community的位置
        /// <returns>true:程式找到了要變動的點  false:找不到,KMEANS停止</returns>
        /// </summary>
        private bool findCenter()
        {
            bool changed = false;
            foreach (MyEllipse e in communitysE)
            {
                double totalX = 0;
                double totalY = 0;
                int amount = 0;
                foreach (MyEllipse t in pointsE)
                {
                    if (t.BelongCommunity == e.ID)
                    {
                        totalX += t.CurPoint.X;
                        totalY += t.CurPoint.Y;
                        amount++;
                    }
                }
                Point newPoint = new Point(totalX / amount, totalY / amount);
                if (getDistance(newPoint, e.CurPoint) > NORMAL)
                {
                    changed = true;
                }
                e.CurPoint = newPoint;
            }
            return changed;
        }

        /// <summary>
        /// 計算兩點間距離
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns>返回距離的平方</returns>
        private double getDistance(Point a, Point b)
        {
            return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y);
        }

    }
}

上面是執行結束之後的結果。總共有3000多個點(我就不把資料貼出來了),大概1S左右執行完畢。

感想:

自己寫程式碼的時候總喜歡把各種東西複雜化,還有很大進步空間。