当前位置:   article > 正文

DBSCAN聚类算法原理(含C++代码)

dbscan

概述

        DBSCAN(density-based spatial clustering)是一种基于密度的聚类算法,在机器学习和数据挖掘领域有广泛的应用,其聚类原理通俗点讲是每个簇类的密度高于该簇类周围的密度,噪声点的密度小于任一簇类的密度。如下图簇类ABC的密度大于周围的密度,噪声的密度低于任一簇类的密度,因此DBSCAN算法也能用于异常点检测。本文对DBSCAN算法进行了详细总结 。

 1. DBSCAN算法的样本点组成

        DBSCAN算法处理后的聚类样本点分为:核心点(core points),边界点(border points)和噪声点(noise),这三类样本点的定义如下:

核心点:对某一数据集D,若样本p的 ε-领域内至少包含MinPts个样本(包括样本p),那么样本p称核心点。即:

称p为核心点,其中 ε-领域 Nε(p)的表达式为:

边界点:对于非核心点的样本b,若b在任意核心点p的ε-领域内,那么样本b称为边界点。即:

称b为边界点。
噪声点:对于非核心点的样本n,若n不在任意核心点p的 ε-领域内,那么样本n称为噪声点。即: 

称n为噪声点。

假设MinPts=4,如下图的核心点、非核心点与噪声的分布:

2. DBSCAN算法原理

        由上节可知,DBSCAN算法划分数据集D为核心点,边界点和噪声点,并按照一定的连接规则组成簇类。介绍连接规则前,先定义下面这几个概念:

        密度直达(directly density-reachable):若q处于p的 ε-邻域内,且p为核心点,则称q由p密度直达;

        密度可达(density-reachable):若q处于p的 ε-邻域内,且p,q均为核心点,则称q的邻域点由p密度可达;

        密度相连(density-connected):若p,q均为非核心点,且p,q处于同一个簇类中,则称q与p密度相连。

下图给出了上述概念的直观显示(MinPts):

        其中核心点E由核心点A密度直达,边界点B由核心点A密度可达,边界点B与边界点C密度相连,N为孤单的噪声点。

        DBSCAN是基于密度的聚类算法,原理为:只要任意两个样本点是密度直达或密度可达的关系,那么该两个样本点归为同一簇类,上图的样本点ABCE为同一簇类。因此,DBSCAN算法从数据集D中随机选择一个核心点作为“种子”,由该种子出发确定相应的聚类簇,当遍历完所有核心点时,算法结束。

        若我们使用的距离是曼哈顿(manhattan)距离,则邻域性状为矩形;若使用的距离是欧拉距离,则邻域形状为圆形。

DBSCAN算法可以抽象为以下几步:

        1)找到每个样本的 ε-邻域内的样本个数,若个数大于等于MinPts,则该样本为核心点;

        2)找到每个核心样本密度直达和密度可达的样本,且该样本亦为核心样本,忽略所有的非核心样本;

        3)若非核心样本在核心样本的 ε-邻域内,则非核心样本为边界样本,反之为噪声。

3. DBSCAN算法的优缺点

优点:

        1)DBSCAN不需要指定簇类的数量;

        2)DBSCAN可以处理任意形状的簇类;

        3)DBSCAN可以检测数据集的噪声,且对数据集中的异常点不敏感;

        4)DBSCAN结果对数据集样本的随机抽样顺序不敏感(细心的读者会发现样本的顺序不一样,结果也可能不一样,如非核心点处于两个聚类的边界,若核心点抽样顺序不同,非核心点归于不同的簇类);

缺点:

        1)DBSCAN最常用的距离度量为欧式距离,对于高维数据集,会带来维度灾难,导致选择合适的ε-值很难;

        2)若不同簇类的样本集密度相差很大,则DBSCAN的聚类效果很差,因为这类数据集导致选择合适的minPts和ε-值非常难,很难适用于所有簇类。

4. C++ 代码实现

