小赖子的英国生活和资讯

机器学习系列之: 怎么样数鸡鸡? (3) 分类 Clustering

阅读 桌面完整版
machine-learning 机器学习系列之: 怎么样数鸡鸡?  (3) 分类 Clustering I.T. 学习笔记 数据结构与算法

machine-learning

本数鸡系列:

这已经是3个月前的事了, 后来懒癌, 加收益变少没动力, 于是一直搁放着, 今天突然想起, 有点时间变想把这事完结了.

上次说到, 通过大津算法 Otsu’s Method计算出一个阀值, 然后就可以把图片变成黑白的, 比如白的是鸡, 黑的是空气.

我们就可以遍历这张黑白图片的每个相素点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 遍历图片的每个相素点
/// </summary>
/// <param name="binImage">黑白图片</param>
private static void ClusterAllPoints(Bitmap binImage)
{
    for (int i = 0; i < binImage.Width; i++)
    {
        for (int j = 0; j < binImage.Height; j++)
        {
            if (binImage.GetPixel(i, j).ToArgb() == Color.White.ToArgb())
            {
                ClusterPoint(new Point(i, j));
            }
        }
    }
}
/// <summary>
/// 遍历图片的每个相素点
/// </summary>
/// <param name="binImage">黑白图片</param>
private static void ClusterAllPoints(Bitmap binImage)
{
    for (int i = 0; i < binImage.Width; i++)
    {
        for (int j = 0; j < binImage.Height; j++)
        {
            if (binImage.GetPixel(i, j).ToArgb() == Color.White.ToArgb())
            {
                ClusterPoint(new Point(i, j));
            }
        }
    }
}

这里会用到 ClusterPoint 这个方法对于当前点进行分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/// <summary>
/// KNN 算法
/// </summary>
/// <param name="p">相素点</param>
private static void ClusterPoint(Point p)
{
    List<Point> chosenCluster = null;
    double votesCast = 0d;
 
    // 如果还没有发现任何 Clusters 也就是第一个相素点
    if (Clusters.Count == 0)
    {
        List<Point> l = new List<Point>();
        l.Add(p);
        Clusters.Add(l);
    }
    else
    {
        // 否则我们需要遍历当前所有的 类
        Clusters.ForEach(cluster =>
        {
            // 找到和这个 点在规定阀值内的所有点
            List<Point> votingPoints = cluster.FindAll(point =>
                IsCloseTo(point, p));
 
            // 然后需要把这些点的投票权重加起来
            double totalVotes =
                votingPoints.AsParallel().Sum(
                    aPoint => CalculateVoteOfPoint(aPoint, p));
 
            // 如果投票总值比当前的大, 那么更新选定类别
            if (totalVotes > votesCast)
            {
                votesCast = totalVotes;
                chosenCluster = cluster;
            }
        });
 
        // 把这一点加到选定的类别中
        if (chosenCluster != null)
        {
            chosenCluster.Add(p);
        }
        else
        {
            // 如果找不到类别, 那么新创建一个并添加该点. 
            List<Point> l = new List<Point>();
            l.Add(p);
            Clusters.Add(l);
        }
    }
}
/// <summary>
/// KNN 算法
/// </summary>
/// <param name="p">相素点</param>
private static void ClusterPoint(Point p)
{
    List<Point> chosenCluster = null;
    double votesCast = 0d;

    // 如果还没有发现任何 Clusters 也就是第一个相素点
    if (Clusters.Count == 0)
    {
        List<Point> l = new List<Point>();
        l.Add(p);
        Clusters.Add(l);
    }
    else
    {
        // 否则我们需要遍历当前所有的 类
        Clusters.ForEach(cluster =>
        {
            // 找到和这个 点在规定阀值内的所有点
            List<Point> votingPoints = cluster.FindAll(point =>
                IsCloseTo(point, p));

            // 然后需要把这些点的投票权重加起来
            double totalVotes =
                votingPoints.AsParallel().Sum(
                    aPoint => CalculateVoteOfPoint(aPoint, p));

            // 如果投票总值比当前的大, 那么更新选定类别
            if (totalVotes > votesCast)
            {
                votesCast = totalVotes;
                chosenCluster = cluster;
            }
        });

        // 把这一点加到选定的类别中
        if (chosenCluster != null)
        {
            chosenCluster.Add(p);
        }
        else
        {
            // 如果找不到类别, 那么新创建一个并添加该点. 
            List<Point> l = new List<Point>();
            l.Add(p);
            Clusters.Add(l);
        }
    }
}

计算每个点的投票值, 我们可以用距离来算, 距离越远, 那么它对当前点的投票权重就越低.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// <summary>
/// 两点之间的 Euclidean 距离
/// </summary>
/// <param name="p1">第一个点</param>
/// <param name="p2">第二个点</param>
/// <returns></returns>
private static double EuclideanDistanceBetween(Point p1, Point p2)
{
    return Math.Sqrt(
        Math.Pow((p1.X - p2.X), 2) +
        Math.Pow((p1.Y - p2.Y), 2));
}
 
