WPF實現KMEANS演算法
阿新 • • 發佈:2019-02-17
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左右執行完畢。
感想:
自己寫程式碼的時候總喜歡把各種東西複雜化,還有很大進步空間。