rockglacierNET / patch_dataset /collect_region.py
kabiriitb's picture
Upload folder using huggingface_hub
80e070e verified
Raw History Blame Contribute Delete
10.6 kB
"""
RockGlacierNet β€” Region collection (whole-Sikkim grid). [lives in patch_dataset/]
Grids the AOI (bounding box of all inventory polygons + AOI_MARGIN_M ~= the Sikkim
glacier belt) into RAW_TILE_PX tiles and downloads, per tile:
* a 12-band Sentinel-2 + terrain GeoTIFF, and
* a 1-band mask rasterized from ALL polygons onto that tile's exact grid.
REGION_MODE = "sampled" : every tile touching a glacier + REGION_BG_KEEP background tiles.
REGION_MODE = "dense" : every tile in the AOI (full mosaic, multi-GB).
Robustness: the median composite is kept light with a scene-level cloud filter
(S2_CLOUD_PCT); if a tile still trips GEE "User memory limit exceeded", it is
auto-split into a finer sub-grid, each part downloaded and mosaicked in memory.
Outputs (nothing outside patch_dataset/):
patch_dataset/raw_region/features/tile_RRRR_CCCC.tif
patch_dataset/raw_region/masks/tile_RRRR_CCCC.tif
Run (env with ee+geemap+geopandas+rasterio, e.g. `brats`):
python patch_dataset/collect_region.py # sampled (default)
python patch_dataset/collect_region.py --mode dense
python patch_dataset/collect_region.py --limit 3 # smoke test
All tunables live in patch_dataset/config.py.
"""
import os
import sys
import time
import argparse
import requests
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # import local config
import config as C
import ee
import geopandas as gpd
import rasterio
from rasterio.io import MemoryFile
from rasterio.merge import merge as rio_merge
from rasterio.features import rasterize
from shapely.geometry import box
# ─────────────────────────────────────────────────────────────────────────────
# Earth Engine: memory-light 12-band composite (band order == C.BAND_NAMES)
# ─────────────────────────────────────────────────────────────────────────────
def feature_image(roi):
coll = (ee.ImageCollection(C.S2_COLLECTION)
.filterBounds(roi)
.filterDate(C.S2_DATE_START, C.S2_DATE_END)
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", C.S2_CLOUD_PCT)))
def mask_clouds(im):
qa = im.select("QA60")
m = qa.bitwiseAnd(1 << 10).eq(0).And(qa.bitwiseAnd(1 << 11).eq(0))
return im.updateMask(m).divide(C.S2_SR_DIVISOR)
s2 = coll.map(mask_clouds).median()
ndvi = s2.normalizedDifference(["B8", "B4"]).rename("NDVI")
ndwi = s2.normalizedDifference(["B3", "B8"]).rename("NDWI")
ndsi = s2.normalizedDifference(["B3", "B11"]).rename("NDSI")
s2 = s2.select(["B2", "B3", "B4", "B8", "B11", "B12"]).addBands([ndvi, ndwi, ndsi])
dem = ee.ImageCollection(C.DEM_COLLECTION).select("DEM").mosaic()
# CRITICAL: reproject to a METRIC grid before ee.Terrain. After .mosaic() the
# image defaults to EPSG:4326 (degrees); computing slope there yields garbage
# (~1 deg everywhere). Reproject to UTM @30 m so slope/aspect are correct.
dem_m = dem.reproject(crs=C.TARGET_CRS, scale=30)
terr = ee.Terrain.products(dem_m)
img = (s2.addBands(dem_m.rename("Elev"))
.addBands(terr.select("slope").rename("Slope"))
.addBands(terr.select("aspect").rename("Aspect")))
return img.clip(roi)
# ─────────────────────────────────────────────────────────────────────────────
# Download helpers (UTM bbox -> GeoTIFF bytes, with memory-aware sub-tiling)
# ─────────────────────────────────────────────────────────────────────────────
def _utm_to_ee_roi(bbox_utm):
ll = gpd.GeoSeries([box(*bbox_utm)], crs=C.TARGET_CRS).to_crs("EPSG:4326").total_bounds
return ee.Geometry.Rectangle([ll[0], ll[1], ll[2], ll[3]])
def _download_bytes(bbox_utm, tries=3):
"""Download one bbox as GeoTIFF bytes. Raises MemoryError on GEE memory limit."""
roi = _utm_to_ee_roi(bbox_utm)
url = feature_image(roi).getDownloadURL(
{"region": roi, "scale": C.SCALE_M, "crs": C.TARGET_CRS, "format": "GEO_TIFF"})
last = None
for k in range(tries):
r = requests.get(url, timeout=300)
if r.status_code == 200 and r.content[:2] in (b"II", b"MM"): # valid TIFF magic
return r.content
last = r.content[:200]
if r.status_code == 400 and b"memory limit" in r.content.lower():
raise MemoryError(last) # signal: split further
time.sleep(2 * (k + 1)) # transient: retry
raise RuntimeError(f"download failed: {last}")
def fetch_tile(bbox_utm, max_split=4):
"""Download bbox; on GEE memory error, recursively split into a finer grid and
mosaic the parts in memory. Returns (array[C,H,W], transform, crs).
NB: MemoryFile must stay open while its dataset is read, so we copy the array
out before closing (otherwise GDAL reads a freed buffer)."""
split = 1
while True:
minx, miny, maxx, maxy = bbox_utm
xs = np.linspace(minx, maxx, split + 1)
ys = np.linspace(miny, maxy, split + 1)
try:
chunks = [_download_bytes((xs[i], ys[j], xs[i+1], ys[j+1]))
for i in range(split) for j in range(split)]
except MemoryError:
if split >= max_split:
raise RuntimeError("memory limit even at max split")
split *= 2 # 1 -> 2 -> 4 (up to 16 sub-tiles)
continue
mfs, dsets = [], []
try:
for b in chunks:
mf = MemoryFile(b); mfs.append(mf); dsets.append(mf.open())
if len(dsets) == 1:
ds = dsets[0]
return ds.read().copy(), ds.transform, ds.crs
arr, transform = rio_merge(dsets)
return arr.copy(), transform, dsets[0].crs
finally:
for d in dsets: d.close()
for mf in mfs: mf.close()
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="Whole-Sikkim region grid collection.")
ap.add_argument("--shapefile", default=str(C.SHAPEFILE_PATH))
ap.add_argument("--mode", choices=["sampled", "dense"], default=C.REGION_MODE)
ap.add_argument("--limit", type=int, default=None, help="download at most N tiles")
args = ap.parse_args()
try:
ee.Initialize(project=C.GEE_PROJECT)
except Exception:
ee.Authenticate(); ee.Initialize(project=C.GEE_PROJECT)
C.RAW_REGION_FEATURES_DIR.mkdir(parents=True, exist_ok=True)
C.RAW_REGION_MASKS_DIR.mkdir(parents=True, exist_ok=True)
gdf = gpd.read_file(args.shapefile).to_crs(C.TARGET_CRS)
gdf = gdf[gdf.geometry.notna() & ~gdf.geometry.is_empty].copy()
sidx = gdf.sindex
minx, miny, maxx, maxy = gdf.total_bounds
m, ts = C.AOI_MARGIN_M, C.RAW_TILE_PX * C.SCALE_M
minx, miny, maxx, maxy = minx - m, miny - m, maxx + m, maxy + m
xs = np.arange(minx, maxx, ts)
ys = np.arange(miny, maxy, ts)
rng = np.random.RandomState(C.RANDOM_SEED)
# Plan tiles
plan = [] # (row, col, bbox, has_glacier)
for r, y in enumerate(ys):
for c, x in enumerate(xs):
bb = (x, y, x + ts, y + ts)
cand = list(sidx.intersection(bb))
has = bool(cand) and gdf.geometry.iloc[cand].intersects(box(*bb)).any()
if args.mode == "dense" or has or rng.rand() < C.REGION_BG_KEEP:
plan.append((r, c, bb, has))
if args.limit:
plan = plan[:args.limit]
n_g = sum(1 for *_, h in plan if h)
print(f"AOI {(maxx-minx)/1000:.0f}x{(maxy-miny)/1000:.0f} km | grid {len(xs)}x{len(ys)} "
f"= {len(xs)*len(ys)} cells | mode={args.mode} | downloading {len(plan)} tiles "
f"({n_g} glacier, {len(plan)-n_g} background)")
done = skip = fail = 0
for n, (r, c, bb, has) in enumerate(plan):
fpath = C.RAW_REGION_FEATURES_DIR / f"tile_{r:04d}_{c:04d}.tif"
mpath = C.RAW_REGION_MASKS_DIR / f"tile_{r:04d}_{c:04d}.tif"
if fpath.exists() and mpath.exists():
skip += 1; continue
try:
arr, transform, crs = fetch_tile(bb)
H, W = arr.shape[1], arr.shape[2]
with rasterio.open(fpath, "w", driver="GTiff", height=H, width=W,
count=arr.shape[0], dtype="float32",
crs=crs, transform=transform) as dst:
dst.write(arr.astype(np.float32))
# mask: rasterize all polygons intersecting this tile onto its grid
cand = list(sidx.intersection(bb))
geoms = [g for g in gdf.geometry.iloc[cand] if g.intersects(box(*bb))]
mask = (rasterize([(g, 1) for g in geoms], out_shape=(H, W),
transform=transform, fill=0, dtype="uint8")
if geoms else np.zeros((H, W), "uint8"))
with rasterio.open(mpath, "w", driver="GTiff", height=H, width=W, count=1,
dtype="uint8", crs=crs, transform=transform, nodata=0) as dst:
dst.write(mask, 1)
done += 1
if done % 20 == 0 or n == len(plan) - 1:
print(f" {n+1}/{len(plan)} done={done} skip={skip} fail={fail} "
f"(last tile {H}x{W}, fg={int(mask.sum())})")
except Exception as e:
fail += 1
print(f" tile_{r:04d}_{c:04d} FAILED: {e}")
print(f"\nRegion collection complete. done={done} skipped={skip} failed={fail}")
print(f"Features -> {C.RAW_REGION_FEATURES_DIR}")
print(f"Masks -> {C.RAW_REGION_MASKS_DIR}")
if __name__ == "__main__":
main()