0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DBSCANで点群を個木分割する(LiDAR / 連続樹冠の限界まで解説)

0
Posted at

DBSCAN.png

これは何?

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点

image.png

別の方法で検出した樹木本数は 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?