U-Net や Grounding DINO を活用した GeoAI による建物フットプリント抽出チュートリアル
本文の状態
日本語全文を表示中
詳細モードで約10分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
MarkTechPost
このチュートリアルは、NAIP 画像から建物フットプリントを抽出する GeoAI ワークフローの設計法を示し、U-Net や Grounding DINO などのモデルを実装・比較する実践的なガイドを提供する。
AI深層分析を開く2026年8月3日 10:10
AI深層分析
キーポイント
GeoAI ワークフローの構築
地理空間深層学習環境の設定から始まり、ラスター画像とベクトルラベルの取得、空間特性の解析を経て、ジオ参照されたイメージチップとセグメンテーションマスクを生成する一連のプロセスを解説している。
多様なモデルの実装と比較
ResNet-34 エンコーダーを持つ U-Net の学習と評価に加え、スライディングウィンドウ推論、ゼロショットセグメンテーション(Grounding DINO と SAM)、および事前学習済み Mask R-CNN によるインスタンスセグメンテーションの結果を比較する。
実世界データへの適用
Microsoft Planetary Computer の NAIP 画像と Overture Maps の建物ラベルを用いて、同様のパイプラインを実世界の領域に拡張する方法を示し、予測マスクの清掃や正則化、IoU や F1 スコアの計算を含む。
環境構築と依存関係の自動インストール
Google Colab環境でgeoai-pyなどの主要ライブラリがインポート失敗時に自動的にpip installされ、再実行時のキャッシュ利用により高速化される仕組みを実装している。
学習設定パラメータの定義
U-Netアーキテクチャ(ResNet34エンコーダ)を用いた建物抽出タスクのために、タイルサイズやストライド、バッチサイズ、学習率などのトレーニングハイパーパラメータを辞書形式で統一管理している。
重要な引用
In this tutorial, we design a complete GeoAI workflow for extracting building footprints from high-resolution NAIP aerial imagery.
Beyond semantic segmentation, we convert predicted masks into cleaned and regularized building polygons, calculate IoU and F1 metrics.
"Runtime > Restart session, then re-run this cell. The install is cached, so it will be fast the second time."
"Colab: Runtime > Change runtime type > Hardware accelerator > T4 GPU"
編集コメントを表示
編集コメント
このチュートリアルは、理論的な解説だけでなく、実際のコードとデータソースを用いた実践的な学習リソースとして価値が高い。特に複数のモデルを比較する構成は、技術選定を行う開発者にとって有益な情報源となる。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
本チュートリアルでは、高解像度の NAIP 航空画像から建物の輪郭(フットプリント)を抽出するための GeoAI ワークフローの設計方法を解説します。
まず、地理空間ディープラーニング環境を設定し、ラスター画像とベクトルラベルをダウンロードして、それらの空間特性を確認します。その後、地理座標付きの画像チップとセグメンテーションマスクを生成します。
次に、ResNet-34 エンコーダーを持つ U-Net モデルをトレーニングし、学習挙動を評価した上で、未見のシーンに対してスライディングウィンドウ推論を適用します。
意味分割(セマンティック・セグメンテーション)を超えて、予測されたマスクを清掃・正規化された建物ポリゴンに変換し、IoU や F1 スコアなどの指標を計算します。さらに、Grounding DINO と SAM を用いたゼロショットセグメンテーションの検討や、事前学習済み Mask R-CNN インスタンス分割モデルとの結果比較も行います。
最後に、Microsoft Planetary Computer の NAIP 画像と Overture Maps の建物ラベルを用いて、このパイプラインを実世界の地域に拡張する方法も紹介します。
import os
import subprocess
import sys
import time
import warnings
warnings.filterwarnings("ignore")
IN_COLAB = "google.colab" in sys.modules
def pip_install(packages, quiet=True):
"""Install packages with pip from inside the notebook process."""
cmd = [sys.executable, "-m", "pip", "install", "--upgrade"]
if quiet:
cmd.append("-q")
subprocess.run(cmd + list(packages), check=False)
try:
import geoai
except ImportError:
print(">>> Installing geoai-py and friends (takes ~2-4 minutes on Colab)...")
pip_install(
[
"geoai-py",
"segmentation-models-pytorch",
"buildingregulariser",
]
)
try:
import geoai
except Exception as e:
raise SystemExit(
f"Import failed after install ({e}).\n"
"=> Runtime > Restart session, then re-run this cell. "
"The install is cached, so it will be fast the second time."
)
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import rasterio
import torch
from rasterio.plot import plotting_extent
from IPython.display import display
print(f"geoai : {geoai.__version__}")
print(f"torch : {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU : {torch.cuda.get_device_name(0)}")
else:
print("!! No GPU detected. Training will still run but be much slower.")
print(" Colab: Runtime > Change runtime type > Hardware accelerator > T4 GPU")
DEVICE = geoai.get_device()
print(f"geoai device : {DEVICE}")
CFG = {
"tile_size": 512,
"stride": 256,
"buffer_radius": 0,
"architecture": "unet",
"encoder": "resnet34",
"encoder_weights": "imagenet",
"num_channels": 3,
"num_classes": 2,
"batch_size": 8,
"num_epochs": 12,
"learning_rate": 1e-3,
"val_split": 0.2,
"window_size": 512,
"overlap": 256,
"run_zero_shot": True,
"run_pretrained": True,
"run_real_aoi": False,
}
WORK = "/content/geoai_tutorial" if IN_COLAB else os.path.abspath("geoai_tutorial")
os.makedirs(WORK, exist_ok=True)
os.chdir(WORK)
print(f"working dir : {WORK}")
def banner(text):
print("\n" + "=" * 92 + f"\n {text}\n" + "=" * 92)
def timed(fn, label):
"""Run fn(), report wall time, never let one step kill the notebook."""
banner(label)
t0 = time.time()
try:
out = fn()
print(f"\n[OK] {label} — {time.time() - t0:.1f}s")
return out
except Exception as exc:
import traceback
print(f"\n[SKIPPED] {label}\n{type(exc).__name__}: {exc}")
traceback.print_exc(limit=3)
return None
HF = "https://huggingface.co/datasets/giswqs/geospatial/resolve/main"
train_raster_url = f"{HF}/naip_rgb_train.tif"
train_vector_url = f"{HF}/naip_train_buildings.geojson"
test_raster_url = f"{HF}/naip_test.tif"
def step1():
train_raster = geoai.download_file(train_raster_url)
train_vector = geoai.download_file(train_vector_url)
test_raster = geoai.download_file(test_raster_url)
for p in (train_raster, train_vector, test_raster):
print(f" {os.path.getsize(p) / 1e6:8.2f} MB {p}")
return train_raster, train_vector, test_raster
paths = timed(step1, "STEP 1 — Downloading sample NAIP imagery and building labels")
TRAIN_RASTER, TRAIN_VECTOR, TEST_RASTER = paths
環境を設定し、必要な GeoAI および深層学習ライブラリをインストールして GPU の利用可否を確認します。データセット作成、モデルのトレーニング、推論、およびオプションの処理段階における中心的な設定パラメータを定義します。その後、作業用ディレクトリを作成し、再利用可能な実行ユーティリティを定義して、NAIP 画像と建物フットプリントのラベルデータをダウンロードします。
Copy CodeCopiedUse a different Browser
def step2():
info = geoai.get_raster_info(TRAIN_RASTER)
for k, v in info.items():
print(f" {k:<16}: {v}")
print("\n--- per-band statistics ---")
print(geoai.get_raster_stats(TRAIN_RASTER))
print("\n--- vector info ---")
vinfo = geoai.get_vector_info(TRAIN_VECTOR)
for k, v in vinfo.items():
print(f" {k:<16}: {v}")
gdf = gpd.read_file(TRAIN_VECTOR)
print(f"\n {len(gdf)} training buildings | CRS {gdf.crs}")
print(gdf.head(3))
geoai.view_vector(
gdf,
raster_path=TRAIN_RASTER,
outline_only=True,
edge_color="yellow",
outline_linewidth=0.8,
figsize=(11, 11),
title="NAIP training scene + building footprints",
)
try:
display(geoai.view_vector_interactive(TRAIN_VECTOR, layer_name="Buildings"))
except Exception as e:
print(f" (interactive map unavailable here: {e})")
return gdf
LABELS_GDF = timed(step2, "STEP 2 — Inspecting raster + vector data")
TILES_DIR = os.path.join(WORK, "tiles")
def step3():
stats = geoai.export_geotiff_tiles(
in_raster=TRAIN_RASTER,
out_folder=TILES_DIR,
in_class_data=TRAIN_VECTOR,
tile_size=CFG["tile_size"],
stride=CFG["stride"],
buffer_radius=CFG["buffer_radius"],
all_touched=True,
skip_empty_tiles=False,
quiet=False,
)
n_img = len(os.listdir(f"{TILES_DIR}/images"))
n_lbl = len(os.listdir(f"{TILES_DIR}/labels"))
print(f"\n chips: {n_img} images / {n_lbl} masks")
if isinstance(stats, dict):
tot = max(stats.get("total_tiles", n_img), 1)
print(f" tiles containing buildings: {stats.get('tiles_with_features')} "
f"({100 * stats.get('tiles_with_features', 0) / tot:.1f}%)")
print(f" foreground pixels: {stats.get('feature_pixels'):,}")
geoai.display_training_tiles(TILES_DIR, num_tiles=6, figsize=(18, 6))
return stats
TILE_STATS = timed(step3, "STEP 3 — Exporting image chips and label masks")
ラスターとベクターデータを確認し、座標系や次元、統計情報、特徴構造を把握します。航空画像上に建物ラベルを重ねて表示し、空間的な探索が可能なインタラクティブなマップも作成します。その後、ソース画像を重なり合う地理参照チップに分割し、モデル学習用の対応するラスターマスクを生成します。
Copy CodeCopiedUse a different Browser
MODEL_DIR = os.path.join(WORK, "models_unet")
BEST_MODEL = os.path.join(MODEL_DIR, "best_model.pth")
def step4():
geoai.train_segmentation_model(
images_dir=f"{TILES_DIR}/images",
labels_dir=f"{TILES_DIR}/labels",
output_dir=MODEL_DIR,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
encoder_weights=CFG["encoder_weights"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
batch_size=CFG["batch_size"],
num_epochs=CFG["num_epochs"],
learning_rate=CFG["learning_rate"],
val_split=CFG["val_split"],
save_best_only=True,
early_stopping_patience=5,
verbose=True,
)
print(f"\n best checkpoint: {BEST_MODEL}")
print(f" size: {os.path.getsize(BEST_MODEL) / 1e6:.1f} MB")
return BEST_MODEL
timed(step4, f"STEP 4 — Training {CFG['architecture']}/{CFG['encoder']} "
f"for {CFG['num_epochs']} epochs")
def step5():
hist_path = os.path.join(MODEL_DIR, "training_history.pth")
geoai.plot_performance_metrics(
history_path=hist_path,
figsize=(15, 5),
verbose=True,
save_path=os.path.join(WORK, "training_curves.png"),
)
h = torch.load(hist_path, weights_only=False)
best_ep = int(np.argmax(h["val_iou"])) + 1
print(f"\n best val IoU {max(h['val_iou']):.4f} at epoch {best_ep}")
print(" Reading the curves: val loss rising while train loss falls => overfitting;")
print(" both flat and high => underfitting (more epochs, bigger encoder, or more chips).")
timed(step5, "STEP 5 — Training diagnostics")
U-Net セマンティックセグメンテーションモデルを、ResNet-34 エンコーダーを用いて構築し、準備した画像とマスクのタイルで学習を行います。学習プロセスには、検証データの分割、早期打ち切り(early stopping)、チェックポイントの保存、パフォーマンス監視を設定します。
その後、学習履歴を読み込んで学習曲線をプロットし、検証 IoU が最大となるエポックを特定します。
PRED_MASK = os.path.join(WORK, "test_prediction.tif")
PRED_PROB = os.path.join(WORK, "test_probability.tif")
def step6():
geoai.semantic_segmentation(
input_path=TEST_RASTER,
output_path=PRED_MASK,
model_path=BEST_MODEL,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
window_size=CFG["window_size"],
overlap=CFG["overlap"],
batch_size=4,
probability_path=PRED_PROB,
)
geoai.print_raster_info(PRED_MASK, show_preview=False)
geoai.plot_prediction_comparison(
original_image=TEST_RASTER,
prediction_image=PRED_MASK,
titles=["NAIP test scene", "Predicted building mask"],
figsize=(16, 8),
prediction_colormap="viridis",
save_path=os.path.join(WORK, "prediction_comparison.png"),
)
with rasterio.open(PRED_MASK) as src:
m = src.read(1)
px = float(abs(src.transform.a) * abs(src.transform.e))
print(f" predicted building pixels: {int((m > 0).sum()):,} "
f"({100 * (m > 0).mean():.2f}% of scene, ~{(m > 0).sum() * px:,.0f} m2)")
timed(step6, "STEP 6 — Sliding-window inference on the test scene")
VEC_RAW = os.path.join(WORK, "buildings_raw.geojson")
VEC_ORTHO = os.path.join(WORK, "buildings_orthogonal.geojson")
VEC_FINAL = os.path.join(WORK, "buildings_final.geojson")
def step7():
grouped = geoai.region_groups(
PRED_MASK,
connectivity=2,
min_size=50,
out_image=os.path.join(WORK, "test_prediction_cleaned.tif"),
)
clean_mask = os.path.join(WORK, "test_prediction_cleaned.tif")
raw = geoai.raster_to_vector(
clean_mask,
output_path=VEC_RAW,
threshold=0,
min_area=15,
simplify_tolerance=0.5,
)
print(f" raw polygons : {len(raw)}")
ortho = geoai.orthogonalize(
input_path=clean_mask,
output_path=VEC_ORTHO,
epsilon=1.5,
min_area=15,
)
print(f" orthogonalized : {len(ortho)}")
final = geoai.regularization(ortho, angle_tolerance=12, simplify_tolerance=0.4)
final = geoai.add_geometric_properties(
final, properties=["area", "perimeter", "solidity", "elongation", "orientation"]
)
final.to_file(VEC_FINAL, driver="GeoJSON")
print(f" final footprints : {len(final)}")
print(final.head())
if "area" in final.columns:
print("\n footprint area stats (m2):")
print(final["area"].describe().round(1).to_string())
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
with rasterio.open(TEST_RASTER) as src:
rgb = src.read([1, 2, 3]).transpose(1, 2, 0)
rgb = np.clip(rgb / np.percentile(rgb, 99), 0, 1)
ext = plotting_extent(src)
for ax, g, t in zip(axes, [raw, final], ["Raw polygonization", "Orthogonalized + regularized"]):
ax.imshow(rgb, extent=ext)
g.plot(ax=ax, facecolor="none", edgecolor="red", linewidth=1.1)
ax.set_title(t)
ax.set_axis_off()
plt.tight_layout()
plt.show()
try:
display(geoai.view_vector_interactive(VEC_FINAL, layer_name="Predicted buildings"))
except Exception:
pass
return final
FINAL_GDF = timed(step7, "STEP 7 — Vectorizing and regularizing the predicted footprints")
見知らぬ NAIP の画像データに対してスライディングウィンドウ推論を適用し、予測結果と確率ラスタの両方を生成します。その後、小さなノイズ領域を除去して予測マスクをベクトルポリゴンに変換し、建物の境界線がより明確になるよう幾何形状を整えます。さらに幾何学的な属性を計算し、単純にポリゴン化した結果と、直交化・整形後の出力を比較します。
Copy CodeCopiedUse a different Browser
def step8():
gt_raster = os.path.join(WORK, "train_gt_mask.tif")
geoai.vector_to_raster(
vector_path=TRAIN_VECTOR,
output_path=gt_raster,
reference_raster=TRAIN_RASTER,
fill_value=0,
all_touched=True,
dtype=np.uint8,
)
train_pred = os.path.join(WORK, "train_prediction.tif")
geoai.semantic_segmentation(
input_path=TRAIN_RASTER,
output_path=train_pred,
model_path=BEST_MODEL,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
window_size=CFG["window_size"],
overlap=CFG["overlap"],
quiet=True,
)
metrics = geoai.calc_segmentation_metrics(
ground_truth=gt_raster,
prediction=train_pred,
num_classes=2,
metrics=["iou", "f1"],
)
print("\n --- pixel-wise metrics (class 0 = background, class 1 = building) ---")
for k, v in metrics.items():
print(f" {k:<10}: {np.round(v, 4)}")
print("\n Building-class IoU is the number that matters; background IoU is inflated")
print(" by the huge negative class and always looks great.")
geoai.plot_prediction_comparison(
original_image=TRAIN_RASTER,
prediction_image=train_pred,
ground_truth_image=gt_raster,
titles=["Imagery", "Prediction", "Ground truth"],
figsize=(18, 6),
save_path=os.path.join(WORK, "accuracy_comparison.png"),
)
return metrics
METRICS = timed(step8, "STEP 8 — Quantitative accuracy assessment")
def step9():
if not CFG["run_zero_shot"]:
print(" disabled in CFG"); return
chip = os.path.join(WORK, "test_chip.tif")
with rasterio.open(TEST_RASTER) as src:
b = src.bounds
cx, cy = (b.left + b.right) / 2, (b.bottom + b.top) / 2
half = min((b.right - b.left), (b.top - b.bottom)) / 6
bbox = [cx - half, cy - half, cx + half, cy + half]
geoai.clip_raster_by_bbox(TEST_RASTER, chip, bbox=bbox, bbox_type="geo")
print(f" clipped chip: {chip}")
sam = geoai.GroundedSAM(
detector_id="IDEA-Research/grounding-dino-tiny",
segmenter_id="facebook/sam-vit-base",
tile_size=1024,
overlap=128,
threshold=0.3,
)
out_mask = os.path.join(WORK, "zeroshot_mask.tif")
gdf = sam.segment_image(
input_path=chip,
output_path=out_mask,
text_prompts=["building", "house", "rooftop"],
polygon_refinement=True,
export_polygons=True,
min_polygon_area=30,
simplify_tolerance=1.5,
)
geoai.plot_prediction_comparison(
original_image=chip,
prediction_image=out_mask,
titles=["Chip", "Zero-shot: 'building / house / rooftop'"],
figsize=(14, 7),
prediction_colormap="viridis",
)
print(f" zero-shot objects found: {len(gdf) if gdf is not None else 0}")
geoai.empty_cache()
timed(step9, "STEP 9 — Zero-shot text-prompted segmentation (Grounding DINO + SAM)")
セグメンテーションモデルの評価は、予測結果とラスタライズされた正解ビルラベルを比較して行います。ピクセルレベルの IoU と F1 スコアを算出し、画像・予測結果・参照マスクを並べて可視化します。その後、Grounding DINO と SAM を用いて、追加のモデル学習なしでテキストプロンプトに基づくゼロショットビルセグメンテーションを実行します。
Copy CodeCopiedUse a different Browser
def step10():
if not CFG["run_pretrained"]:
print(" disabled in CFG"); return
extractor = geoai.BuildingFootprintExtractor(model_path="building_footprints_usa.pth")
gdf = extractor.process_raster(
TEST_RASTER,
output_path=os.path.join(WORK, "buildings_maskrcnn.geojson"),
batch_size=4,
confidence_threshold=0.5,
overlap=0.25,
mask_threshold=0.5,
min_object_area=100,
filter_edges=True,
)
if gdf is None or len(gdf) == 0:
print(" no instances returned"); return
print(f" building instances: {len(gdf)}")
reg = extractor.regularize_buildings(gdf, min_area=20, angle_threshold=15)
reg.to_file(os.path.join(WORK, "buildings_maskrcnn_regularized.geojson"), driver="GeoJSON")
extractor.visualize_results(TEST_RASTER, gdf=reg, figsize=(12, 12))
if FINAL_GDF is not None:
print(f"\n your U-Net : {len(FINAL_GDF)} polygons")
print(f" pretrained R-CNN: {len(reg)} polygons")
print(" Different counts are expected: U-Net merges adjacent roofs, Mask R-CNN splits")
print(" them into instances. Pick the paradigm that matches your downstream question.")
geoai.empty_cache()
timed(step10, "STEP 10 — Pretrained Mask R-CNN instance segmentation")
def step11():
if not CFG["run_real_aoi"]:
print(" disabled (set CFG['run_real_aoi'] = True to run; needs open internet)")
return
bbox = (-83.9400, 35.9500, -83.9250, 35.9600)
items = geoai.pc_stac_search(
collection="naip",
bbox=list(bbox),
time_range="2021-01-01/2023-12-31",
max_items=3,
)
print(f" STAC items found: {len(items)}")
aoi_dir = os.path.join(WORK, "aoi")
tif = geoai.download_naip(bbox=bbox, output_dir=aoi_dir, max_items=1, preview=False)
print(f" NAIP: {tif}")
ovt = os.path.join(aoi_dir, "overture_buildings.geojson")
geoai.download_overture_buildings(bbox=bbox, output=ovt, overture_type="building")
print(f" Overture buildings: {ovt}")
print(geoai.extract_building_stats(ovt))
raster = tif[0] if isinstance(tif, (list, tuple)) else tif
geoai.export_geotiff_tiles(
in_raster=raster,
out_folder=os.path.join(aoi_dir, "tiles"),
in_class_data=ovt,
tile_size=512,
stride=256,
)
print(" AOI dataset ready — feed it to train_segmentation_model() exactly as in STEP 4.")
timed(step11, "STEP 11 — (optional) Real AOI: Planetary Computer NAIP + Overture Maps labels")
def step12():
outputs = [f for f in sorted(os.listdir(WORK))
if f.endswith((".tif", ".geojson", ".png", ".pth"))]
print(" artifacts produced:")
for f in outputs:
print(f" {os.path.getsize(os.path.join(WORK, f)) / 1e6:8.2f} MB {f}")
zip_path = os.path.join(WORK, "geoai_results.zip")
subprocess.run(
["zip", "-qr", zip_path, ".", "-i", "*.geojson", "*.png", "*.tif", "-x", "*tiles*"],
cwd=WORK, check=False,
)
print(f"\n bundle: {zip_path}")
if IN_COLAB:
print(" Download it with: from google.colab import files; "
"files.download('%s')" % zip_path)
timed(step12, "STEP 12 — Results summary")
banner("DONE")
print("""
Where to go next
- Swap the head, keep the code: architecture="deeplabv3plus", encoder_name="efficientnet-b3"
(or any timm encoder) in train_segmentation_model().
- 4-band NAIP (RGB+NIR): num_channels=4 everywhere; the first conv is auto-adapted.
- Multi-class land cover: geoai.train_segmentation_landcover() + geoai.export_landcover_tiles(),
with DiceLoss / FocalLoss / TverskyLoss from geoai.landcover_train for class imbalance.
- Instance segmentation you train yourself: geoai.train_MaskRCNN_model() then
geoai.instance_segmentation(...
原文を表示
In this tutorial, we design a complete GeoAI workflow for extracting building footprints from high-resolution NAIP aerial imagery. We begin by configuring the geospatial deep learning environment, downloading raster imagery and vector labels, and inspecting their spatial properties before generating georeferenced image chips and segmentation masks. We then train a U-Net model with a ResNet-34 encoder, evaluate its learning behavior, and apply sliding-window inference to an unseen scene. Beyond semantic segmentation, we convert predicted masks into cleaned and regularized building polygons, calculate IoU and F1 metrics, explore zero-shot segmentation with Grounding DINO and SAM, and compare the results with a pretrained Mask R-CNN instance segmentation model. We also demonstrate how the same pipeline extends to real-world areas using NAIP imagery from Microsoft Planetary Computer and building labels from Overture Maps.
Copy CodeCopiedUse a different Browser
import os
import subprocess
import sys
import time
import warnings
warnings.filterwarnings("ignore")
IN_COLAB = "google.colab" in sys.modules
def pip_install(packages, quiet=True):
"""Install packages with pip from inside the notebook process."""
cmd = [sys.executable, "-m", "pip", "install", "--upgrade"]
if quiet:
cmd.append("-q")
subprocess.run(cmd + list(packages), check=False)
try:
import geoai
except ImportError:
print(">>> Installing geoai-py and friends (takes ~2-4 minutes on Colab)...")
pip_install(
[
"geoai-py",
"segmentation-models-pytorch",
"buildingregulariser",
]
)
try:
import geoai
except Exception as e:
raise SystemExit(
f"Import failed after install ({e}).\n"
"=> Runtime > Restart session, then re-run this cell. "
"The install is cached, so it will be fast the second time."
)
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import rasterio
import torch
from rasterio.plot import plotting_extent
from IPython.display import display
print(f"geoai : {geoai.__version__}")
print(f"torch : {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU : {torch.cuda.get_device_name(0)}")
else:
print("!! No GPU detected. Training will still run but be much slower.")
print(" Colab: Runtime > Change runtime type > Hardware accelerator > T4 GPU")
DEVICE = geoai.get_device()
print(f"geoai device : {DEVICE}")
CFG = {
"tile_size": 512,
"stride": 256,
"buffer_radius": 0,
"architecture": "unet",
"encoder": "resnet34",
"encoder_weights": "imagenet",
"num_channels": 3,
"num_classes": 2,
"batch_size": 8,
"num_epochs": 12,
"learning_rate": 1e-3,
"val_split": 0.2,
"window_size": 512,
"overlap": 256,
"run_zero_shot": True,
"run_pretrained": True,
"run_real_aoi": False,
}
WORK = "/content/geoai_tutorial" if IN_COLAB else os.path.abspath("geoai_tutorial")
os.makedirs(WORK, exist_ok=True)
os.chdir(WORK)
print(f"working dir : {WORK}")
def banner(text):
print("\n" + "=" * 92 + f"\n {text}\n" + "=" * 92)
def timed(fn, label):
"""Run fn(), report wall time, never let one step kill the notebook."""
banner(label)
t0 = time.time()
try:
out = fn()
print(f"\n[OK] {label} — {time.time() - t0:.1f}s")
return out
except Exception as exc:
import traceback
print(f"\n[SKIPPED] {label}\n{type(exc).__name__}: {exc}")
traceback.print_exc(limit=3)
return None
HF = "https://huggingface.co/datasets/giswqs/geospatial/resolve/main"
train_raster_url = f"{HF}/naip_rgb_train.tif"
train_vector_url = f"{HF}/naip_train_buildings.geojson"
test_raster_url = f"{HF}/naip_test.tif"
def step1():
train_raster = geoai.download_file(train_raster_url)
train_vector = geoai.download_file(train_vector_url)
test_raster = geoai.download_file(test_raster_url)
for p in (train_raster, train_vector, test_raster):
print(f" {os.path.getsize(p) / 1e6:8.2f} MB {p}")
return train_raster, train_vector, test_raster
paths = timed(step1, "STEP 1 — Downloading sample NAIP imagery and building labels")
TRAIN_RASTER, TRAIN_VECTOR, TEST_RASTER = paths
We configure the environment, install the required GeoAI and deep learning libraries, and verify GPU availability. We define the central configuration parameters for dataset creation, model training, inference, and optional processing stages. We then create the working directory, define reusable execution utilities, and download the NAIP imagery and building footprint labels.
Copy CodeCopiedUse a different Browser
def step2():
info = geoai.get_raster_info(TRAIN_RASTER)
for k, v in info.items():
print(f" {k:<16}: {v}")
print("\n--- per-band statistics ---")
print(geoai.get_raster_stats(TRAIN_RASTER))
print("\n--- vector info ---")
vinfo = geoai.get_vector_info(TRAIN_VECTOR)
for k, v in vinfo.items():
print(f" {k:<16}: {v}")
gdf = gpd.read_file(TRAIN_VECTOR)
print(f"\n {len(gdf)} training buildings | CRS {gdf.crs}")
print(gdf.head(3))
geoai.view_vector(
gdf,
raster_path=TRAIN_RASTER,
outline_only=True,
edge_color="yellow",
outline_linewidth=0.8,
figsize=(11, 11),
title="NAIP training scene + building footprints",
)
try:
display(geoai.view_vector_interactive(TRAIN_VECTOR, layer_name="Buildings"))
except Exception as e:
print(f" (interactive map unavailable here: {e})")
return gdf
LABELS_GDF = timed(step2, "STEP 2 — Inspecting raster + vector data")
TILES_DIR = os.path.join(WORK, "tiles")
def step3():
stats = geoai.export_geotiff_tiles(
in_raster=TRAIN_RASTER,
out_folder=TILES_DIR,
in_class_data=TRAIN_VECTOR,
tile_size=CFG["tile_size"],
stride=CFG["stride"],
buffer_radius=CFG["buffer_radius"],
all_touched=True,
skip_empty_tiles=False,
quiet=False,
)
n_img = len(os.listdir(f"{TILES_DIR}/images"))
n_lbl = len(os.listdir(f"{TILES_DIR}/labels"))
print(f"\n chips: {n_img} images / {n_lbl} masks")
if isinstance(stats, dict):
tot = max(stats.get("total_tiles", n_img), 1)
print(f" tiles containing buildings: {stats.get('tiles_with_features')} "
f"({100 * stats.get('tiles_with_features', 0) / tot:.1f}%)")
print(f" foreground pixels: {stats.get('feature_pixels'):,}")
geoai.display_training_tiles(TILES_DIR, num_tiles=6, figsize=(18, 6))
return stats
TILE_STATS = timed(step3, "STEP 3 — Exporting image chips and label masks")
We inspect the raster and vector datasets to understand their coordinate systems, dimensions, statistics, and feature structures. We visualize the building labels over the aerial imagery and generate an interactive map for spatial exploration. We then divide the source imagery into overlapping georeferenced chips and create matching raster masks for model training.
Copy CodeCopiedUse a different Browser
MODEL_DIR = os.path.join(WORK, "models_unet")
BEST_MODEL = os.path.join(MODEL_DIR, "best_model.pth")
def step4():
geoai.train_segmentation_model(
images_dir=f"{TILES_DIR}/images",
labels_dir=f"{TILES_DIR}/labels",
output_dir=MODEL_DIR,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
encoder_weights=CFG["encoder_weights"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
batch_size=CFG["batch_size"],
num_epochs=CFG["num_epochs"],
learning_rate=CFG["learning_rate"],
val_split=CFG["val_split"],
save_best_only=True,
early_stopping_patience=5,
verbose=True,
)
print(f"\n best checkpoint: {BEST_MODEL}")
print(f" size: {os.path.getsize(BEST_MODEL) / 1e6:.1f} MB")
return BEST_MODEL
timed(step4, f"STEP 4 — Training {CFG['architecture']}/{CFG['encoder']} "
f"for {CFG['num_epochs']} epochs")
def step5():
hist_path = os.path.join(MODEL_DIR, "training_history.pth")
geoai.plot_performance_metrics(
history_path=hist_path,
figsize=(15, 5),
verbose=True,
save_path=os.path.join(WORK, "training_curves.png"),
)
h = torch.load(hist_path, weights_only=False)
best_ep = int(np.argmax(h["val_iou"])) + 1
print(f"\n best val IoU {max(h['val_iou']):.4f} at epoch {best_ep}")
print(" Reading the curves: val loss rising while train loss falls => overfitting;")
print(" both flat and high => underfitting (more epochs, bigger encoder, or more chips).")
timed(step5, "STEP 5 — Training diagnostics")
We train a U-Net semantic segmentation model with a ResNet-34 encoder using the prepared image and mask tiles. We configure the training process with validation splitting, early stopping, checkpoint saving, and performance monitoring. We then load the training history, plot the learning curves, and identify the epoch that produces the highest validation IoU.
Copy CodeCopiedUse a different Browser
PRED_MASK = os.path.join(WORK, "test_prediction.tif")
PRED_PROB = os.path.join(WORK, "test_probability.tif")
def step6():
geoai.semantic_segmentation(
input_path=TEST_RASTER,
output_path=PRED_MASK,
model_path=BEST_MODEL,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
window_size=CFG["window_size"],
overlap=CFG["overlap"],
batch_size=4,
probability_path=PRED_PROB,
)
geoai.print_raster_info(PRED_MASK, show_preview=False)
geoai.plot_prediction_comparison(
original_image=TEST_RASTER,
prediction_image=PRED_MASK,
titles=["NAIP test scene", "Predicted building mask"],
figsize=(16, 8),
prediction_colormap="viridis",
save_path=os.path.join(WORK, "prediction_comparison.png"),
)
with rasterio.open(PRED_MASK) as src:
m = src.read(1)
px = float(abs(src.transform.a) * abs(src.transform.e))
print(f" predicted building pixels: {int((m > 0).sum()):,} "
f"({100 * (m > 0).mean():.2f}% of scene, ~{(m > 0).sum() * px:,.0f} m2)")
timed(step6, "STEP 6 — Sliding-window inference on the test scene")
VEC_RAW = os.path.join(WORK, "buildings_raw.geojson")
VEC_ORTHO = os.path.join(WORK, "buildings_orthogonal.geojson")
VEC_FINAL = os.path.join(WORK, "buildings_final.geojson")
def step7():
grouped = geoai.region_groups(
PRED_MASK,
connectivity=2,
min_size=50,
out_image=os.path.join(WORK, "test_prediction_cleaned.tif"),
)
clean_mask = os.path.join(WORK, "test_prediction_cleaned.tif")
raw = geoai.raster_to_vector(
clean_mask,
output_path=VEC_RAW,
threshold=0,
min_area=15,
simplify_tolerance=0.5,
)
print(f" raw polygons : {len(raw)}")
ortho = geoai.orthogonalize(
input_path=clean_mask,
output_path=VEC_ORTHO,
epsilon=1.5,
min_area=15,
)
print(f" orthogonalized : {len(ortho)}")
final = geoai.regularization(ortho, angle_tolerance=12, simplify_tolerance=0.4)
final = geoai.add_geometric_properties(
final, properties=["area", "perimeter", "solidity", "elongation", "orientation"]
)
final.to_file(VEC_FINAL, driver="GeoJSON")
print(f" final footprints : {len(final)}")
print(final.head())
if "area" in final.columns:
print("\n footprint area stats (m2):")
print(final["area"].describe().round(1).to_string())
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
with rasterio.open(TEST_RASTER) as src:
rgb = src.read([1, 2, 3]).transpose(1, 2, 0)
rgb = np.clip(rgb / np.percentile(rgb, 99), 0, 1)
ext = plotting_extent(src)
for ax, g, t in zip(axes, [raw, final], ["Raw polygonization", "Orthogonalized + regularized"]):
ax.imshow(rgb, extent=ext)
g.plot(ax=ax, facecolor="none", edgecolor="red", linewidth=1.1)
ax.set_title(t)
ax.set_axis_off()
plt.tight_layout()
plt.show()
try:
display(geoai.view_vector_interactive(VEC_FINAL, layer_name="Predicted buildings"))
except Exception:
pass
return final
FINAL_GDF = timed(step7, "STEP 7 — Vectorizing and regularizing the predicted footprints")
We apply sliding-window inference to an unseen NAIP scene and generate both prediction and probability rasters. We remove small noisy regions, convert the predicted mask into vector polygons, and regularize the footprint geometries to produce cleaner building boundaries. We also calculate geometric properties and compare the raw polygonized results with the orthogonalized and regularized outputs.
Copy CodeCopiedUse a different Browser
def step8():
gt_raster = os.path.join(WORK, "train_gt_mask.tif")
geoai.vector_to_raster(
vector_path=TRAIN_VECTOR,
output_path=gt_raster,
reference_raster=TRAIN_RASTER,
fill_value=0,
all_touched=True,
dtype=np.uint8,
)
train_pred = os.path.join(WORK, "train_prediction.tif")
geoai.semantic_segmentation(
input_path=TRAIN_RASTER,
output_path=train_pred,
model_path=BEST_MODEL,
architecture=CFG["architecture"],
encoder_name=CFG["encoder"],
num_channels=CFG["num_channels"],
num_classes=CFG["num_classes"],
window_size=CFG["window_size"],
overlap=CFG["overlap"],
quiet=True,
)
metrics = geoai.calc_segmentation_metrics(
ground_truth=gt_raster,
prediction=train_pred,
num_classes=2,
metrics=["iou", "f1"],
)
print("\n --- pixel-wise metrics (class 0 = background, class 1 = building) ---")
for k, v in metrics.items():
print(f" {k:<10}: {np.round(v, 4)}")
print("\n Building-class IoU is the number that matters; background IoU is inflated")
print(" by the huge negative class and always looks great.")
geoai.plot_prediction_comparison(
original_image=TRAIN_RASTER,
prediction_image=train_pred,
ground_truth_image=gt_raster,
titles=["Imagery", "Prediction", "Ground truth"],
figsize=(18, 6),
save_path=os.path.join(WORK, "accuracy_comparison.png"),
)
return metrics
METRICS = timed(step8, "STEP 8 — Quantitative accuracy assessment")
def step9():
if not CFG["run_zero_shot"]:
print(" disabled in CFG"); return
chip = os.path.join(WORK, "test_chip.tif")
with rasterio.open(TEST_RASTER) as src:
b = src.bounds
cx, cy = (b.left + b.right) / 2, (b.bottom + b.top) / 2
half = min((b.right - b.left), (b.top - b.bottom)) / 6
bbox = [cx - half, cy - half, cx + half, cy + half]
geoai.clip_raster_by_bbox(TEST_RASTER, chip, bbox=bbox, bbox_type="geo")
print(f" clipped chip: {chip}")
sam = geoai.GroundedSAM(
detector_id="IDEA-Research/grounding-dino-tiny",
segmenter_id="facebook/sam-vit-base",
tile_size=1024,
overlap=128,
threshold=0.3,
)
out_mask = os.path.join(WORK, "zeroshot_mask.tif")
gdf = sam.segment_image(
input_path=chip,
output_path=out_mask,
text_prompts=["building", "house", "rooftop"],
polygon_refinement=True,
export_polygons=True,
min_polygon_area=30,
simplify_tolerance=1.5,
)
geoai.plot_prediction_comparison(
original_image=chip,
prediction_image=out_mask,
titles=["Chip", "Zero-shot: 'building / house / rooftop'"],
figsize=(14, 7),
prediction_colormap="viridis",
)
print(f" zero-shot objects found: {len(gdf) if gdf is not None else 0}")
geoai.empty_cache()
timed(step9, "STEP 9 — Zero-shot text-prompted segmentation (Grounding DINO + SAM)")
We evaluate the segmentation model by comparing its predictions with rasterized ground-truth building labels. We calculate pixel-level IoU and F1 metrics and visualize the imagery, predictions, and reference masks together. We then apply Grounding DINO and SAM to perform zero-shot building segmentation using text prompts without additional model training.
Copy CodeCopiedUse a different Browser
def step10():
if not CFG["run_pretrained"]:
print(" disabled in CFG"); return
extractor = geoai.BuildingFootprintExtractor(model_path="building_footprints_usa.pth")
gdf = extractor.process_raster(
TEST_RASTER,
output_path=os.path.join(WORK, "buildings_maskrcnn.geojson"),
batch_size=4,
confidence_threshold=0.5,
overlap=0.25,
mask_threshold=0.5,
min_object_area=100,
filter_edges=True,
)
if gdf is None or len(gdf) == 0:
print(" no instances returned"); return
print(f" building instances: {len(gdf)}")
reg = extractor.regularize_buildings(gdf, min_area=20, angle_threshold=15)
reg.to_file(os.path.join(WORK, "buildings_maskrcnn_regularized.geojson"), driver="GeoJSON")
extractor.visualize_results(TEST_RASTER, gdf=reg, figsize=(12, 12))
if FINAL_GDF is not None:
print(f"\n your U-Net : {len(FINAL_GDF)} polygons")
print(f" pretrained R-CNN: {len(reg)} polygons")
print(" Different counts are expected: U-Net merges adjacent roofs, Mask R-CNN splits")
print(" them into instances. Pick the paradigm that matches your downstream question.")
geoai.empty_cache()
timed(step10, "STEP 10 — Pretrained Mask R-CNN instance segmentation")
def step11():
if not CFG["run_real_aoi"]:
print(" disabled (set CFG['run_real_aoi'] = True to run; needs open internet)")
return
bbox = (-83.9400, 35.9500, -83.9250, 35.9600)
items = geoai.pc_stac_search(
collection="naip",
bbox=list(bbox),
time_range="2021-01-01/2023-12-31",
max_items=3,
)
print(f" STAC items found: {len(items)}")
aoi_dir = os.path.join(WORK, "aoi")
tif = geoai.download_naip(bbox=bbox, output_dir=aoi_dir, max_items=1, preview=False)
print(f" NAIP: {tif}")
ovt = os.path.join(aoi_dir, "overture_buildings.geojson")
geoai.download_overture_buildings(bbox=bbox, output=ovt, overture_type="building")
print(f" Overture buildings: {ovt}")
print(geoai.extract_building_stats(ovt))
raster = tif[0] if isinstance(tif, (list, tuple)) else tif
geoai.export_geotiff_tiles(
in_raster=raster,
out_folder=os.path.join(aoi_dir, "tiles"),
in_class_data=ovt,
tile_size=512,
stride=256,
)
print(" AOI dataset ready — feed it to train_segmentation_model() exactly as in STEP 4.")
timed(step11, "STEP 11 — (optional) Real AOI: Planetary Computer NAIP + Overture Maps labels")
def step12():
outputs = [f for f in sorted(os.listdir(WORK))
if f.endswith((".tif", ".geojson", ".png", ".pth"))]
print(" artifacts produced:")
for f in outputs:
print(f" {os.path.getsize(os.path.join(WORK, f)) / 1e6:8.2f} MB {f}")
zip_path = os.path.join(WORK, "geoai_results.zip")
subprocess.run(
["zip", "-qr", zip_path, ".", "-i", "*.geojson", "*.png", "*.tif", "-x", "*tiles*"],
cwd=WORK, check=False,
)
print(f"\n bundle: {zip_path}")
if IN_COLAB:
print(" Download it with: from google.colab import files; "
"files.download('%s')" % zip_path)
timed(step12, "STEP 12 — Results summary")
banner("DONE")
print("""
Where to go next
- Swap the head, keep the code: architecture="deeplabv3plus", encoder_name="efficientnet-b3"
(or any timm encoder) in train_segmentation_model().
- 4-band NAIP (RGB+NIR): num_channels=4 everywhere; the first conv is auto-adapted.
- Multi-class land cover: geoai.train_segmentation_landcover() + geoai.export_landcover_tiles(),
with DiceLoss / FocalLoss / TverskyLoss from geoai.landcover_train for class imbalance.
- Instance segmentation you train yourself: geoai.train_MaskRCNN_model() then
geoai.instance_segmentation(...
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み