Lesson 36

L36 樹高図 2件統合分析 — DCHM が描く 2 エリアの林相と森林構造

LiDARDCHM森林構造ラスタ分析rasterio林相推定
所要 20-30 分 / 想定レベル: リテラシ + GIS応用 / データ: DoBoX dsid 1626 + 1627 (樹高図 1m + 50cm), 代表 2 自治体 GeoTIFF

データ取得手順

このスクリプトは初回実行時にデータを自動取得します(DoBoX からの直接ダウンロード)。

IDデータセット名
#222dataset #222
#333dataset #333
#666dataset #666
#888都市計画区域情報_区域データ_安芸高田市_行政区域
#1626樹高図(1m)
#1627樹高図(50cm)

実行コマンド:

cd "2026 DoBoX 教材"
python -X utf8 lessons/L36_canopy_height.py

DoBoX のオープンデータは申請不要・商用/非商用とも利用可。 data/extras/.gitignore 対象(約 57 GB のキャッシュ)。 スクリプト実行で自動再生成されます。

学習目標と問い

カバー宣言: 本記事は DoBoX のシリーズ 「樹高図 1m / 樹高図 50cm」 2 件 (dataset_id = 1626, 1627) を統合し、広島県 2 自治体エリア (世羅町 1m, 熊野町 50cm) の樹高分布から 林相 (針葉樹/広葉樹/伐採地/都市・農地) を推定する研究記事です。 両データセットは航空レーザ計測 (LiDAR) 由来の樹冠高さラスタ (DCHM) で、 1 ピクセルあたり 1m もしくは 0.5m のメッシュで地物高さが記録されています。

シリーズ構造判定: (a) 同一手法・解像度違い + (b) 自治体ごとの分割

このシリーズの 2 件は、「解像度 1m vs 50cm」での同一手法 2 系統です。 両者は同じ航空レーザ計測のオリジナルデータから、解像度を変えて (1m メッシュ / 50cm メッシュ) 県内 23 自治体それぞれを GeoTIFF に切出した形で公開されています。

L32〜L35 (港湾・河川シリーズ) は線・点のインフラ施設データだったが、 L36 から始まるLiDAR ファミリ面 (ラスタ)のフィジカル測定が主役。 扱う技術もまったく異なる。

独自用語の定義 (本記事内で固定)

用語定義
樹高 (じゅこう)地表から樹冠頂点までの高さ (m)。本記事では DCHM ピクセル値そのもの。 建物・電柱なども「地物」として高さを取るため、純粋な「木の高さ」ではないことに注意。 広島県の樹高図仕様では「200m 以上は雲・霧として除去、それ以下のノイズも可能な限り除去」とされている。
DCHM (Digital Canopy Height Model)樹冠高さモデル。航空レーザ測量で得た DSM (Digital Surface Model = 地表全体の高さ) から DTM (Digital Terrain Model = 地面のみの高さ) を差し引いて作成した地物高さラスタ。 本記事の樹高図そのもの。1 セルが 1m もしくは 0.5m。
DSMDigital Surface Model。航空レーザが最初に当たった点 (= 樹冠頂点・建物屋上・地面) の高さを集めたラスタ。地物の上端を見ている。
DTMDigital Terrain Model。航空レーザの最後に当たった点 (= 地面に届いた点) を補間した「地面のみ」の高さラスタ。建物・木は除去済み。
航空レーザ (LiDAR)Light Detection And Ranging。航空機から地面に向けてレーザパルスを毎秒数十万発撃ち、戻り光の往復時間から距離を測る測量手法。 1 m² あたり数〜数十パルスを撃ち、樹冠と地面を別々に検出して 3D 点群を作る。 広島県では砂防課・林野庁治山課・国交省で公共測量として実施。
林相 (りんそう)森林の構成・外見的特徴。針葉樹 (スギ・ヒノキ等) / 広葉樹 (コナラ・ブナ等) / 混交林 / 竹林 / 伐採地 などに分類される。本記事では樹高分布パターンから推定するヒューリスティック手法を採用。
地物高さ階級本記事で独自定義した 7 階級 (≤1m 地表, 1-3m 低木, 3-6m 低中木, 6-10m 中木, 10-15m 中高木, 15-20m 高木, ≥20m 巨木)。 DoBoX 公式の分類ではなく、本記事の分析用ヒューリスティック
overview / ピラミッドGeoTIFF に埋め込まれた低解像度版。例えば「/1/128」は元データの 1/128 解像度を意味する。 1m × 27,248 × 16,357 (445M セル) のラスタも、/128 オーバビューなら 213×128 = 27K セル (1.5万倍小) で全体概観できる。 本記事ではこれを使ってメモリを 0.1 MB に抑える
有効データ率TIFF 全セルのうち、nodata でも NaN でもない有効値を持つセルの比率。 TIFF は行政界 +100m バッファで矩形に切出されているため、行政界の外側は nodata になる。

研究の問い (RQ)

