DBSCAN(density-based spatial clustering)是一种基于密度的聚类算法,在机器学习和数据挖掘领域有广泛的应用,其聚类原理通俗点讲是每个簇类的密度高于该簇类周围的密度,噪声点的密度小于任一簇类的密度。如下图簇类ABC的密度大于周围的密度,噪声的密度低于任一簇类的密度,因此DBSCAN算法也能用于异常点检测。本文对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,如下图的核心点、非核心点与噪声的分布:
由上节可知,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)若非核心样本在核心样本的 ε-邻域内,则非核心样本为边界样本,反之为噪声。
优点:
1)DBSCAN不需要指定簇类的数量;
2)DBSCAN可以处理任意形状的簇类;
3)DBSCAN可以检测数据集的噪声,且对数据集中的异常点不敏感;
4)DBSCAN结果对数据集样本的随机抽样顺序不敏感(细心的读者会发现样本的顺序不一样,结果也可能不一样,如非核心点处于两个聚类的边界,若核心点抽样顺序不同,非核心点归于不同的簇类);
缺点:
1)DBSCAN最常用的距离度量为欧式距离,对于高维数据集,会带来维度灾难,导致选择合适的ε-值很难;
2)若不同簇类的样本集密度相差很大,则DBSCAN的聚类效果很差,因为这类数据集导致选择合适的minPts和ε-值非常难,很难适用于所有簇类。
main.cpp
// 包含DBSCAN算法的头文件
#include"DBSCAN.h"
// 包含输入输出流的头文件
#include
// 包含文件流的头文件
#include
// 包含字符串的头文件
#include
// 包含PCL库的头文件,用于点云的输入输出、定义点的类型和可视化
#include
#include
#include
// 使用dbscan命名空间
using namespace dbscan;
// 主函数
int main() {
// 定义一个新的点云对象
pcl::PointCloud::Ptr cloud (new pcl::PointCloud);
// 从文件中加载点云数据
if (pcl::io::loadPCDFile("/home/fairlee/unknown_motion_state_landmarks.pcd", *cloud) == -1)
{
// 如果加载失败,输出错误信息并返回-1
PCL_ERROR ("Couldn't read file \n");
return (-1);
}
// 定义一个存储Point3类型点的vector
std::vector> pointCloud;
// 遍历加载的点云数据,将其添加到pointCloud vector中
for(const auto& point: *cloud)
{
pointCloud.emplace_back(point.x, point.y, point.z);
}
// 使用DBSCAN算法进行聚类
DBSCAN dbscan(0.8f, 25, pointCloud);
std::vector>cluster = dbscan.GetClusterPointSet();
// 定义一个输出文件流对象
std::ofstream output;
// 遍历每个聚类
for (int i = 0; i < cluster.size(); i++)
{
// 为每个聚类创建一个新的文本文件
output.open("cluster" + std::to_string(i + 1) + ".txt");
// 遍历聚类中的每个点
for (size_t j = 0; j < cluster[i].size(); j++)
{
// 将点的坐标写入文件
output << pointCloud[cluster[i][j]].x << " "
<< pointCloud[cluster[i][j]].y << " "
<< pointCloud[cluster[i][j]].z << " " << std::endl;
}
// 关闭文件
output.close();
}
// 输出执行成功的消息
std::cout << "执行成功!" << std::endl;
// 创建一个新的可视化对象
pcl::visualization::PCLVisualizer viewer("Cluster Viewer");
// 遍历每个聚类
for (int i = 0; i < cluster.size(); i++)
{
// 创建一个新的点云对象来存储当前聚类的点
pcl::PointCloud::Ptr cluster_cloud(new pcl::PointCloud);
// 为当前聚类分配一个随机颜色
uint8_t r = static_cast(rand() % 256);
uint8_t g = static_cast(rand() % 256);
uint8_t b = static_cast(rand() % 256);
// 从原始点云中提取当前聚类的点,并将它们添加到新的点云对象中
for (size_t j = 0; j < cluster[i].size(); j++)
{
pcl::PointXYZRGB point;
point.x = pointCloud[cluster[i][j]].x;
point.y = pointCloud[cluster[i][j]].y;
point.z = pointCloud[cluster[i][j]].z;
point.r = r;
point.g = g;
point.b = b;
cluster_cloud->points.push_back(point);
}
// 更新新点云的宽度和高度
cluster_cloud->width = cluster_cloud->points.size();
cluster_cloud->height = 1;
// 为当前聚类创建一个唯一的字符串标识符
std::string cluster_id = "cluster" + std::to_string(i);
// 将当前聚类的点云添加到可视化对象中
viewer.addPointCloud(cluster_cloud, cluster_id);
// 设置点云的渲染属性(例如,点的大小)
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, cluster_id);
}
// 启动可视化循环
while (!viewer.wasStopped())
{
viewer.spinOnce();
}
// 输出提示信息并等待用户输入,防止程序立即退出
std::cout << "Press enter to continue...";
std::cin.ignore(std::numeric_limits::max(), '\n');
// 返回0表示程序正常退出
return 0;
}
Point3.h
// 防止头文件被多次包含
#ifndef POINT3
#define POINT3
// 定义一个常量表示未分类的点的类别标签
const int NOT_CLASSIFIED = -1;
// 定义一个命名空间dbscan,以便将Point3类与其他类区分开
namespace dbscan {
// 定义一个模板类Point3,T是点的坐标数据类型(例如float或double)
template
class Point3
{
public:
// 点的x, y, z坐标
T x, y, z;
// 点所属的聚类的标签
int cluster = NOT_CLASSIFIED;
// 带参数的构造函数,用于初始化点的坐标
Point3(T x, T y, T z) :x(x), y(y), z(z) {
}
// 析构函数
~Point3() {
}
// 重载减法运算符,用于计算两个点的差
Point3 operator-(const Point3& other) {
return Point3(x - other.x, y - other.y, z - other.z);
}
// 重载加法运算符,用于计算两个点的和
Point3 operator+(const Point3& other) {
return Point3(x + other.x, y + other.y, z + other.z);
}
// 计算当前点到另一个点的平方距离的函数
T getSqaureDistanceTo(const Point3& other) {
T dx = x - other.x;
T dy = y - other.y;
T dz = z - other.z;
return (dx*dx + dy*dy + dz*dz);
}
private:
// 这个类没有私有成员或方法
};
}
// 结束防止头文件被多次包含的预处理块
#endif // !POINT3
PointDataSource.h
// 防止头文件被多次包含
#ifndef POINTDATASOURCE
#define POINTDATASOURCE
// 包含Point3类的定义
#include"Point3.h"
// 包含vector类模板的定义
#include
// 定义一个命名空间dbscan,以便将PointDataSource类与其他类区分开
namespace dbscan {
// 定义一个模板类PointDataSource,T是点的坐标数据类型(例如float或double)
template
class PointDataSource
{
public:
// 构造函数,用于通过Point3类型的指针和点的数量来初始化点云数据
PointDataSource(Point3* ptr, size_t count) :m_ptr(ptr), m_count(count) {
}
// 构造函数,用于通过一个Point3类型的vector来初始化点云数据
PointDataSource(std::vector>& pointCloud) :m_ptr(&pointCloud[0]), m_count(pointCloud.size()) {
}
// 默认构造函数,将点云数据的指针和数量初始化为nullptr和0
PointDataSource() :m_ptr(nullptr), m_count(0) {
}
// 析构函数
~PointDataSource(){
}
// 默认的赋值运算符
PointDataSource& operator=(const PointDataSource& other) = default;
// 返回点云中的点的数量
size_t size() {
return m_count;
}
// 重载下标运算符,用于访问点云中的点
Point3& operator[](size_t index){
return m_ptr[index];
}
// 返回指向点云数据开始的指针
const Point3* begin() {
return m_ptr;
}
// 返回指向点云数据结束的指针
const Point3* end() {
return m_ptr + m_count;
}
private:
// 指向点云数据的指针
Point3* m_ptr;
// 点云中的点的数量
size_t m_count;
};
}
// 结束防止头文件被多次包含的预处理块
#endif // ! POINTDATASOURCE
DBSCAN.h
// 防止头文件被多次包含
#ifndef Dbscan
#define Dbscan
#include"Point3.h" // 包含Point3类的定义
#include"PointDataSource.h" // 包含PointDataSource类的定义
#include // PCL库中定义的点类型
#include // PCL库中的Kd树搜索方法
// 定义一个命名空间dbscan,以便将DBSCAN类与其他类区分开
namespace dbscan {
const int NOISE = -2; // 定义一个常量表示噪声点的类别标签
const int NOT_CLASSIFIED = -1; // 定义一个常量表示未分类点的类别标签
// 定义一个模板类DBSCAN,T是点的数据类型(例如float或double)
template
class DBSCAN
{
public:
// 默认构造函数
DBSCAN() = default;
// 带参数的构造函数
// Neighbourhood: 用于确定邻域的半径
// MinPts: 形成一个聚类所需的最小点数
// pointCloud: 输入的点云数据
DBSCAN(float Neighbourhood, int MinPts, std::vector>& pointCloud)
:Neighbourhood(Neighbourhood), MinPts(MinPts), pointCloud(pointCloud) {
}
// 析构函数
~DBSCAN() {
}
// 获取聚类结果的函数
std::vector> GetClusterPointSet() {
std::vector> cluster; // 存储最终的聚类结果
std::vector kernelObj; // 存储核心对象的索引
neighbourPoints.resize(pointCloud.size()); // 调整邻近点的大小与点云大小相同
neighbourDistance.resize(pointCloud.size()); // 调整邻近点距离的大小与点云大小相同
// 选择核心对象并找出它们的邻居
SelectKernelAndNeighbour(kernelObj);
// 迭代标记同一聚类的点
int k = -1; // 初始化聚类数
for (int i = 0; i < kernelObj.size(); i++)
{
if (pointCloud[kernelObj[i]].cluster != NOT_CLASSIFIED)
{
continue;
}
std::vector queue;
queue.push_back(kernelObj[i]);
pointCloud[kernelObj[i]].cluster = ++k;
while (!queue.empty())
{
size_t index = queue.back(); // 弹出最后一个核心对象
queue.pop_back();
if (neighbourPoints[index].size() > MinPts)
{
for (int j = 0; j < neighbourPoints[index].size(); j++)
{
if (k == pointCloud[neighbourPoints[index][j]].cluster)
{
continue;
}
queue.push_back(neighbourPoints[index][j]);
pointCloud[neighbourPoints[index][j]].cluster = k;
}
}
}
}
// 将聚类结果存储在cluster变量中
cluster.resize(k + 1);
for (size_t i = 0; i < pointCloud.size(); i++)
{
if (pointCloud[i].cluster != NOISE)
{
cluster[pointCloud[i].cluster].push_back(i);
}
}
return cluster;
}
private:
float Neighbourhood; // 邻域半径
int MinPts; // 形成聚类的最小点数
PointDataSource pointCloud; // 点云数据
std::vector> neighbourPoints; // 存储每个点的邻近点的索引
std::vector> neighbourDistance; // 存储每个点到其邻近点的距离
// 选择核心对象并找出它们的邻居的函数
void SelectKernelAndNeighbour(std::vector& kernelObj) {
pcl::PointCloud::Ptr cloud(new pcl::PointCloud);
cloud->points.resize(pointCloud.size());
for (size_t i = 0; i < pointCloud.size(); i++)
{
cloud->points[i].x = pointCloud[i].x;
cloud->points[i].y = pointCloud[i].y;
cloud->points[i].z = pointCloud[i].z;
}
pcl::KdTreeFLANN kdtree;
kdtree.setInputCloud(cloud);
for (size_t i = 0; i < pointCloud.size(); i++)
{
kdtree.radiusSearch(cloud->points[i], Neighbourhood, neighbourPoints[i], neighbourDistance[i]);
if (neighbourPoints[i].size() >= MinPts)
{
kernelObj.push_back(i);
}
else
{
pointCloud[i].cluster = NOISE;
}
}
}
};
}
#endif // !Dbscan
算法运行结果:
DBSCAN.m
%DBSCAN聚类函数
%data:点云数据
%radius:搜索半径
%MinPts:最小点数
function clusters=DBSCAN(data,radius,MinPts)
n = size(data,1);
kdtree = KDTreeSearcher(data);
clusters = zeros(n,1);
nearPointIndexs = rangesearch(kdtree,data,radius);
%找出所有符合条件的核心对象
kenelObj = [];
for i=1:size(nearPointIndexs,1)
nearPoints = nearPointIndexs{i};
if length(nearPoints) >= MinPts
kenelObj(end+1) = i;
else
clusters(i) = -1;
end
end
%根据密度进行聚类,-1为噪声点,0代表未定义
classes = 0;
for i=1:length(kenelObj)
if clusters(kenelObj(i)) ~= 0
continue;
end
result = kenelObj(i);
classes = classes+1;
clusters(kenelObj(i)) = classes;
pointer = 1;
fprintf("第%d类开始进行聚类...\n",classes);
%通过广度遍历的方式来进行聚类
while(pointer <= length(result))
index = result(pointer);
nearPoints = nearPointIndexs{index};
if(length(nearPoints) > MinPts)
tmp = clusters(nearPoints) ~= classes;
tmpRes = nearPoints(tmp);
clusters(tmpRes) = classes;
result=[result,tmpRes];
end
pointer = pointer + 1;
end
end
end
main.m
%DBSCAN聚类方法
clc
clear
close all;
%获取点云数据
[fileName,pathName]=uigetfile({'*.pcd';'*.txt'},'Input Data-File'); %选择要进行计算的三维点云数据文件路径
if isempty(fileName) || length(fileName) == 1
fprintf("未选择点云文件!\n");
return;
end
pc = pcread([pathName,fileName]);
Data = pc.Location; %获取点云的位置信息
tic
clusters = DBSCAN(Data,0.4,10);
classes = unique(clusters);
toc
fprintf("共有%d个类别(包含噪声点)\n",length(classes));
figure
hold on
grid on
rotate3d on
for i=1:length(classes)
clusterRes = Data(clusters==classes(i),:);
plot3(clusterRes(:,1),clusterRes(:,2),clusterRes(:,3),'.');
end
title('dbscan聚类');