HERE Technologies and Wherobots announce partnership Learn More

Building NAIP mosaics with RasterFlow

This notebook demonstrates how to generate high-resolution aerial mosaics from the National Agriculture Imagery Program (NAIP) using Wherobots RasterFlow’s built-in NAIP datasets. You will learn how to define an Area of Interest, build a NAIP composite mosaic, and visualize the results as RGB imagery.

It mirrors the Sentinel-2 mosaic notebook (RasterFlow_S2_Mosaic.ipynb), but swaps satellite imagery for NAIP’s sub-meter aerial imagery.

Built-in NAIP datasets

RasterFlow ships four built-in NAIP datasets, selectable by name in the build_mosaics task’s datasets input. They come from NAIP’s two imagery products, each at 30 cm and 60 cm, and all of them store 8-bit (Byte) values:

Dataset NAIP product Bands Resolution
naip_rgb_30cm RGB (visual) red, green, blue 30 cm
naip_rgb_60cm RGB (visual) red, green, blue 60 cm
naip_30cm 4-band (analytic) red, green, blue, near-infrared 30 cm
naip_60cm 4-band (analytic) red, green, blue, near-infrared 60 cm

This notebook uses the RGB datasets. They are what the Tile2Net, Meta CHM and SAM3 recipes run on, and with three bands instead of four there is less imagery to read and store. The RGB product uses lossy JPEG compression, so use a 4-band dataset when you need near-infrared (for example for NDVI or false-color views), exact pixel values, or a model that takes near-infrared, such as ChesapeakeRSC.

A built-in mosaic’s bands are labeled <dataset>:<band> — for example naip_rgb_30cm:red, naip_rgb_30cm:green and naip_rgb_30cm:blue. The 4-band datasets add a nir band, as in naip_30cm:nir.

NAIP is flown state-by-state on a multi-year cycle, and the available resolution varies by state and year. RasterFlow selects imagery for the requested resolution and date window from its built-in NAIP index; if a location has no imagery at the requested resolution in that window, the mosaic will be empty. We use two AOIs below, each chosen because it has coverage at the target resolution:

  • 30 cm → College Park, MD (2023) — the AOI from the SAM3 notebook.
  • 60 cm → Nashua, NH (2021) — the AOI from the CHM notebook.

Setup and Imports

import shapely
import wkls
from sedona.spark import *

config = SedonaContext.builder().getOrCreate()
sedona = SedonaContext.create(config)

Using the Wherobots Tasks API

Each RasterFlow workflow runs as a task through the Wherobots Python SDK. run_task below starts a task run, waits for it to finish, and returns the URI of its output. Call wb.tasks.get("<task name>").input_schema to see every input a task accepts.

import time

import shapely

from wherobots import TaskRunState, Wherobots

# In a Wherobots notebook the SDK uses your Wherobots credentials, elsewhere it reads WHEROBOTS_API_KEY
wb = Wherobots()


def run_task(task_name, inputs, poll_seconds=30):
    """Run a RasterFlow task, wait for it to finish, and return its output URI."""
    run = wb.task_runs.get(wb.tasks.run(task_name, inputs, region="aws-us-west-2").task_run_id)
    print(f"Started {task_name} run {run.task_run_id}")
    while run.is_active:
        time.sleep(poll_seconds)
        run = wb.task_runs.get(run.task_run_id)
    if run.state != TaskRunState.SUCCEEDED:
        raise RuntimeError(f"{task_name} run {run.task_run_id} ended {run.state}: {run.error_message}")
    # A successful run can temporarily have no results if the output read fails.
    for _ in range(3):
        if run.results is not None:
            break
        time.sleep(poll_seconds)
        run = wb.task_runs.get(run.task_run_id)
    if run.results is None:
        raise RuntimeError(
            f"Results for successful run {run.task_run_id} are unavailable. "
            "Fetch this run again with wb.task_runs.get(run_id); do not resubmit the task."
        )
    # Each RasterFlow task has one output, named by the task (e.g. mosaic_index_uri)
    (output_uri,) = run.results.values()
    return output_uri