main.cpp

 

  1. // 包含DBSCAN算法的头文件
  2. #include"DBSCAN.h"
  3. // 包含输入输出流的头文件
  4. #include<iostream>
  5. // 包含文件流的头文件
  6. #include<fstream>
  7. // 包含字符串的头文件
  8. #include<string>
  9. // 包含PCL库的头文件,用于点云的输入输出、定义点的类型和可视化
  10. #include <pcl/io/pcd_io.h>
  11. #include <pcl/point_types.h>
  12. #include <pcl/visualization/pcl_visualizer.h>
  13. // 使用dbscan命名空间
  14. using namespace dbscan;
  15. // 主函数
  16. int main() {
  17. // 定义一个新的点云对象
  18. pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZRGB>);
  19. // 从文件中加载点云数据
  20. if (pcl::io::loadPCDFile<pcl::PointXYZRGB>("/home/fairlee/unknown_motion_state_landmarks.pcd", *cloud) == -1)
  21. {
  22. // 如果加载失败,输出错误信息并返回-1
  23. PCL_ERROR ("Couldn't read file \n");
  24. return (-1);
  25. }
  26. // 定义一个存储Point3<float>类型点的vector
  27. std::vector<Point3<float>> pointCloud;
  28. // 遍历加载的点云数据,将其添加到pointCloud vector中
  29. for(const auto& point: *cloud)
  30. {
  31. pointCloud.emplace_back(point.x, point.y, point.z);
  32. }
  33. // 使用DBSCAN算法进行聚类
  34. DBSCAN<float> dbscan(0.8f, 25, pointCloud);
  35. std::vector<std::vector<size_t>>cluster = dbscan.GetClusterPointSet();
  36. // 定义一个输出文件流对象
  37. std::ofstream output;
  38. // 遍历每个聚类
  39. for (int i = 0; i < cluster.size(); i++)
  40. {
  41. // 为每个聚类创建一个新的文本文件
  42. output.open("cluster" + std::to_string(i + 1) + ".txt");
  43. // 遍历聚类中的每个点
  44. for (size_t j = 0; j < cluster[i].size(); j++)
  45. {
  46. // 将点的坐标写入文件
  47. output << pointCloud[cluster[i][j]].x << " "
  48. << pointCloud[cluster[i][j]].y << " "
  49. << pointCloud[cluster[i][j]].z << " " << std::endl;
  50. }
  51. // 关闭文件
  52. output.close();
  53. }
  54. // 输出执行成功的消息
  55. std::cout << "执行成功!" << std::endl;
  56. // 创建一个新的可视化对象
  57. pcl::visualization::PCLVisualizer viewer("Cluster Viewer");
  58. // 遍历每个聚类
  59. for (int i = 0; i < cluster.size(); i++)
  60. {
  61. // 创建一个新的点云对象来存储当前聚类的点
  62. pcl::PointCloud<pcl::PointXYZRGB>::Ptr cluster_cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
  63. // 为当前聚类分配一个随机颜色
  64. uint8_t r = static_cast<uint8_t>(rand() % 256);
  65. uint8_t g = static_cast<uint8_t>(rand() % 256);
  66. uint8_t b = static_cast<uint8_t>(rand() % 256);
  67. // 从原始点云中提取当前聚类的点,并将它们添加到新的点云对象中
  68. for (size_t j = 0; j < cluster[i].size(); j++)
  69. {
  70. pcl::PointXYZRGB point;
  71. point.x = pointCloud[cluster[i][j]].x;
  72. point.y = pointCloud[cluster[i][j]].y;
  73. point.z = pointCloud[cluster[i][j]].z;
  74. point.r = r;
  75. point.g = g;
  76. point.b = b;
  77. cluster_cloud->points.push_back(point);
  78. }
  79. // 更新新点云的宽度和高度
  80. cluster_cloud->width = cluster_cloud->points.size();
  81. cluster_cloud->height = 1;
  82. // 为当前聚类创建一个唯一的字符串标识符
  83. std::string cluster_id = "cluster" + std::to_string(i);
  84. // 将当前聚类的点云添加到可视化对象中
  85. viewer.addPointCloud(cluster_cloud, cluster_id);
  86. // 设置点云的渲染属性(例如,点的大小)
  87. viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, cluster_id);
  88. }
  89. // 启动可视化循环
  90. while (!viewer.wasStopped())
  91. {
  92. viewer.spinOnce();
  93. }
  94. // 输出提示信息并等待用户输入,防止程序立即退出
  95. std::cout << "Press enter to continue...";
  96. std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  97. // 返回0表示程序正常退出
  98. return 0;
  99. }