広島県の樹高図 2 解像度 (1m / 50cm) は、それぞれ世羅町 (中山間, 林業地)熊野町 (都市近郊, 山岳混在)という異なる地域特性のエリアを公開している。 両エリアの樹高ヒストグラム・空間分布・林相を比較すると、 どんな地域構造の違いが見え、解像度差はどう影響するか?

  1. 2 dataset の面積・解像度・有効データ率の構造比較は?
  2. 樹高ヒストグラム (0-30m) のピーク位置・多峰性はどう違うか?
  3. 高木 (>=15m) / 低木 (<5m) / 地表 (<1m) の構成比は?
  4. 高木地・低木地の空間分布はどう偏在しているか?
  5. 樹高分布から林相はどう推定できるか?
  6. 解像度差 (1m vs 50cm) はノイズ・極端値にどう影響するか?

仮説 H1〜H6

到達点

2 解像度の樹高図ラスタを「2 自治体の代表エリア」として読み解き、 合計 706 M セル (約 7 億ピクセル) の DCHM データを overview ピラミッドを使って効率的に概観し、樹高ヒストグラム・空間分布・林相推定の 3 角度から 広島県の中山間 vs 都市近郊の森林構造の違いを実データで裏付ける。 さらに解像度差 (1m vs 50cm) が分析にどう影響するかを統計的に示す。

本記事のスコープ外: 本記事は DCHM (樹高ラスタ) の分布構造に集中する。 個々の樹木の樹種同定(画像分類タスク, 別記事)、 個葉スケールの形状解析(50cm でも個葉は分解できない)、 時系列変化(本記事の TIFF は 1 時点の計測, 経年変化は L37「計測年度図」と組合わせる発展課題)、 正確な針葉樹/広葉樹判別(LiDAR 単独では困難, 衛星画像 NDVI 等の組合せが必要) は本記事では扱わず発展課題とする。 また、本記事の「林相推定」は樹高分布パターンに基づくヒューリスティックであり、林野庁の公式分類ではない

使用データ

本記事が使用する 2 dataset の一覧。両者は同一手法 (LiDAR DCHM) の解像度違いで、 県内 22-23 自治体それぞれに GeoTIFF が公開されています。本記事は各 1 自治体を代表として用います。

dsid名称解像度公開単位本記事の代表TIFF サイズセル数DoBoX
1626樹高図(1m)1.0 m/cell22 自治体別 GeoTIFF世羅町 (34462) 1,700 MB 445.7 M #1626
1627樹高図(50cm)0.5 m/cell23 自治体別 GeoTIFF熊野町 (34307) 995 MB 260.8 M #1627

データ仕様 (両 dataset 共通)

L32-L35 シリーズとの重要な相違

L32-L35 (港湾/河川)L36 (本記事, 樹高図)
データ形式CSV (テーブル) + Shapefile (ベクタ)GeoTIFF (ラスタ)
レコード単位1 行 = 1 施設・1 ポリゴン1 セル = 地表 1m² (or 0.25m²) の高さ値
件数数十〜数千数億セル (世羅 446M, 熊野 261M)
分析操作groupby / sjoin / overlayimshow / 階級分類 / overview
主指標件数・延長 m・容量樹高 m・階級比率
必要パッケージgeopandas, shapelyrasterio (+ geopandas)
ファイルサイズ数 KB 〜 数 MB数百 MB 〜 16 GB

2 自治体の特性比較

世羅町 (1m)熊野町 (50cm)
立地備後内陸 (中山間)広島市東 (都市近郊)
面積 (行政界) 278.0 km² 33.6 km²
TIFF 範囲 (bbox) 27.2 × 16.4 km 7.6 × 8.6 km
主産業農林業 (世羅梨・りんご・大根)筆 (熊野筆) 製造
有効データ率 66.9% 58.5%
平均樹高 8.61 m 8.13 m
中央値 9.20 m 8.50 m

データ品質メモ

ダウンロード

生データ (DoBoX 直リンク)

容量警告: 上記 2 ZIP の合計は約 1.5 GB、展開後の TIFF は約 2.7 GB。 ハンズオン環境のディスク・メモリに余裕があるか事前確認してください。 他の自治体版 (例: 50cm 広島市は 15.9 GB) は本記事のスコープ外。

中間データ (本記事生成 CSV)

図 (本記事生成 PNG)

再現用 Python スクリプト

L36_canopy_height.py を取得して プロジェクトルートで py -X utf8 lessons/L36_canopy_height.py を実行。 TIFF が無ければ ZIP 自動 DL → 自動展開 → overview 読込で分析が走ります (合計 1.5 GB DL, 約 1-3 分)。

分析 1: 2 dataset とエリアの構造を可視化

狙い

2 dataset (樹高図 1m / 50cm) と本記事が選んだ 2 エリア (世羅町 / 熊野町) の 規模・形状・データ量を 1 枚の絵で示す。樹高ヒストグラムを重ねて 地域特性の違いを最初に視覚化する。

手法 (リテラシレベル解説)

GeoTIFF を扱う基本ツールは rasterio:

ピラミッド戦略: 1m × 27,248 × 16,357 (446M セル, 1.7GB) を全部読むと OOM。 overview /1/64 を使えば 425×255 = 0.11M セル (0.1 MB) で 全体概観が可能。本記事はこのピラミッド読みを徹底することで 1 分以内完走を実現する。

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import rasterio
from rasterio.enums import Resampling

# 各 TIFF を overview で読む (目標 ≈ 64m/cell)
TARGET_CELL_M = 64
for area in AREAS:
    with rasterio.open(area["tif"]) as ds:
        # overview 倍率リスト [2, 4, 8, 16, 32, 64, 128]
        ovr_choices = ds.overviews(1)
        target_factor = TARGET_CELL_M / area["resolution_m"]
        # 目標サイズ以上の最初の倍率を選ぶ
        best = next((f for f in ovr_choices if f >= target_factor), ovr_choices[-1])

        ov_w = ds.width // best
        ov_h = ds.height // best

        # 低解像度版を一括読込 (average resampling)
        arr = ds.read(1, out_shape=(ov_h, ov_w), resampling=Resampling.average)
        area["arr"] = arr  # shape: (ov_h, ov_w), dtype=float32

        # nodata マスク
        valid_mask = (~np.isnan(arr)) & (arr != ds.nodata) & (arr > -1000)
        area["valid"] = arr[valid_mask]

図と読み取り (図 1: dataset カード + ヒストグラム重ね)

なぜこの図か: 2 dataset の規模と特性を「カード形式」で文字情報として、 かつ「樹高ヒストグラム重ね」で量的特性として、同時に 1 枚で伝えるため。 左カード = 言語的概要、右ヒスト = 量的概観。

図 1: 2 dataset 概要カード (左) と 樹高ヒストグラム重ね (右)
図 1: 2 dataset 概要カード (左) と 樹高ヒストグラム重ね (右)

読み取り:

表と読み取り (基本統計)

label n_cells_total n_cells_valid valid_ratio min max mean median std p10 p25 p75 p90 p99 n_zero n_low n_mid n_high
sera_1m 世羅町 108375 72550 0.669435 0.0 30.831446 8.609228 9.201750 4.646875 1.441272 5.173980 11.820711 14.246659 18.838821 5575 12009 49778 5188
kumano_50cm 熊野町 16065 9403 0.585310 -1.0 24.855686 8.128922 8.503679 4.567696 1.655452 4.058023 11.593868 13.932288 17.883951 404 2274 6177 546

読み取り: 平均樹高は世羅 8.61 m vs 熊野 8.13 m で大差。 中央値も世羅 9.20 m vs 熊野 8.50 m。 P90 (上位 10%) は世羅 14.2 m vs 熊野 13.9 mで 両エリアとも 10m 以上の高木はある。 P99 (上位 1%)世羅 18.8 m vs 熊野 17.9 mでほぼ同等で、 両者ともに最高樹高は 20m 超 (= 成熟林) を含むことが分かる。

分析 2: 樹高ラスタの主題図化

狙い

樹高分布の空間構造 (どこに高木 / どこに低木) を地図で見る。 ヒストグラムは「全体としてどう」しか分からないが、 主題図は「町のどの場所にどの樹高があるか」を示す。

手法 (リテラシレベル解説)

樹高ラスタを matplotlib で塗り絵にする (= choropleth raster):

  1. rasterio で overview を読み込む (前節の結果)
  2. 樹高を 7 階級に切る (≤1m, 1-3, 3-6, 6-10, 10-15, 15-20, ≥20m)
  3. matplotlib.colors.ListedColormap + BoundaryNorm で離散カラーマップを作る
  4. ax.imshow(arr, cmap=cmap, norm=norm, extent=bounds) で塗り絵
  5. 行政界ポリゴン (admin) を重ねて視認性を上げる

カラー設計:

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from matplotlib.colors import ListedColormap, BoundaryNorm

# 階級境界と色 (7 階級)
cmap_levels = [0, 1, 3, 6, 10, 15, 20, 30]
cmap_colors = ["#e0e0e0", "#fff7bc", "#fec44f", "#a1d99b",
               "#41ab5d", "#005a32", "#54278f"]
custom_cmap = ListedColormap(cmap_colors)
custom_norm = BoundaryNorm(cmap_levels, ncolors=len(cmap_colors), clip=True)

# 各エリアを描画
for ax, area in zip(axes, AREAS):
    arr = area["arr"]                       # overview 読込済 (small)
    arr_show = np.clip(arr, 0, 30)          # 表示用クリップ
    extent = (area["bounds"].left, area["bounds"].right,
              area["bounds"].bottom, area["bounds"].top)
    ax.imshow(arr_show, cmap=custom_cmap, norm=custom_norm,
              extent=extent, origin="upper", interpolation="nearest")
    if area["admin"] is not None:
        area["admin"].boundary.plot(ax=ax, color="#222", linewidth=1.5)

図と読み取り (図 2: 樹高ラスタ主題図)

なぜこの図か: 樹高分布を「数値の塊」ではなく「町の地図」として見せたいから。 学習者は「自分の町だったらどう見えるか」を直感的に理解しやすい。

