Building Change Detection with ChangeStar¶
This notebook demonstrates building change detection using the ChangeStar model from the torchange package, integrated into GeoAI.
Overview¶
ChangeStar is a building change detection model that uses Changen2 pre-trained weights. It takes two images (before/after) and outputs:
- Change map: A binary map highlighting areas where buildings have changed.
- Semantic segmentation (T1): Building footprints in the first (before) image.
- Semantic segmentation (T2): Building footprints in the second (after) image.
The GeoAI integration provides:
- GeoTIFF I/O: Read and write georeferenced raster data with proper CRS and transform.
- Tiled processing: Handle large rasters by splitting into overlapping tiles.
- Vector output: Export change polygons as GeoJSON, GeoPackage, or Shapefile.
- Visualization: Built-in methods for plotting results.
Install packages¶
# %pip install geoai-py
Import libraries¶
import os
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
import geoai
from geoai.change_detection import (
ChangeStarDetection,
changestar_detect,
list_changestar_models,
)
List available models¶
ChangeStar offers several model variants trained with different Changen2 pre-training strategies.
models = list_changestar_models()
for short_name, full_name in models.items():
print(f"{short_name:30s} -> {full_name}")
Setup¶
# Check available device
device = geoai.get_device()
print(f"Using device: {device}")
# Set up output directory
out_folder = "changestar_results"
Path(out_folder).mkdir(exist_ok=True)
print(f"Output directory: {out_folder}")
Download sample data¶
We'll use NAIP imagery for Las Vegas to demonstrate building change detection.
naip_2019_url = "https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_naip_2019_a.tif"
naip_2022_url = "https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_naip_2022_a.tif"
naip_2019_path = geoai.download_file(naip_2019_url)
naip_2022_path = geoai.download_file(naip_2022_url)
print(f"Downloaded 2019 NAIP: {naip_2019_path}")
print(f"Downloaded 2022 NAIP: {naip_2022_path}")
Visualize input imagery¶
geoai.view_raster(naip_2019_path)
geoai.view_raster(naip_2022_path)
Initialize ChangeStar model¶
Create a ChangeStarDetection instance. The model weights are automatically downloaded on first use.
detector = ChangeStarDetection(model_name="s1_s1c1_vitb")
Run change detection¶
The predict method takes two GeoTIFF images and returns change maps and semantic segmentation results.
result = detector.predict(
naip_2019_path,
naip_2022_path,
output_change=os.path.join(out_folder, "change_map.tif"),
output_t1_semantic=os.path.join(out_folder, "t1_buildings.tif"),
output_t2_semantic=os.path.join(out_folder, "t2_buildings.tif"),
output_vector=os.path.join(out_folder, "changes.gpkg"),
)
print("Result keys:", list(result.keys()))
for key, value in result.items():
if hasattr(value, "shape"):
print(f" {key}: shape={value.shape}, dtype={value.dtype}")
else:
print(f" {key}: {value}")
Visualize results¶
fig = detector.visualize(
naip_2019_path,
naip_2022_path,
result=result,
figsize=(25, 5),
title1="NAIP 2019",
title2="NAIP 2022",
)
plt.show()
Visualize with overlay¶
This shows the building segmentation and change detection overlaid on the original imagery.
fig = detector.visualize_overlay(
naip_2019_path,
naip_2022_path,
result=result,
figsize=(20, 6),
title1="NAIP 2019",
title2="NAIP 2022",
)
plt.show()
Using the convenience function¶
For quick one-off analysis, use the changestar_detect() function.
result2 = changestar_detect(
naip_2019_path,
naip_2022_path,
model_name="s1_s1c1_vitb",
output_change=os.path.join(out_folder, "change_map_v2.tif"),
)
print(f"Change pixels: {result2['change_map'].sum():,}")
print(
f"Change area: {result2['change_map'].sum() / result2['change_map'].size * 100:.2f}%"
)
Comparing model variants¶
Let's compare the s1 and s9 model variants.
# S1 model (already computed above)
result_s1 = result
# S9 model
detector_s9 = ChangeStarDetection(model_name="s9_s9c1_vitb")
result_s9 = detector_s9.predict(naip_2019_path, naip_2022_path)
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
axes[0].imshow(result_s1["change_map"], cmap="gray")
axes[0].set_title(f"S1 Model\n(Changed pixels: {result_s1['change_map'].sum():,})")
axes[0].axis("off")
axes[1].imshow(result_s9["change_map"], cmap="gray")
axes[1].set_title(f"S9 Model\n(Changed pixels: {result_s9['change_map'].sum():,})")
axes[1].axis("off")
# Overlay comparison
combined = np.zeros((*result_s1["change_map"].shape, 3), dtype=np.uint8)
combined[result_s1["change_map"] == 1, 0] = 255 # S1 in red
combined[result_s9["change_map"] == 1, 2] = 255 # S9 in blue
# Both in magenta
both = (result_s1["change_map"] == 1) & (result_s9["change_map"] == 1)
combined[both] = [255, 0, 255]
axes[2].imshow(combined)
axes[2].set_title("Comparison\n(Red=S1 only, Blue=S9 only, Magenta=Both)")
axes[2].axis("off")
plt.tight_layout()
plt.show()
Adjusting the threshold¶
The default probability threshold is 0.5. You can adjust it to be more or less sensitive.
thresholds = [0.3, 0.5, 0.7]
fig, axes = plt.subplots(1, len(thresholds), figsize=(18, 6))
for ax, thresh in zip(axes, thresholds):
result_t = detector.predict(naip_2019_path, naip_2022_path, threshold=thresh)
ax.imshow(result_t["change_map"], cmap="gray")
ax.set_title(
f"Threshold = {thresh}\n" f"(Changed pixels: {result_t['change_map'].sum():,})"
)
ax.axis("off")
plt.tight_layout()
plt.show()
View saved outputs¶
# List saved files
for f in sorted(os.listdir(out_folder)):
fpath = os.path.join(out_folder, f)
size_mb = os.path.getsize(fpath) / 1024 / 1024
print(f"{f:40s} {size_mb:.2f} MB")
# View the change map GeoTIFF
geoai.view_raster(os.path.join(out_folder, "change_map.tif"))