Download high-resolution NAIP satellite imagery for any polygon you can describe — universities, farms, watersheds, parks, custom study areas — using Google Earth Engine. Hand it a GeoJSON or a list of shapely geometries; it hands you back GeoTIFFs clipped to your shapes.
Originally built to harvest aerial imagery for ~3,000 U.S. universities. Generalised here so you don't have to write the same plumbing again.
NAIP imagery clipped to University of Rochester campus polygons — October 2021, ~0.6 m/pixel.
Imagery courtesy USDA FSA NAIP (public domain).
- Bring your own polygons. GeoDataFrame, GeoJSON, Shapefile, GeoPackage, KML, raw shapely geometry, dict — they all work.
- Inventory first, download second. Find which months of NAIP exist for each shape before committing to a full download.
- Resilient downloads. Automatic fallback through multiple resolutions when an export hits Earth Engine's payload limit. Resumable: re-running skips files you already have.
- Concurrent. Thread pool for parallel inventory queries and downloads.
- Typed. Ships a
py.typedmarker — full mypy support out of the box. - MIT licensed. Use it commercially, fork it, embed it.
pip install "geoimagery[all]"Install from source
git clone https://github.com/hpa-code/geoimagery
cd geoimagery
python3 -m venv .venv
source .venv/bin/activate
pip install ".[all]"Each user needs their own free Google Earth Engine account and a Cloud project — credentials cannot be shared.
-
Sign up at https://earthengine.google.com/signup/ (noncommercial use is free).
-
Create or select a Google Cloud project and enable the Earth Engine API.
-
Authenticate once on your machine:
earthengine authenticate
That stores a token under ~/.config/earthengine/. You won't need to repeat it.
This is the recommended way. If you have a GeoJSON, Shapefile, or GeoPackage, you don't need to write any Python.
export GEE_PROJECT=your-gcp-project-id
python examples/from_geojson.py path/to/your_areas.geojson ./naip_outputThat will:
- build an availability inventory at
naip_output/availability.csv - download every available NAIP month for every polygon into
naip_output/ - write a per-row status log at
naip_output/download_log.csv
Safe to re-run — already-downloaded files are skipped, so you can Ctrl+C and resume any time.
For scripting, pipelines, or when you need more control.
import geoimagery as gi
# 1. Initialise Earth Engine (uses your stored credentials).
gi.initialize(project="my-gcp-project")
# 2. Find what NAIP imagery is available for your areas.
inventory = gi.list_available_dates(
"my_areas.geojson",
start_date="2022-01-01",
end_date="2024-12-31",
)
inventory.to_csv("availability.csv", index=False)
# 3. Download every available month for every polygon, clipped to each shape.
results = gi.download(
"my_areas.geojson",
dates=inventory,
output_dir="./naip_output",
max_workers=5,
)
results.to_csv("download_log.csv", index=False)./naip_output/ now contains one .tif per (area, month), named like area-id_area-name_June_2023.tif.
from shapely.geometry import box
import geoimagery as gi
gi.initialize(project="my-gcp-project")
aoi = box(-77.62, 43.12, -77.60, 43.14) # small patch of Rochester, NY
gi.download(aoi, dates=["June 2023"], output_dir="./out")gi.download(
my_geodataframe,
dates=["June 2022", "August 2023", "May 2024"],
output_dir="./out",
)Every function accepts any of these — no conversion needed:
| Input | Example |
|---|---|
| Path to a vector file | "areas.geojson", "areas.shp", Path("areas.gpkg") |
| GeoDataFrame | gpd.read_file(...) |
| Single shapely geometry | box(xmin, ymin, xmax, ymax) |
| List of shapely geometries | [poly1, poly2, poly3] |
| GeoJSON dict | {"type": "FeatureCollection", "features": [...]} |
If your file uses non-standard ID/name columns:
gi.download(
"farms.shp",
dates=["July 2024"],
output_dir="./out",
id_column="FARM_ID",
name_column="OWNER",
)For each (polygon × month) pair, geoimagery writes a 3-band (R, G, B) GeoTIFF clipped exactly to your polygon boundary. Filenames follow the pattern {id}_{name}_{Month}_{Year}.tif.
download_log.csv records the outcome of every attempt: Downloaded, Already Downloaded, No Data for Month, Download Failed, or Error: ....
NAIP itself is public domain (USDA Farm Service Agency) and free to use, including commercially. Google Earth Engine has separate quotas:
- The noncommercial tier is free but has concurrent-request limits (~40) and per-request payload caps (~32 MB / 10k×10k pixels). geoimagery handles the payload cap by automatically falling back through 0.6 m → 1 m → 2 m → 4 m resolutions.
- For commercial use, see Google's Earth Engine pricing page.
You are responsible for staying within the tier appropriate to your use.
| Function | Purpose |
|---|---|
initialize(project=...) |
Initialise the Earth Engine client. Call once per process. |
list_available_dates(source, start_date, end_date) |
Return a DataFrame of NAIP months available for each input geometry. |
download(source, dates, output_dir, ...) |
Download clipped GeoTIFFs. |
load_geometries(source, ...) |
Normalise any accepted input to a WGS84 GeoDataFrame. |
parse_available_dates(value), build_month_window(label), sanitize_filename_component(s) |
Pure-Python helpers. |
Full docstrings: python -c "import geoimagery; help(geoimagery)".
Issues, PRs, and feedback are very welcome. See CONTRIBUTING.md for development setup. By participating you agree to the Code of Conduct.
- This project is licensed under the MIT License — see LICENSE.
- NAIP imagery is in the public domain, courtesy of the USDA Farm Service Agency. The customary courtesy citation when redistributing imagery is "Imagery courtesy USDA FSA NAIP."
- Google Earth Engine is a trademark of Google LLC. NAIP is a program of the USDA Farm Service Agency. This project is not affiliated with, endorsed by, or sponsored by Google LLC or the USDA. Use of the library is subject to the Google Earth Engine Terms of Service.
If you use geoimagery in academic work, please cite it via CITATION.cff (GitHub renders a "Cite this repository" button automatically).