図 2: 2 エリアの樹高ラスタ主題図 (灰=地表, 黄=低木, 緑=中木, 深緑=高木, 紫=巨木)
図 2: 2 エリアの樹高ラスタ主題図 (灰=地表, 黄=低木, 緑=中木, 深緑=高木, 紫=巨木)

読み取り:

表と読み取り (TIFF メタ + 行政界面積)

項目世羅町熊野町
解像度 (m/cell)1.00.5
セル数 (M)445.7260.8
TIFF bbox 面積 (km²)445.765.2
行政界面積 (km²)278.033.6
有効セル比率66.9%58.5%
overview 倍率使用/1/64/1/127
実効セル (m/cell)6464

読み取り: 世羅町は熊野町より行政界が大きい (278 km² vs 34 km²)。 有効セル比率は両エリア 50-70% 程度 (= 行政界の外側 30-50% は nodata)。 これはバッファ +100m で矩形切出された結果、行政界が複雑な形ほど無効セルが増えるため。

分析 3: 7 階級構成比と空間分布 (small multiples)

狙い

樹高を7 階級に切って、各階級が町の何 % を占めるかを量的に示す。 さらに「階級ごとに地図化」(small multiples) で、どの階級がどこに分布するかを空間的に確認する。

手法 (リテラシレベル解説)

2 つのアプローチ:

  1. STEP1 階級分類: np.histogram で 7 ビンに割り、各ビンのセル数を集計。 百分率に直して積み上げ棒グラフで 1 列に表示 (図 3)。
  2. STEP2 階級別マスク: 各階級ごとに「そこに該当するセル」だけを 1 にしたバイナリマスクを作り、 カラーマップで階級色を塗る。4 主階級 × 2 エリア = 8 パネルを並べて 「都市部に多いのはどの階級か / 山地に多いのはどの階級か」を比較 (図 4)。

実装 (STEP1 階級集計)

L36_canopy_height.py 行 312–366

312
313
314
315
316
317
318
319
bins_class = [-1000, 1, 3, 6, 10, 15, 20, 200]
class_labels = ["<1m 地表", "1-3m 低木", "3-6m 低中木", "6-10m 中木",
                "10-15m 中高木", "15-20m 高木", ">=20m 巨木"]
class_counts = {}
for area in AREAS:
    valid = area["valid"]
    cc, _ = np.histogram(valid, bins=bins_class)
    class_counts[area["key"]] = cc

実装 (STEP2 マスク描画)

L36_canopy_height.py 行 814–849

 1
 2
 3
 4
 5
 6
 7
 8
 9
823
824
825
826
827
828
mask_def = [
    ("地表 (<1m)",   lambda a: ((a >= 0) & (a < 1)),  "#bbbbbb"),
    ("低木 (1-5m)",  lambda a: ((a >= 1) & (a < 5)),  "#fec44f"),
    ("中木 (5-15m)", lambda a: ((a >= 5) & (a < 15)), "#41ab5d"),
    ("高木 (>=15m)", lambda a: (a >= 15),             "#54278f"),
]
for row_i, area in enumerate(AREAS):
    arr = area["arr"]
    valid_mask = area["valid_mask"]
    for col_i, (lbl, fn, col) in enumerate(mask_def):
        ax = axes[row_i, col_i]
        m = fn(arr) & valid_mask
        show = np.where(m, 1.0, np.nan)
        cmap = ListedColormap([col])
        ax.imshow(show, cmap=cmap, extent=extent, vmin=0, vmax=1)

図と読み取り (図 3: 階級別構成比)

なぜこの図か: 「町の何 % が高木か」「何 % が地表か」という量的構成を、 階級色で塗り分けた 1 本のスタックバーで一目で比較するため。

図 3: 7 階級構成比 (積み上げ棒)
図 3: 7 階級構成比 (積み上げ棒)

読み取り:

図と読み取り (図 4: 階級別マスクマップ small multiples)

なぜこの図か: 「地表は南、高木は北」のような空間偏在を 階級ごとに色を変えて並列表示することで一目で比較するため。 2 エリア × 4 階級 = 8 パネル の small multiples。

図 4: 階級別マスクマップ (左→右: 地表/低木/中木/高木, 上=世羅, 下=熊野)
図 4: 階級別マスクマップ (左→右: 地表/低木/中木/高木, 上=世羅, 下=熊野)

読み取り:

表と読み取り (7 階級 wide 表)

class sera_n kumano_n sera_pct kumano_pct
<1m 地表 5575 406 7.68 4.32
1-3m 低木 6494 1515 8.95 16.11
3-6m 低中木 8622 1184 11.88 12.59
6-10m 中木 21244 2808 29.28 29.86
10-15m 中高木 25427 2944 35.05 31.31
15-20m 高木 4861 521 6.70 5.54
>=20m 巨木 327 25 0.45 0.27

読み取り:

分析 4: 樹高分布の詳細 (2峰性・boxplot・パーセンタイル)

狙い

樹高分布の「2 峰性」「裾の重さ」を細かく見る。 0.5m 刻みの詳細ヒスト・パーセンタイル・boxplot で、分布形状そのものを比較する。