Point3.h

  1. // 防止头文件被多次包含
  2. #ifndef POINT3
  3. #define POINT3
  4. // 定义一个常量表示未分类的点的类别标签
  5. const int NOT_CLASSIFIED = -1;
  6. // 定义一个命名空间dbscan,以便将Point3类与其他类区分开
  7. namespace dbscan {
  8. // 定义一个模板类Point3,T是点的坐标数据类型(例如float或double)
  9. template<typename T>
  10. class Point3
  11. {
  12. public:
  13. // 点的x, y, z坐标
  14. T x, y, z;
  15. // 点所属的聚类的标签
  16. int cluster = NOT_CLASSIFIED;
  17. // 带参数的构造函数,用于初始化点的坐标
  18. Point3(T x, T y, T z) :x(x), y(y), z(z) {
  19. }
  20. // 析构函数
  21. ~Point3() {
  22. }
  23. // 重载减法运算符,用于计算两个点的差
  24. Point3 operator-(const Point3& other) {
  25. return Point3(x - other.x, y - other.y, z - other.z);
  26. }
  27. // 重载加法运算符,用于计算两个点的和
  28. Point3 operator+(const Point3& other) {
  29. return Point3(x + other.x, y + other.y, z + other.z);
  30. }
  31. // 计算当前点到另一个点的平方距离的函数
  32. T getSqaureDistanceTo(const Point3& other) {
  33. T dx = x - other.x;
  34. T dy = y - other.y;
  35. T dz = z - other.z;
  36. return (dx*dx + dy*dy + dz*dz);
  37. }
  38. private:
  39. // 这个类没有私有成员或方法
  40. };
  41. }
  42. // 结束防止头文件被多次包含的预处理块
  43. #endif // !POINT3

PointDataSource.h

  1. // 防止头文件被多次包含
  2. #ifndef POINTDATASOURCE
  3. #define POINTDATASOURCE
  4. // 包含Point3类的定义
  5. #include"Point3.h"
  6. // 包含vector类模板的定义
  7. #include<vector>
  8. // 定义一个命名空间dbscan,以便将PointDataSource类与其他类区分开
  9. namespace dbscan {
  10. // 定义一个模板类PointDataSource,T是点的坐标数据类型(例如float或double)
  11. template<typename T>
  12. class PointDataSource
  13. {
  14. public:
  15. // 构造函数,用于通过Point3类型的指针和点的数量来初始化点云数据
  16. PointDataSource(Point3<T>* ptr, size_t count) :m_ptr(ptr), m_count(count) {
  17. }
  18. // 构造函数,用于通过一个Point3类型的vector来初始化点云数据
  19. PointDataSource(std::vector<Point3<T>>& pointCloud) :m_ptr(&pointCloud[0]), m_count(pointCloud.size()) {
  20. }
  21. // 默认构造函数,将点云数据的指针和数量初始化为nullptr和0
  22. PointDataSource() :m_ptr(nullptr), m_count(0) {
  23. }
  24. // 析构函数
  25. ~PointDataSource(){
  26. }
  27. // 默认的赋值运算符
  28. PointDataSource& operator=(const PointDataSource& other) = default;
  29. // 返回点云中的点的数量
  30. size_t size() {
  31. return m_count;
  32. }
  33. // 重载下标运算符,用于访问点云中的点
  34. Point3<T>& operator[](size_t index){
  35. return m_ptr[index];
  36. }
  37. // 返回指向点云数据开始的指针
  38. const Point3<T>* begin() {
  39. return m_ptr;
  40. }
  41. // 返回指向点云数据结束的指针
  42. const Point3<T>* end() {
  43. return m_ptr + m_count;
  44. }
  45. private:
  46. // 指向点云数据的指针
  47. Point3<T>* m_ptr;
  48. // 点云中的点的数量
  49. size_t m_count;
  50. };
  51. }
  52. // 结束防止头文件被多次包含的预处理块
  53. #endif // ! POINTDATASOURCE