30 cm NAIP mosaic — College Park, MD

We build a 30 cm RGB NAIP mosaic over College Park, Maryland using the built-in naip_rgb_30cm dataset. College Park has 30 cm NAIP coverage in 2023, so we use a date window covering that year. (This is the same AOI used in the SAM3 notebook, RasterFlow_SAM3.ipynb.)

Preview: example mosaic

A pre-generated NAIP 30 cm optimized mosaic over College Park is available in the Wherobots Cloud map viewer, so you can see the kind of output this section builds without waiting for the build to finish.

View the interactive map here.

Note: This pre-generated sample was built from the 4-band NAIP product via the GTI approach (see RasterFlow_Bring_Your_Own_Rasters_NAIP.ipynb), so its store also holds a near-infrared band and its bands are labeled r / g / b / ir. The preview renders only the RGB bands. A mosaic you build with the built-in naip_rgb_30cm dataset has only the three RGB bands, labeled naip_rgb_30cm:red / naip_rgb_30cm:green / naip_rgb_30cm:blue.

Selecting the Area of Interest (AOI)

We use College Park, Maryland — an urban/suburban area in the Northeastern US with recent 30 cm NAIP coverage.

# Generate a WKT geometry for College Park, MD using Well-Known Locations (https://github.com/wherobots/wkls)
collegepark_aoi = wkls.us.md.collegepark.wkt()

min_lon, min_lat, max_lon, max_lat = shapely.from_wkt(collegepark_aoi).bounds
print(f"Bounds: [{min_lon:.4f}, {min_lat:.4f}, {max_lon:.4f}, {max_lat:.4f}]")

Building the 30 cm mosaic

This step builds an RGB NAIP mosaic at 30 cm resolution using the built-in naip_rgb_30cm dataset. RasterFlow queries its built-in index of the RGB product for 30 cm imagery intersecting the AOI within the date window, reprojects/resamples into the target CRS, and mosaics the tiles into a Zarr store with red, green, and blue bands.

Note: This step can take several minutes to complete. If you already have a pre-built mosaic store, you can skip this build by assigning its URI directly in the next cell.

# Optionally, you can skip running the build cell below and use this pre-generated mosaic store:
# mosaic_store = "s3://wherobots-examples/rasterflow/mosaics/collegepark_optimized.zarr"
mosaic_index_uri = run_task(
    "build_mosaics",
    {
        # Built-in RGB NAIP 30 cm dataset (red, green, blue)
        "datasets": ["naip_rgb_30cm"],

        # Area of interest as WKT in EPSG:4326
        "aoi": collegepark_aoi,

        # Acquisition window — College Park has 30 cm NAIP coverage in 2023
        "start": "2023-01-01T00:00:00Z",
        "end": "2024-01-01T00:00:00Z",

        # Output CRS (Web Mercator)
        "target_crs": "EPSG:3857",
    },
)

# Inspect the mosaic index (one row per output mosaic location)
mosaic_index_df = sedona.read.format("geoparquet").load(mosaic_index_uri)
mosaic_index_df.show(truncate=60)
# This example AOI has one geometry, so we select the first (only) mosaic location.
mosaic_store = mosaic_index_df.first()["location"]

mosaic_store

Build an optimized Zarr and visualize the output

RasterFlow writes its outputs as Zarr stores at native resolution, which isn’t ideal for interactive viewing — every pan or zoom would have to stream full-resolution pixels. build_zarr_multiscales adds downsampled overview levels (image pyramids) so the map can stream coarse tiles when zoomed out and full-resolution pixels when zoomed in. This typically takes a few minutes for large outputs.

Once built, open it in the Wherobots Cloud map viewer with a cloud.wherobots.com/map link to the store’s URI.