手法 (リテラシレベル解説)

3 つの可視化を 4 panels に並べる:

実装

L36_canopy_height.py 行 1787–1820

 1
 2
 3
 4
 5
 6
 7
 8
 9
1796
1797
1798
1799
1800
import matplotlib.pyplot as plt
percs = [10, 25, 50, 75, 90, 99]
sera_p = [s_sera[f"p{p}"] if p != 50 else s_sera["median"] for p in percs]
kuma_p = [s_kuma[f"p{p}"] if p != 50 else s_kuma["median"] for p in percs]

# 詳細ヒスト (0.5m bin)
ax.hist(area["valid"], bins=np.arange(0, 30.5, 0.5), color=area["color"], alpha=0.85)

# boxplot
ax.boxplot([sera_v, kuma_v], showfliers=False, patch_artist=True)

# パーセンタイル比較
ax.bar(xs - 0.18, sera_p, 0.35, label="世羅町")
ax.bar(xs + 0.18, kuma_p, 0.35, label="熊野町")

図と読み取り (図 5: 詳細分布)

なぜこの図か: 1m bin では潰れる 2 峰性 (低 / 高) のピークが、0.5m bin で明瞭に見える。 boxplot で四分位、パーセンタイル曲線で全体形状を比較できる。

図 5: 詳細分布 (0.5m bin ヒスト + boxplot + パーセンタイル比較)
図 5: 詳細分布 (0.5m bin ヒスト + boxplot + パーセンタイル比較)

読み取り:

表と読み取り (パーセンタイル wide)

パーセンタイル世羅 (1m)熊野 (50cm)差 (世羅-熊野)
P101.441.66-0.21
P255.174.06+1.12
P509.208.50+0.70
P7511.8211.59+0.23
P9014.2513.93+0.31
P9918.8417.88+0.95

読み取り: P25 (下から 1/4 番目) は世羅 5.17 m vs 熊野 4.06 mで 差 +1.12m。 これは「町の下から 25 番目の樹高ですらこれほど違う」= 中山間 vs 都市近郊の根本構造差。 P99 (上位 1%) は両エリアとも 20m 超 = どちらも巨木は存在する。

分析 5: 高木の空間集塊性 (重心と矢印)

狙い

高木 (>=15m) と低木 (<3m) の地理的重心を計算し、両者の空間偏在を矢印で可視化する。 H5 (熊野町の高木は山岳に集中) を量的に検証。

手法 (リテラシレベル解説)

「重心」とは、ある条件を満たすセル群の平均位置 (X, Y)。 高木セルがすべて山地に偏っているなら、その重心は山地寄りに来る。 低木セルが平地に偏っているなら、その重心は平地寄りに来る。 両者の重心位置の差 (距離)が大きいほど、空間偏在が強い。

実装

L36_canopy_height.py 行 1854–1898

 1
 2
 3
 4
 5
 6
 7
 8
 9
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
def spatial_centroid(arr, mask):
    ys, xs = np.where(mask)
    if len(ys) == 0:
        return None, None
    return float(ys.mean()), float(xs.mean())

# 各エリア
high_mask = (arr >= 15) & valid_mask
low_mask = ((arr < 3) & (arr >= 0)) & valid_mask
yh_avg, xh_avg = spatial_centroid(arr, high_mask)
yl_avg, xl_avg = spatial_centroid(arr, low_mask)

# 画像 row → 世界座標 Y (上下逆) を変換
cy_h = bounds.top - (yh_avg + 0.5) * cell_size
cx_h = bounds.left + (xh_avg + 0.5) * cell_size

# 矢印
ax.annotate("", xy=(cx_h, cy_h), xytext=(cx_l, cy_l),
            arrowprops=dict(arrowstyle="->", color="red", linewidth=2))

図と読み取り (図 6: 集塊性)

なぜこの図か: 高木・低木の重心を点と矢印で示すと、 「町のどの方向に高木が偏っているか」が一目で分かる。 ヒストグラムでは見えない空間構造を補う。

図 6: 高木 (緑塗り) と低木重心 (×) → 高木重心 (★) の矢印
図 6: 高木 (緑塗り) と低木重心 (×) → 高木重心 (★) の矢印

読み取り:

表と読み取り (重心と偏在距離)

エリア高木重心 (X, Y, m)低木重心 (X, Y, m)正規化偏在距離
世羅町78963, -15132176915, -1537970.227
熊野町39803, -18179638741, -1837990.374

読み取り: 偏在距離の値は H5 検証指標の根拠。世羅町・熊野町ともに正規化距離 0.05+ なら有意な空間偏在ありと判定。

分析 6: 林相ヒューリスティック推定

狙い

樹高分布パターンから林相 (森林の構成タイプ) を簡易推定する。 本記事独自のヒューリスティックで、林野庁公式分類ではないが、 「学習者が手元のデータから森林タイプを見当つける」教育的価値はある。

手法 (リテラシレベル解説)

本記事の林相判定ルール (ヒューリスティック):

限界:

実装

L36_canopy_height.py 行 1927–2031

 1
 2
 3
 4
 5
 6
 7
 8
 9