/// <summary>
/// 根据 Euclidean 距离来计算权重
/// </summary>
/// <param name="neighbour"></param>
/// <param name="candidate"></param>
/// <returns></returns>
private static double CalculateVoteOfPoint(
    Point neighbour,
    Point candidate)
{
    return 1 / EuclideanDistanceBetween(neighbour, candidate);
}
/// <summary>
/// 两点之间的 Euclidean 距离
/// </summary>
/// <param name="p1">第一个点</param>
/// <param name="p2">第二个点</param>
/// <returns></returns>
private static double EuclideanDistanceBetween(Point p1, Point p2)
{
    return Math.Sqrt(
        Math.Pow((p1.X - p2.X), 2) +
        Math.Pow((p1.Y - p2.Y), 2));
}

/// <summary>
/// 根据 Euclidean 距离来计算权重
/// </summary>
/// <param name="neighbour"></param>
/// <param name="candidate"></param>
/// <returns></returns>
private static double CalculateVoteOfPoint(
    Point neighbour,
    Point candidate)
{
    return 1 / EuclideanDistanceBetween(neighbour, candidate);
}

这样一来, 我们就可以得到几个类别, 不过我们需要进一步提高精度, 因为有些白色的点是属于背景噪音, 比如我们可以假定, 小于10个相素点的类别不算鸡(可能是毛啊啥的)

1
2
3
4
5
6
7
8
9
/// <summary>
/// 去掉背景噪音
/// </summary>
/// <param name="noiseThreshold">The threshold</param>
private static void CullNoiseClusters(int noiseThreshold)
{
    Clusters.RemoveAll(cluster =>
        cluster.Count < noiseThreshold);
}
/// <summary>
/// 去掉背景噪音
/// </summary>
/// <param name="noiseThreshold">The threshold</param>
private static void CullNoiseClusters(int noiseThreshold)
{
    Clusters.RemoveAll(cluster =>
        cluster.Count < noiseThreshold);
}

还有就是, 有可能两个类别的中心靠得太近了, 所以我们需要合并两个非常近的鸡, 有可能是鸡展开翅膀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// <summary>
/// 如果中心太靠近, 合并两个类
/// </summary>
private static void MergeClusters()
{
    List<List<Point>> delList = new List<List<Point>>();
    List<Point>[] c = Clusters.ToArray();
    int ptr1 = 0;
    int ptr2 = 1;
 
    while (ptr1 < c.Length)
    {
        while (ptr2 < c.Length)
        {
            List<Point> l1 = c[ptr1];
            List<Point> l2 = c[ptr2];
            Point ctr1 = GetClusterCentrePoint(l1);
            Point ctr2 = GetClusterCentrePoint(l2);
            if (EuclideanDistanceBetween(ctr1, ctr2) < MergeThreshold)
            {
                l1.AddRange(l2);
                delList.Add(l2);
            }
            ptr2++;
        }
        ptr1++;
        ptr2 = ptr1 + 1;
    }
    delList.ForEach(list => Clusters.Remove(list));
}
/// <summary>
/// 如果中心太靠近, 合并两个类
/// </summary>
private static void MergeClusters()
{
    List<List<Point>> delList = new List<List<Point>>();
    List<Point>[] c = Clusters.ToArray();
    int ptr1 = 0;
    int ptr2 = 1;

    while (ptr1 < c.Length)
    {
        while (ptr2 < c.Length)
        {
            List<Point> l1 = c[ptr1];
            List<Point> l2 = c[ptr2];
            Point ctr1 = GetClusterCentrePoint(l1);
            Point ctr2 = GetClusterCentrePoint(l2);
            if (EuclideanDistanceBetween(ctr1, ctr2) < MergeThreshold)
            {
                l1.AddRange(l2);
                delList.Add(l2);
            }
            ptr2++;
        }
        ptr1++;
        ptr2 = ptr1 + 1;
    }
    delList.ForEach(list => Clusters.Remove(list));
}

类取中心的方法很简单:

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 返回每个鸡类的中心点
/// </summary>
/// <param name="cluster">类别</param>
/// <returns>The centre point</returns>
private static Point GetClusterCentrePoint(List<Point> cluster)
{
    return new Point(
        (int)cluster.AsParallel().Average(p => p.X),
        (int)cluster.AsParallel().Average(p => p.Y));
}
/// <summary>
/// 返回每个鸡类的中心点
/// </summary>
/// <param name="cluster">类别</param>
/// <returns>The centre point</returns>
private static Point GetClusterCentrePoint(List<Point> cluster)
{
    return new Point(
        (int)cluster.AsParallel().Average(p => p.X),
        (int)cluster.AsParallel().Average(p => p.Y));
}

这样, 数鸡就完成了:

1
2
3
4
// 数了多少只? 
Console.WriteLine(
    "\nI 发现 {0} 只鸡 ",
    Clusters.Count);
// 数了多少只? 
Console.WriteLine(
    "\nI 发现 {0} 只鸡 ",
    Clusters.Count);

强烈推荐

微信公众号: 小赖子的英国生活和资讯 JustYYUK

阅读 桌面完整版
Exit mobile version