Pass aoi to select a chunk-aligned bounding window from a single Zarr source. It accepts polygon or multipolygon WKT in EPSG:4326, or an S3 GeoJSON or GeoParquet URI with a CRS. Pixels outside the polygon remain within that rectangular window. Omit aoi to process the full store.

from urllib.parse import quote

optimized_store = run_task("build_zarr_multiscales", {"source_store": mosaic_store, "aoi": collegepark_aoi})

print(f"Optimized store: {optimized_store}")
# Open the freshly built mosaic in the Wherobots Cloud map viewer:
print(f"View on the map: https://cloud.wherobots.com/map?l0.src={quote(optimized_store, safe='')}")

60 cm NAIP mosaic — Nashua, NH

The naip_rgb_60cm dataset builds a mosaic from NAIP’s 60 cm RGB imagery. Pick 60 cm over 30 cm when:

  • You need coverage for a year or location that wasn’t flown at 30 cm — 60 cm imagery is available for more states and more years.
  • You’re mosaicking a large area and want smaller outputs and faster builds — 60 cm imagery is roughly a quarter of the pixels of 30 cm.

We build over Nashua, NH, which has a mix of urban, park, and forest cover. New Hampshire has 60 cm NAIP coverage in 2021, so we use a date window covering that year. (This is the same AOI used in the CHM notebook, RasterFlow_CHM.ipynb.)

Preview: example mosaic

A pre-generated 60 cm NAIP mosaic over Nashua, NH is available in the Wherobots Cloud map viewer. It was built from the 4-band NAIP product, so its store also holds a near-infrared band; a mosaic you build with naip_rgb_60cm has only red, green, and blue.

View the interactive map here.

Selecting the AOI and building the mosaic

We build the Nashua mosaic with naip_rgb_60cm the same way as the 30 cm mosaic above. As in the 30 cm example above, you can add an additional cell to pass the resulting store to build_zarr_multiscales and open it in the Wherobots Cloud map viewer to view your freshly built output.

# Generate a WKT geometry for Nashua, NH using Well-Known Locations (https://github.com/wherobots/wkls)
nashua_aoi = wkls.us.nh.nashua.wkt()

min_lon, min_lat, max_lon, max_lat = shapely.from_wkt(nashua_aoi).bounds
print(f"Bounds: [{min_lon:.4f}, {min_lat:.4f}, {max_lon:.4f}, {max_lat:.4f}]")
coarse_index_uri = run_task(
    "build_mosaics",
    {
        # Built-in RGB NAIP 60 cm dataset (red, green, blue)
        "datasets": ["naip_rgb_60cm"],

        # Area of interest as WKT in EPSG:4326
        "aoi": nashua_aoi,

        # Acquisition window — New Hampshire has 60 cm NAIP coverage in 2021
        "start": "2021-01-01T00:00:00Z",
        "end": "2022-01-01T00:00:00Z",

        # Output CRS (Web Mercator)
        "target_crs": "EPSG:3857",
    },
)

coarse_store = sedona.read.format("geoparquet").load(coarse_index_uri).first()["location"]
print(f"60 cm mosaic store: {coarse_store}")

Summary

This notebook demonstrated how to:

  1. Build a 30 cm RGB NAIP aerial mosaic over College Park, MD with the built-in naip_rgb_30cm dataset
  2. Optimize the Zarr store with overviews and open it in the Wherobots Cloud map viewer
  3. Build a 60 cm RGB NAIP mosaic over Nashua, NH with naip_rgb_60cm

Next steps

  • Compare a NAIP mosaic with the Sentinel-2 mosaic over the same AOI (see RasterFlow_S2_Mosaic.ipynb)
  • Run a model on the NAIP mosaic — for example text-prompted object detection with SAM3 on the College Park mosaic (RasterFlow_SAM3.ipynb) or tree canopy height with Meta CHM on the Nashua mosaic (RasterFlow_CHM.ipynb)
  • Use the mosaic as input for model inference using the run_mosaic_inference task
  • Build a 4-band mosaic with naip_30cm or naip_60cm when you need the near-infrared band, for example to compute NDVI