1936
1937
1938
def estimate_lin_so(stats):
    pct_high = stats["n_high"] / stats["n_cells_valid"] * 100
    pct_zero = stats["n_zero"] / stats["n_cells_valid"] * 100
    pct_mid  = stats["n_mid"]  / stats["n_cells_valid"] * 100
    pct_low  = stats["n_low"]  / stats["n_cells_valid"] * 100
    if pct_zero >= 50:
        return "都市・農地・水域優占 (低樹高)"
    if pct_high >= 30:
        return "成熟林優占 (高木卓越)"
    if pct_mid >= 30 and pct_low >= 15:
        return "若齢林・植林手入れ中 (中木+低木)"
    return "林相混在 (中山間典型)"

結果と読み取り

エリア地表 (<1m) %低木 (1-5m) %中木 (5-15m) %高木 (>=15m) %推定林相
世羅町7.716.668.67.2若齢林・植林手入れ中 (中木+低木)
熊野町4.324.265.75.8若齢林・植林手入れ中 (中木+低木)

読み取り:

分析 7: 解像度差とノイズ (1m vs 50cm)

狙い

解像度差 (1m vs 50cm) がノイズ・外れ値・分散にどう影響するかを定量化。 H6 (50cm はノイズ多) を検証。

手法 (リテラシレベル解説)

4 つのチェック項目:

  1. 負値セル比率: 樹高は理論上 ≥0m。負値は DSM-DTM 差分計算のセンサ・地形誤差。 50cm の方が高解像度なので、誤差が表面化しやすい?
  2. 30m 超セル比率: 200m 超は仕様で除去済み。30m 超は巨木 or ノイズの二択。
  3. 標準偏差 (std): 解像度が高い方が単木個葉のばらつきを拾うので大?
  4. パーセンタイル曲線比較: 全体形状の差を 6 点で比較。

実装

L36_canopy_height.py 行 1978–2024

 1
 2
 3
 4
 5
 6
 7
 8
 9
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
neg_counts = []
for area in AREAS:
    v = area["valid"]
    n_neg = int((v < 0).sum())
    neg_counts.append({
        "label": area["label"],
        "n_neg": n_neg,
        "neg_pct": n_neg / len(v) * 100,
        "min": float(v.min()),
    })

big_counts = []
for area in AREAS:
    v = area["valid"]
    n_big = int((v >= 30).sum())
    big_counts.append({
        "label": area["label"],
        "n_big": n_big,
        "big_pct": n_big / len(v) * 100,
        "max": float(v.max()),
    })

図と読み取り (図 8: ノイズ分析 4 panels)

なぜこの図か: 解像度差の影響を「負値・極端値・std・分位」の 4 視点で並列確認するため。

図 8: 解像度差ノイズ分析 (負値・>30m・std・パーセンタイル)
図 8: 解像度差ノイズ分析 (負値・>30m・std・パーセンタイル)

読み取り:

表 (ノイズ集計)

label n_neg neg_pct min
世羅町 0 0.00000 0.000000
熊野町 2 0.02127 -1.195504
label n_big big_pct max
世羅町 1 0.001378 30.831446
熊野町 0 0.000000 24.855686

読み取り: 負値はデータ品質の重要な目印。50cm 解像度は計測精度の限界を露呈する。 教育材料としては「LiDAR データもまだ完璧ではない、ノイズや異常値が含まれる」というデータリテラシを学ぶ良い機会。

分析 8: DCHM 概念と中央サンプルの体感

狙い

DCHM がどう作られるかの概念 (DSM - DTM = DCHM) を視覚化し、 さらに各エリアの中央 256 セルサンプルを本来の解像度で見せて 「1m と 50cm の違いはこれだけ」を体感してもらう。

手法 (リテラシレベル解説)

2 ステップ:

  1. STEP1 概念図: DSM (地表全体の高さ)、DTM (地面のみ)、DCHM = DSM - DTM の 3 者を 1 枚の絵で説明。 木・建物・草地のイラストで「地物が出ている分が樹高」を直感的に。
  2. STEP2 中央サンプル: rasterio.Window で各 TIFF の中央 128x128 セルだけを読込み、 その本来解像度で表示。1m なら 128m × 128m の正方領域、50cm なら 64m × 64m の領域。

実装

L36_canopy_height.py 行 2047–2075

 1
 2
 3
 4
 5
 6
 7
 8
 9
