これは何?
DBSCAN(Density-Based Spatial Clustering of Applications with Noise) は、「密度の高い領域」を自動的にクラスターとして検出するアルゴリズムだ。
森林LiDARでの期待される使いどころ:
- 個木のセグメンテーション(1本ずつに点群を分ける)
- 地物の種類ごとのグループ化(木・建物・電柱)
- ノイズ点の自動除去
この記事では「DBSCANで御岳山の木を1本ずつ数えられるか」を実際に試し、詰まったところも含めてそのまま書く。
アルゴリズムの流れ
各点について:
eps 以内に min_samples 点以上 → コア点(クラスターの核)
コア点に隣接している → ボーダー点(クラスターの縁)
どちらでもない → ノイズ点(ラベル = -1)
eps(イプシロン)は近傍半径を意味します。
k-means との最大の違いは2つ:
| 項目 | k-means | DBSCAN |
|---|---|---|
| クラスター数の指定 | 必要 | 不要 |
| クラスターの形 | 球形のみ | 任意の形 |
| ノイズ処理 | なし | ノイズ点を明示的に除外 |
パラメータの決め方 — k近傍距離グラフ
eps の目安は k近傍距離グラフ(エルボー法) で探す。
from scipy.spatial import cKDTree
import numpy as np
import matplotlib.pyplot as plt
k = 10
tree = cKDTree(pts[:, :2])
dists, _ = tree.query(pts[:, :2], k=k + 1)
kth_dist = np.sort(dists[:, k])
plt.plot(kth_dist)
plt.xlabel('点のインデックス(距離でソート)')
plt.ylabel(f'k={k} 番目の近傍距離 (m)')
plt.show()
グラフが急に立ち上がる「肘」の部分が適切な eps の目安になる。
実装
from sklearn.cluster import DBSCAN
labels = DBSCAN(eps=3.0, min_samples=5).fit(pts[:, :2]).labels_
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = (labels == -1).sum()
print(f'クラスター数: {n_clusters}')
print(f'ノイズ点数: {n_noise} ({n_noise/len(labels)*100:.1f}%)')
labels_ の値の意味:
-
0, 1, 2, …: 各クラスターのID -
-1: ノイズ点
御岳山データで個木分割を試みる
データの準備
import laspy
las = laspy.read('09KC7495.las')
classification = np.array(las.classification)
xyz = np.vstack([las.x, las.y, las.z]).T
# DSM点(class 1, 9)を抽出 → ボクセル化 → 外れ値除去
dsm_pts = xyz[np.isin(classification, [1, 9])]
# ...(ボクセル化・外れ値除去処理)
# clean: 45,664点、400m × 300m の御岳山
# 樹冠部の点のみに絞る(絶対標高の中央値 + 5m 以上)
z_median = np.median(clean[:, 2])
crown_mask = clean[:, 2] > z_median + 5
crown_pts = clean[crown_mask] # 19,740点
別の方法で検出した樹木本数は 941本 を基準値にすると、明らかにうまく分けることができていない。
実験①: eps を変えてみる(min_samples=5 固定)
for eps in [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7]:
labels = DBSCAN(eps=eps, min_samples=5).fit(crown_pts[:, :2]).labels_
nc = len(set(labels)) - (1 if -1 in labels else 0)
print(eps, nc)
1.0 533
1.1 616
1.2 689
1.3 729 ← ピーク
1.4 722
1.5 712
1.6 698
1.7 649
eps=1.3 でクラスター数がピークになる。これは:
eps が小さすぎる(< 1.2)
→ min_samples=5 を満たせずノイズ落ちする点が増える
→ クラスター数が減る
eps=1.3 ← ちょうど1本の木の樹冠点が5点集まれるギリギリ
eps が大きすぎる(> 1.3)
→ 隣の木の樹冠点と繋がってマージし始める
→ クラスター数が減る
ただし 729 本は基準の 941 本を 22% 下回る。
実験②: min_samples を変えてみる(eps=1.3 固定)
for ms in [2, 3, 4, 5]:
labels = DBSCAN(eps=1.3, min_samples=ms).fit(crown_pts[:, :2]).labels_
nc = len(set(labels)) - (1 if -1 in labels else 0)
noise = (labels == -1).mean() * 100
print(f'min_samples={ms}: {nc}本, ノイズ={noise:.1f}%')
min_samples=2: 2574本, ノイズ=13.4%
min_samples=3: 1578本, ノイズ=23.5%
min_samples=4: 1067本, ノイズ=37.7%
min_samples=5: 729本, ノイズ=51.8%
min_samples=4 のとき 1067 本で基準に最も近い。しかし ノイズ率 37.7% が気になる。
ノイズ率が示すこと
min_samples=5 でノイズ率 51.8% = 樹冠点の半分以上がどのクラスターにも属せない。
計算してみると:
crown_pts ÷ 941本 ≈ 21点/本(平均)
しかし点は樹冠全体に広がっているため、
eps=1.3m の半径内(面積≈5.3m²)に集まるのは平均 3〜4点
→ min_samples=5 の壁を多くの木が超えられない
eps と min_samples を何度調整しても、この構造的な問題は残る。
改善案: 生点群ではなく CHM ピクセルを使う
「高さ情報を2Dに畳み込んだ CHM(樹高ラスター)」のピクセルを点として使えば、密度が均一になる。
import rasterio
with rasterio.open('../data/processed/height_map.tif') as src:
chm = src.read(1)
transform = src.transform
print(f'解像度: {src.res}') # (1.0, 1.0) → 1m解像度
print(f'樹高5m以上: {(chm > 5).sum():,} ピクセル')
# 樹高 5m 以上のピクセル座標を取得
rows, cols = np.where(chm > 5)
xs = cols * transform.a + transform.c
ys = rows * transform.e + transform.f
chm_pts = np.column_stack([xs, ys])
labels = DBSCAN(eps=2.0, min_samples=10).fit(chm_pts).labels_
nc = len(set(labels)) - (1 if -1 in labels else 0)
print(nc) # → 74
74 本。かえって悪化した。
原因はデータを確認すると分かる:
樹高5m以上のピクセル数: 92,137
全ピクセル数: 401 × 301 = 120,701
被覆率: 92,137 ÷ 120,701 = 76%
御岳山の樹冠は面積の76%を隙間なく覆っている。
なぜ DBSCAN は連続樹冠に詰まるのか
【疎な森(DBSCAN が機能する)】
●●● ●●● ●●●
●●● ●●● ●●●
木A [隙間] 木B [隙間] 木C
隙間 < eps → 切れ目でクラスター分割 ✓
【密な森(御岳山の実態)】
●●●●●●●●●●●●●●●●
●●●●●●●●●●●●●●●●
木A 木B 木C 木D 木E
全ピクセルが繋がっている → 全体が1クラスター ✗
eps をどれだけ小さくしても、隣り合う木の樹冠ピクセルが接しているなら分割できない。
DBSCAN は「密度の谷(低密度な隙間)」を見て境界を引く。連続した樹冠にはその谷が存在しない。
正しいアルゴリズムの選び方
| 手法 | 原理 | 連続樹冠に対応 |
|---|---|---|
| DBSCAN | 密度の谷で分割 | ✗ |
| 局所最大値法 | 高さのピーク(山頂)を探す | ✓ |
| Watershed | 高さを地形として流域分割 | ✓ |
DBSCAN が向くのはクラスター間に物理的な隙間がある場合。森林では:
- ✅ 孤立木・散在する低木のグループ検出
- ✅ 建物・車・電柱などの地物分類
- ✅ ノイズ点の除去(隣接点のないノイズを
-1にする) - ❌ 連続した密林の個木分割
まとめ
| 項目 | 内容 |
|---|---|
| 何をするか | 密度の高い領域をクラスターとして自動検出 |
| 核心 | eps 以内に min_samples 点以上あればコア点、連鎖してクラスター形成 |
| eps の決め方 | k近傍距離グラフの「肘」を目安にする |
| パラメータ |
eps(距離の閾値)、min_samples(コア点の最小近傍数) |
| 弱点 | クラスター間に隙間がないと分割できない |
御岳山での実験では、ボクセル化点群・CHMピクセルどちらで試しても連続樹冠の壁に当たった。「試してみたら全部繋がった」という経験は、アルゴリズムの前提条件を理解する上で一番の教材になった。
このシリーズは、東京都多摩地域の森林LiDARデータをPythonで分析する中で出会ったアルゴリズムを1つずつ解説しています。
実装コードは tama-forest notebooks で公開中。
作者のその他の実験 → https://niikun.net