DBSCAN.h

  1. // 防止头文件被多次包含
  2. #ifndef Dbscan
  3. #define Dbscan
  4. #include"Point3.h" // 包含Point3类的定义
  5. #include"PointDataSource.h" // 包含PointDataSource类的定义
  6. #include <pcl/point_types.h> // PCL库中定义的点类型
  7. #include <pcl/kdtree/kdtree_flann.h> // PCL库中的Kd树搜索方法
  8. // 定义一个命名空间dbscan,以便将DBSCAN类与其他类区分开
  9. namespace dbscan {
  10. const int NOISE = -2; // 定义一个常量表示噪声点的类别标签
  11. const int NOT_CLASSIFIED = -1; // 定义一个常量表示未分类点的类别标签
  12. // 定义一个模板类DBSCAN,T是点的数据类型(例如float或double)
  13. template<typename T>
  14. class DBSCAN
  15. {
  16. public:
  17. // 默认构造函数
  18. DBSCAN() = default;
  19. // 带参数的构造函数
  20. // Neighbourhood: 用于确定邻域的半径
  21. // MinPts: 形成一个聚类所需的最小点数
  22. // pointCloud: 输入的点云数据
  23. DBSCAN(float Neighbourhood, int MinPts, std::vector<Point3<T>>& pointCloud)
  24. :Neighbourhood(Neighbourhood), MinPts(MinPts), pointCloud(pointCloud) {
  25. }
  26. // 析构函数
  27. ~DBSCAN() {
  28. }
  29. // 获取聚类结果的函数
  30. std::vector<std::vector<size_t>> GetClusterPointSet() {
  31. std::vector<std::vector<size_t>> cluster; // 存储最终的聚类结果
  32. std::vector<size_t> kernelObj; // 存储核心对象的索引
  33. neighbourPoints.resize(pointCloud.size()); // 调整邻近点的大小与点云大小相同
  34. neighbourDistance.resize(pointCloud.size()); // 调整邻近点距离的大小与点云大小相同
  35. // 选择核心对象并找出它们的邻居
  36. SelectKernelAndNeighbour(kernelObj);
  37. // 迭代标记同一聚类的点
  38. int k = -1; // 初始化聚类数
  39. for (int i = 0; i < kernelObj.size(); i++)
  40. {
  41. if (pointCloud[kernelObj[i]].cluster != NOT_CLASSIFIED)
  42. {
  43. continue;
  44. }
  45. std::vector<T> queue;
  46. queue.push_back(kernelObj[i]);
  47. pointCloud[kernelObj[i]].cluster = ++k;
  48. while (!queue.empty())
  49. {
  50. size_t index = queue.back(); // 弹出最后一个核心对象
  51. queue.pop_back();
  52. if (neighbourPoints[index].size() > MinPts)
  53. {
  54. for (int j = 0; j < neighbourPoints[index].size(); j++)
  55. {
  56. if (k == pointCloud[neighbourPoints[index][j]].cluster)
  57. {
  58. continue;
  59. }
  60. queue.push_back(neighbourPoints[index][j]);
  61. pointCloud[neighbourPoints[index][j]].cluster = k;
  62. }
  63. }
  64. }
  65. }
  66. // 将聚类结果存储在cluster变量中
  67. cluster.resize(k + 1);
  68. for (size_t i = 0; i < pointCloud.size(); i++)
  69. {
  70. if (pointCloud[i].cluster != NOISE)
  71. {
  72. cluster[pointCloud[i].cluster].push_back(i);
  73. }
  74. }
  75. return cluster;
  76. }
  77. private:
  78. float Neighbourhood; // 邻域半径
  79. int MinPts; // 形成聚类的最小点数
  80. PointDataSource<T> pointCloud; // 点云数据
  81. std::vector<std::vector<int>> neighbourPoints; // 存储每个点的邻近点的索引
  82. std::vector<std::vector<T>> neighbourDistance; // 存储每个点到其邻近点的距离
  83. // 选择核心对象并找出它们的邻居的函数
  84. void SelectKernelAndNeighbour(std::vector<size_t>& kernelObj) {
  85. pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
  86. cloud->points.resize(pointCloud.size());
  87. for (size_t i = 0; i < pointCloud.size(); i++)
  88. {
  89. cloud->points[i].x = pointCloud[i].x;
  90. cloud->points[i].y = pointCloud[i].y;
  91. cloud->points[i].z = pointCloud[i].z;
  92. }
  93. pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
  94. kdtree.setInputCloud(cloud);
  95. for (size_t i = 0; i < pointCloud.size(); i++)
  96. {
  97. kdtree.radiusSearch(cloud->points[i], Neighbourhood, neighbourPoints[i], neighbourDistance[i]);
  98. if (neighbourPoints[i].size() >= MinPts)
  99. {
  100. kernelObj.push_back(i);
  101. }
  102. else
  103. {
  104. pointCloud[i].cluster = NOISE;
  105. }
  106. }
  107. }
  108. };
  109. }
  110. #endif // !Dbscan