2056
2057
from rasterio.windows import Window
SS = 128
for area in AREAS:
    with rasterio.open(area["tif"]) as ds:
        cx, cy = ds.width//2, ds.height//2
        win = Window(cx-SS//2, cy-SS//2, SS, SS)
        arr_sample = ds.read(1, window=win)
        if ds.nodata is not None:
            arr_sample = np.where(arr_sample == ds.nodata, np.nan, arr_sample)
        # 表示
        ax.imshow(arr_sample, cmap=custom_cmap, norm=custom_norm)

図と読み取り (図 9: DCHM 概念 + 中央サンプル)

なぜこの図か: 学習者の多くは「樹高図ってどう作ったの?」と疑問を持つ。 概念図 1 枚で DSM-DTM=DCHM を説明し、その後で実データの本来解像度を見せて、 「1m vs 50cm の違いはこれだけ」を体感してもらう。

図 9a: DCHM 概念図 (DSM - DTM = 樹高)
図 9a: DCHM 概念図 (DSM - DTM = 樹高)
図 9b: 各エリアの中央 128×128 セルサンプル — 1m と 50cm の違いを体感
図 9b: 各エリアの中央 128×128 セルサンプル — 1m と 50cm の違いを体感

読み取り (概念図):

読み取り (中央サンプル):

分析 9: overview バイアスと生解像度サンプル — 分析手法の選択が結論を変える

狙い

本記事は大量データを overview ピラミッド (/1/64〜/127) で粗化して読み込む戦略をとった。 これは速度・メモリの観点で必須だが、「平均化による情報損失」という代償がある。 特に樹高 0-1m セルは overview の平均化で大きく潰れることが H2 の検証で明らかになった。 本節で、その定量的バイアス生解像度サンプリングによる補正を見せる。

手法 (リテラシレベル解説)

「overview の平均化バイアス」とは:

本記事では各エリアの中央 1024×1024 セル (= 世羅 1km × 1km, 熊野 0.5km × 0.5km) を本来解像度で読込み、 overview 全域と比較する。

実装

L36_canopy_height.py 行 2109–2136

 1
 2
 3
 4
 5
 6
 7
 8
 9
2118
2119
from rasterio.windows import Window
RAW_SAMPLE = 1024
for area in AREAS:
    with rasterio.open(area["tif"]) as ds:
        cx, cy = ds.width//2, ds.height//2
        win = Window(cx - RAW_SAMPLE//2, cy - RAW_SAMPLE//2,
                     RAW_SAMPLE, RAW_SAMPLE)
        raw = ds.read(1, window=win).astype(np.float32)
    # 有効値で階級比率を再計算
    valid = raw[(~np.isnan(raw)) & (raw != ds.nodata) & (raw > -1000)]
    pct_zero_raw = (valid < 1).sum() / len(valid) * 100

図と読み取り (図 10: overview vs 生解像度比較)

なぜこの図か: 「同じデータでも処理方法でこれだけ階級比率が変わる」という分析手法のバイアスを 視覚的に伝えるため。学習者にはデータ前処理の選択が結論を変えることを学んでほしい。

図 10: overview 全域 vs 生解像度中央サンプル — 4 階級比率の比較
図 10: overview 全域 vs 生解像度中央サンプル — 4 階級比率の比較

読み取り:

表と読み取り (生解像度サンプル統計)

label resolution_m n_valid_cells pct_0_1m pct_1_5m pct_5_15m pct_ge_15m mean_m std_m
世羅町 1.0 1048576 35.66 9.77 43.12 11.45 6.66 6.18
熊野町 0.5 1048576 48.61 18.87 23.78 8.74 4.36 5.81

読み取り:

分析リテラシ上の重要メッセージ: overview ピラミッドは速度・メモリの恩恵がある反面、分布形状を歪める。 特に「mode at 0」「裾の重い分布」「希少階級」を見たい時は、必ず生解像度サンプルでクロスチェックすべき。 本記事の H2 がそうだったように、結論を逆転させることもある。

仮説検証と考察

本記事で立てた 6 つの仮説 (H1〜H6) と、樹高ラスタ分析結果との照合結果:

H主張結果判定
H1世羅町は林業地優占で平均樹高 >= 8m かつ 高木 (>=15m) 比率 >= 15%平均樹高 8.61 m, 高木比率 7.2%部分支持
H2熊野町は都市・農地優占で 0-1m セル比率 >= 40% (生解像度中央サンプルで判定)生解像度サンプル 0-1m 比率 48.6% (overview 全域では 4.3% — 平均化で過小評価)支持
H3両エリアとも 0-1m ビンが最頻 (mode)世羅 mode=10m, 熊野 mode=1m部分支持
H4世羅町の樹高ヒストグラムは 0-1m と 10-20m の 2 峰性低ピーク (0-1m): 5575, 高ピーク (10-20m帯 max): 7150, 中間 (2-10m) 最小: 2756支持
H5熊野町の高木は山岳 (北西側) に集中、低木は平地に分布 (空間偏在)熊野町: 高木重心 vs 低木重心 の正規化距離 = 0.374 (>=0.05 で偏在あり) / 世羅町: 0.227支持
H650cm 解像度は 1m より標準偏差が大 (ノイズ多, 単木個葉のばらつき)世羅 (1m) std=4.65, 熊野 (50cm) std=4.57, 50cm 最小値 (負値) = -1.00m部分支持

判定サマリ: 支持 3 / 部分支持 3 / 反証 0 / 未検証 0 (全 6 仮説中)

主な発見

考察

2 自治体のサンプル分析から、広島県の樹高図 dataset は「DSM-DTM 差分のラスタとして 中山間の林業構造、都市近郊の二極構造を量的に捉えられる」ことが示された。 樹高ヒストグラムだけでは「ゼロ卓越」が支配的に見えるが、 階級分類 + 空間マスクマップ + 重心分析を組合せると、 「町の中で樹高がどこにどう分布するか」を多角的に読み解ける。

解像度差 (1m vs 50cm) は、分布形状ではほぼ同じ傾向を示すが、 ノイズ・外れ値の出現率では 50cm が高い。 高解像度版は個別樹木同定や建物検出に向くが、広域分布把握には 1m で十分。 適材適所の選択が重要。

本記事では林相を樹高ヒューリスティックで推定したが、 これはあくまで教育目的の暫定分類。実用上は森林簿・空中写真・衛星 NDVI との 照合で精度向上が必要。本記事の主たる学びは 「ラスタ DCHM で何が読み取れて、何が読み取れないか」能力境界の理解と言える。

発展課題

結果 X1 → 仮説 Y1 → 課題 Z1: 林相を NDVI と組合せる

結果 X1: 樹高 0-1m セルが両エリアで圧倒的多数 (8-4%)。 これらを林業/非林業に分類できれば林相推定の精度が上がる。

新仮説 Y1: 樹高 0-1m + 衛星 NDVI ≥ 0.4 のセルは「裸地ではない草地」、 NDVI < 0.4 は「都市・農地休耕・水域」と分類できる。

課題 Z1: Sentinel-2 の同年 NDVI ラスタを取得し、樹高 0-1m セルと空間結合 (sjoin) して 4 分類 (草地/裸地/都市/農地) を作る。世羅と熊野で各分類比率を比較。

結果 X2 → 仮説 Y2 → 課題 Z2: L37 計測年度図と組合せた樹高変化

結果 X2: 本記事は 1 時点 (樹高図(1m)) のラスタのみで時系列変化は見えない。

新仮説 Y2: DoBoX の計測年度図 (dsid 1634) は各セルの計測年を示す。 同じエリアの連続 2 年計測があれば、樹高の経年変化 (差分) が計算できる。 若齢林は年 30-50cm の伸び、伐採跡は -20m 級の急減。

課題 Z2: 計測年度図と樹高図を時系列差分し、世羅町で 5 年間に伐採された箇所と新たに植林された箇所をマップ化。 林業計画の効果検証として活用。

結果 X3 → 仮説 Y3 → 課題 Z3: 標高図 (dsid 1623 等) との組合せ

結果 X3: 本記事は樹高だけで、標高や傾斜は見ていない。

新仮説 Y3: 高木 (>=15m) は標高 200-600m 帯の南向き斜面に集中。 急斜面 (>30°) は植林されにくく低木優占。

課題 Z3: 標高図ラスタ (dsid 1623, 1m メッシュ DEM) と組合せて、 樹高 × 標高帯 × 傾斜階級のクロス集計表を作る。 林業適地マッピングへの応用を発展させる。

結果 X4 → 仮説 Y4 → 課題 Z4: 1m vs 50cm 同一エリア比較

結果 X4: 本記事は世羅 (1m) と熊野 (50cm) で異なる自治体を比較したため、 解像度差と地域特性差が混在している。

新仮説 Y4: 同一自治体の 1m vs 50cm を直接比較すれば、 純粋な解像度差の影響が分離できる。50cm は単木個葉ぶんの std 増加、 1m はピクセル平均化による標準偏差減少を予想。

課題 Z4: 廿日市市は 1m / 50cm 両方の TIFF が公開されている。 同じエリアで 50cm を 2x2 平均で 1m 化したものと、元々の 1m を比較し、 「サンプリング差」と「計測精度差」を切り分ける。

結果 X5 → 仮説 Y5 → 課題 Z5: 個別樹木同定 (50cm 解像度の真の価値)

結果 X5: 本記事 overview 分析では 50cm の真の価値 (個別木識別) は活かせていない。

新仮説 Y5: 50cm 解像度なら1 本の木 (樹冠 5-10m) を 100-400 セルで捉えられる。 高木検出アルゴリズム (極大値検出 + watershed segmentation) で本数推定が可能。

課題 Z5: 50cm 熊野 TIFF の 200m 角サンプルで、ガウシアンフィルタ + 局所極大値検出で 個別樹冠の本数 N をカウント。それを ha 単位で割って本数密度 (本/ha) を算出。 林業計画の林分密度と比較。

結果 X6 → 仮説 Y6 → 課題 Z6: 巨木スポット (>=20m) の現地照合

結果 X6: 両エリアで 20m 超セルがごく少数存在 (1556〜164 cell 程度の推定)。 これらは巨木 or ノイズ or 建物?

新仮説 Y6: 20m 超セルは大半が神社・寺院境内の老木 or 高層建築物。 事前に建物 footprint (国土地理院 BLD) でマスクすれば、巨木のみ抽出できる。

課題 Z6: 国土地理院 BLD shp で建物 footprint を取得し、樹高ラスタから建物セルを除外。 残った 20m 超セルが真の巨木スポット。Google Maps Street View で現地確認、 神社・寺院・古木の文化的価値を地理的に評価。