DBSCANで点群データをクラスタリング

# DBSCANとは

DBSCAN（Density-Based Spatial Clustering of Applications with Noise）は、データの密集した領域をクラスタとして認識し、データポイント間の密度に基づいてクラスタリングを行うアルゴリズムです。このアルゴリズムは、クラスタの数を事前に指定する必要がなく、ノイズ（どのクラスタにも属さないデータポイント）の検出が可能です。

### DBSCANの基本概念
DBSCANは2つのパラメータ、$ \epsilon $（eps）と$ \text{minPts} $を使用します。
- **$ \epsilon $（eps）**: あるポイントからこの距離以内にある点をそのポイントの近傍とみなす。
- **$ \text{minPts} $**: クラスタを形成するために必要な近傍点の最小数。

クラスタリングプロセスは以下のステップに従います：
1. 未訪問のポイントをランダムに選択します。
2. 選択したポイントの$ \epsilon $近傍内のポイントを見つけます。
3. 近傍内のポイント数が$ \text{minPts} $未満の場合、このポイントはノイズとしてマークされます。そうでない場合、新しいクラスタが作成されます。
4. 新しいクラスタ内の全ての点に対して、その近傍を見つけ、クラスタに追加します。このステップは、クラスタ内の全ての点の近傍が訪問されるまで続きます。
5. 全ての点が訪問されるまで、ステップ1から4を繰り返します。


### OpenSiv3DにおけるDBSCANの実装例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34295/32a3d258-1f8f-f086-e242-82a69760ed87.png)


```cpp
# include <Siv3D.hpp>


struct Cluster {
	Array<size_t> indices;
};

using DBSCANResult = Array<Cluster>;

template <typename PointType>
Array<size_t> RegionQuery(const Array<PointType>& points, size_t index, double eps) {
	Array<size_t> neighbors;
	for (size_t i = 0; i < points.size(); ++i) {
		if (i == index) continue;
		if (points[i].distanceFrom(points[index]) <= eps) {
			neighbors << i;
		}
	}
	return neighbors;
}

template <typename PointType>
DBSCANResult DBSCAN(const Array<PointType>& points, double eps, size_t minPts) {
	Array<bool> visited(points.size(), false);
	DBSCANResult clusters;

	for (size_t i = 0; i < points.size(); ++i) {
		if (visited[i]) continue;
		visited[i] = true;

		Array<size_t> neighbors = RegionQuery(points, i, eps);

		if (neighbors.size() < minPts) continue;

		Cluster cluster;
		cluster.indices << i;

		for (size_t j = 0; j < neighbors.size(); ++j) {
			size_t pointIdx = neighbors[j];
			if (!visited[pointIdx]) {
				visited[pointIdx] = true;
				Array<size_t> pointNeighbors = RegionQuery(points, pointIdx, eps);
				if (pointNeighbors.size() >= minPts) {
					neighbors.append(pointNeighbors);
				}
			}
			if (!cluster.indices.includes(pointIdx)) {
				cluster.indices << pointIdx;
			}
		}
		clusters << cluster;
	}
	return clusters;
}


// 正規分布に基づいたVec2を生成する関数
Vec2 GenerateGaussianVec2(const Vec2& mean, double stddev, DefaultRNG& rng)
{
	NormalDistribution dist{ 0.0, stddev };
	double x = mean.x + dist(rng);
	double y = mean.y + dist(rng);
	return Vec2(x, y);
}

void DrawClusters(const Array<Vec2>& points, const DBSCANResult& result, const Font& font, double radius = 20.0)
{
	for (size_t i = 0; i < result.size(); ++i)
	{
		const Color col = HSV{ (i * 60), 0.5, 0.5 };
		for (const auto& index : result[i].indices)
		{
			Circle{ points[index], radius }.draw(col);
		}
	}
}

void DrawClusterMeans(const Array<Vec2>& points, const DBSCANResult& result, const Font& font)
{
	for (size_t i = 0; i < result.size(); ++i)
	{
		Vec2 mean;
		for (const auto& index : result[i].indices)
		{
			mean += points[index];
		}
		mean /= result[i].indices.size();
		mean.asCircle(40).draw(ColorF{ 0.95 }).drawFrame(2, ColorF{ 0.11 });
		font(i).drawAt(40, mean, ColorF{ 0.11 });
	}
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Rect canvasArea(0, 0, 600, 600);
	const Rect guiArea(620, 0, 200, 600);
	const Font font{ FontMethod::MSDF, 36, Typeface::Bold };

	Array<Vec2> points;
	auto& rng = GetDefaultRNG();
	DBSCANResult result;
	bool autoDBSCAN = true;
	double stddev = 10.0;
	double eps = 20.0;
	int minpts = 5;

	while (System::Update())
	{
		if (MouseL.pressed() && canvasArea.contains(Cursor::Pos()))
		{
			const Vec2 basePos = Cursor::PosF();
			for (size_t i = 0; i < 5; ++i)
			{
				const Vec2 pos = GenerateGaussianVec2(basePos, stddev, rng);
				points << pos;
			}
			if (autoDBSCAN)
			{
				result = DBSCAN(points, eps, minpts);
			}
		}

		if (SimpleGUI::CheckBox(autoDBSCAN, U"Auto DBSCAN", Vec2{ 620, 40 })
			|| SimpleGUI::Slider(U"eps:{:.0f}"_fmt(eps), eps, 10.0, 100.0, Vec2{ 620, 90 }, 80.0, 100.0))
		{
			result = DBSCAN(points, eps, minpts);
		}

		if (SimpleGUI::Button(U"Clear", Vec2{ 620, 140 }))
		{
			result.clear();
			points.clear();
		}

		canvasArea.draw(Palette::Gray);

		DrawClusters(points, result, font, eps);

		for (const auto& point : points)
		{
			Circle{ point, 2.0 }.draw(Palette::White);
		}

		DrawClusterMeans(points, result, font);
	}
}

```