算法运行结果:

5. MATLAB 代码实现

DBSCAN.m

  1. %DBSCAN聚类函数
  2. %data:点云数据
  3. %radius:搜索半径
  4. %MinPts:最小点数
  5. function clusters=DBSCAN(data,radius,MinPts)
  6. n = size(data,1);
  7. kdtree = KDTreeSearcher(data);
  8. clusters = zeros(n,1);
  9. nearPointIndexs = rangesearch(kdtree,data,radius);
  10. %找出所有符合条件的核心对象
  11. kenelObj = [];
  12. for i=1:size(nearPointIndexs,1)
  13. nearPoints = nearPointIndexs{i};
  14. if length(nearPoints) >= MinPts
  15. kenelObj(end+1) = i;
  16. else
  17. clusters(i) = -1;
  18. end
  19. end
  20. %根据密度进行聚类,-1为噪声点,0代表未定义
  21. classes = 0;
  22. for i=1:length(kenelObj)
  23. if clusters(kenelObj(i)) ~= 0
  24. continue;
  25. end
  26. result = kenelObj(i);
  27. classes = classes+1;
  28. clusters(kenelObj(i)) = classes;
  29. pointer = 1;
  30. fprintf("第%d类开始进行聚类...\n",classes);
  31. %通过广度遍历的方式来进行聚类
  32. while(pointer <= length(result))
  33. index = result(pointer);
  34. nearPoints = nearPointIndexs{index};
  35. if(length(nearPoints) > MinPts)
  36. tmp = clusters(nearPoints) ~= classes;
  37. tmpRes = nearPoints(tmp);
  38. clusters(tmpRes) = classes;
  39. result=[result,tmpRes];
  40. end
  41. pointer = pointer + 1;
  42. end
  43. end
  44. end

main.m

  1. %DBSCAN聚类方法
  2. clc
  3. clear
  4. close all;
  5. %获取点云数据
  6. [fileName,pathName]=uigetfile({'*.pcd';'*.txt'},'Input Data-File'); %选择要进行计算的三维点云数据文件路径
  7. if isempty(fileName) || length(fileName) == 1
  8. fprintf("未选择点云文件!\n");
  9. return;
  10. end
  11. pc = pcread([pathName,fileName]);
  12. Data = pc.Location; %获取点云的位置信息
  13. tic
  14. clusters = DBSCAN(Data,0.4,10);
  15. classes = unique(clusters);
  16. toc
  17. fprintf("共有%d个类别(包含噪声点)\n",length(classes));
  18. figure
  19. hold on
  20. grid on
  21. rotate3d on
  22. for i=1:length(classes)
  23. clusterRes = Data(clusters==classes(i),:);
  24. plot3(clusterRes(:,1),clusterRes(:,2),clusterRes(:,3),'.');
  25. end
  26. title('dbscan聚类');

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/519935
推荐阅读
  

闽ICP备14008679号