A Tutorial on GeoAI: Designing Footprint Extraction from NAIP Imagery Using U-Net, Grounding DINO, SAM, and Mask R-CNN
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.
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: 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")
[truncated for AI cost control]