提示されたコードは、OpenSiv3Dライブラリを用いた2次元空間内の点群に対するDBSCANの実装です。以下はその主要な関数と役割です：

- `RegionQuery`: 与えられた点の$ \epsilon $近傍内の全ての点のインデックスを返します。
- `DBSCAN`: DBSCANアルゴリズムを実行し、見つかったクラスタのリストを返します。
- `GenerateGaussianVec2`: 正規分布に基づいてランダムな点を生成します。
- `DrawClusters` と `DrawClusterMeans`: クラスタとクラスタの重心を描画します。

ユーザーはマウスを使用してクリックすることで、新しいデータポイントを生成し、`autoDBSCAN`が`true`の場合、自動的にDBSCANアルゴリズムが実行されてクラスタリングを行います。

この実装は、点群データが手に入った際、例えばLiDARやRealsenseを使用した3Dスキャンから得られるデータにDBSCANを適用し、車、木、人などの異なる物体をクラスタとして識別するのに役立ちます。また、画像データなど他のタイプの情報にも適用可能です。

# 参考資料

https://jp.mathworks.com/help/stats/dbscan-clustering.html


https://ja.wikipedia.org/wiki/DBSCAN#:~:text=DBSCAN%20%E3%81%AF%E6%9C%80%E3%82%82%E4%B8%80%E8%88%AC%E7%9A%84,%E3%82%89%E3%82%8C%E3%82%8B%E8%B3%9E%EF%BC%89%E3%82%92%E5%8F%97%E8%B3%9E%E3%81%97%E3%81%9F%E3%80%82

https://wakame-msds.com/dbscan/#:~:text=DBSCAN-,%E6%A6%82%E8%A6%81,%E8%A6%8B%E3%81%A4%E3%81%91%E3%80%81%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B%E6%89%8B%E6%B3%95%E3%81%A7%E3%81%99%E3%80%82

https://shoblog.iiyan.net/data-science/machine-learning/density-based-spatial-clustering-of-applications-with-noise/#:~:text=DBSCAN%E3%82%92%E8%A1%8C%E3%81%86%E5%A0%B4%E5%90%88%E3%81%AE%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%81%AF%E4%BB%A5%E4%B8%8B%E3%81%AE3%E7%82%B9%E3%81%A7%E3%81%99%E3%80%82%20*%20%E8%A8%88%E7%AE%97%E3%82%B3%E3%82%B9%E3%83%88%E3%81%8C%E9%AB%98%E3%81%84%20*%20%E5%AF%86%E5%BA%A6%E3%81%8C%E7%95%B0%E3%81%AA%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AB%E9%81%A9%E7%94%A8%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%20*%20%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%8C%E9%9B%A3%E3%81%97%E3%81%84