Download spec.md from kabiriitb/rockglacierNET: direct link, hf CLI and curl.
- Browser
- Download file 24.2 kB
-
https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/spec.md
- Command line
-
hf download hf://kabiriitb/rockglacierNET/spec.md
-
curl -L -o spec.md https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/spec.md
RockGlacierNet β Data Preprocessing Specification (Shapefile β Training-Ready Dataset)
This is a complete, step-by-step recipe for turning one rock-glacier Shapefile (.shp)
into a machine-learning-ready dataset of aligned 12-band feature tensors and binary
segmentation masks.
It is written so that someone with no prior knowledge of the project can follow it end to end. Every stage has:
- What it does (objective)
- Inputs / Outputs (exact files and folders)
- How to run it (the reference script)
- β TEST GATE β a mandatory check that must pass before moving on.
Golden rule: Never skip a TEST GATE. Each gate exists to catch a specific failure that would otherwise silently corrupt model training. If a gate fails, stop and fix the previous stage β do not proceed.
0. Setup (do this first)
0.1 Inputs you must have
- One Shapefile of rock-glacier polygons. A
.shpnever travels alone β make sure the sidecar files are in the same folder:.shx,.dbf, and ideally.prjand.cpg.- Example used to build the original dataset:
Final_merged01.shp(626 polygons, Sikkim Himalayas, CRSEPSG:4326).
- Example used to build the original dataset:
- A Google Earth Engine (GEE) account with a cloud project ID (needed in Phase 2 to download satellite + terrain features).
0.2 Software environment
Install these Python packages (the original project used a conda env; any env works):
pip install geopandas matplotlib rasterio earthengine-api geemap shapely numpy pandas
| Package | Used for |
|---|---|
geopandas, shapely |
reading/splitting the shapefile, reprojection |
rasterio |
rasterizing polygons β masks, reading/writing GeoTIFFs |
earthengine-api, geemap |
downloading Sentinel-2 + DEM features from GEE |
numpy, pandas |
array math, patch extraction, reports |
matplotlib |
EDA plots and patch visualizations |
0.3 β οΈ Fix the hard-coded paths first (critical)
The reference scripts were written on Windows and contain hard-coded paths like
r"D:\RockGlacier_Project\...". Before running anything, open each script and change the
path constants at the top to your project root.
On this machine the project root is:
/data1/nasiruddink/rockglacier/RockGlacierNet
| Script | Constants to update |
|---|---|
EDA/explore_shapefile.py |
SHAPEFILE_PATH, OUTPUT_DIR |
EDA/separate_polygons.py |
SHAPEFILE_PATH, OUTPUT_DIR, PLOTS_DIR |
preprocess/generate_masks.py |
INPUT_DIR, OUTPUT_MASK_DIR |
preprocess/gee_feature_extraction.py |
INPUT_DIR, OUTPUT_DIR, GEE project= |
preprocess/harmonize_dataset.py |
FEATURE_DIR, MASK_DIR, H_FEATURE_DIR, H_MASK_DIR |
preprocess/split_dataset.py |
H_FEATURE_DIR, H_MASK_DIR, DATASET_DIR |
check_separability.py |
H_FEATURE_DIR, H_MASK_DIR |
Tip: pick ONE root folder and keep the sub-folder names exactly as below so the scripts chain together without surprises.
0.4 Folder layout produced by this pipeline
RockGlacierNet/
βββ EDA/
β βββ individual_polygons/ # polygon_0.geojson β¦ polygon_625.geojson
β βββ individual_plots/ # first-5 sanity plots
βββ masks/ # mask_0000.tif β¦ (rasterized polygons) + mask_report.csv
βββ features/ # feature_0000.tif β¦ (12-band GEE stacks)
βββ preprocess/
β βββ harmonized_features/ # cropped to match masks pixel-for-pixel
β βββ harmonized_masks/
βββ dataset/ # train/val/test split of harmonized .tif pairs
β βββ train/{features,masks}/
β βββ val/{features,masks}/
β βββ test/{features,masks}/
βββ patches/ # final 64Γ64 .npy tensors fed to the model
βββ train/{features,masks}/
βββ val/{features,masks}/
βββ test/{features,masks}/
βββ metadata/ # config.json, patch_metadata.csv, dataset_summary.csv, diagnostics_report.txt
0.5 The 12 features (memorize this band order β everything depends on it)
Every feature_*.tif and every feature .npy has exactly 12 channels in this order:
| Band | Name | Source | Notes |
|---|---|---|---|
| 1 | B2 Blue |
Sentinel-2 SR | reflectance 0β1 |
| 2 | B3 Green |
Sentinel-2 SR | reflectance 0β1 |
| 3 | B4 Red |
Sentinel-2 SR | reflectance 0β1 |
| 4 | B8 NIR |
Sentinel-2 SR | reflectance 0β1 |
| 5 | B11 SWIR-1 |
Sentinel-2 SR | reflectance 0β1 |
| 6 | B12 SWIR-2 |
Sentinel-2 SR | reflectance 0β1 |
| 7 | NDVI |
derived | (B8βB4)/(B8+B4), range β1β¦1 |
| 8 | NDWI |
derived | (B3βB8)/(B3+B8), range β1β¦1 |
| 9 | NDSI |
derived | (B3βB11)/(B3+B11), range β1β¦1 |
| 10 | Elevation |
Copernicus GLO-30 DEM | metres |
| 11 | Slope |
derived from DEM | degrees 0β90 |
| 12 | Aspect |
derived from DEM | degrees 0β360 |
Mask convention: 1 = rock glacier, 0 = background.
Phase 1 β EDA & Polygon Separation
Step 1. Inspect the Shapefile
- Objective: Confirm the file loads, see its CRS, feature count, and attributes before trusting it.
- Run:
EDA/explore_shapefile.py - Outputs: console summary +
rock_glaciers_plot.png(overview map).
β TEST GATE 1 β Shapefile sanity
Run this quick check (paste into a Python shell, edit the path):
import geopandas as gpd
gdf = gpd.read_file("PATH/TO/Final_merged01.shp")
print("features:", len(gdf))
print("CRS:", gdf.crs)
print("geom types:", gdf.geom_type.value_counts().to_dict())
print("empty geoms:", gdf.geometry.is_empty.sum())
print("null geoms:", gdf.geometry.isna().sum())
print("invalid geoms:", (~gdf.is_valid).sum())
print("bbox:", gdf.total_bounds)
PASS criteria:
-
features> 0 (original dataset: 626). -
CRSis defined (original:EPSG:4326). IfNone, the file is unusable β get a version with a.prj. - Geometry type is
Polygon/MultiPolygon. -
bboxlooks geographically plausible for your study area (original Sikkim bbox:[88.07, 27.32] β [88.93, 28.08]). - Note any empty/null/invalid geometries β these will be skipped later (expected and OK).
Step 2. Split into individual polygons
- Objective: Break the one big shapefile into one GeoJSON per polygon so each can be
processed independently. GeoJSON is used (not
.shp) to avoid creating thousands of sidecar files. - Run:
EDA/separate_polygons.py - Output:
EDA/individual_polygons/polygon_{idx}.geojson(e.g.polygon_0.geojson). The numericidxis the row index and becomes the ID that links masks β features for the rest of the pipeline.
β TEST GATE 2 β One GeoJSON per polygon
import glob, geopandas as gpd
files = glob.glob("EDA/individual_polygons/polygon_*.geojson")
print("geojson files:", len(files))
g = gpd.read_file(files[0])
print("rows in one file:", len(g), "| CRS:", g.crs) # expect 1 row, same CRS as source
PASS criteria:
- Number of GeoJSON files == number of features from Gate 1 (original: 626).
- Each file contains exactly 1 polygon and keeps the source CRS.
Phase 2 β Mask & Feature Generation
Step 3. Generate ground-truth masks
- Objective: Rasterize each polygon into a binary mask aligned to a fixed 10 m grid.
- Run:
preprocess/generate_masks.py - Key parameters (do not change unless you also change them in Step 4):
TARGET_CRS = EPSG:32645(UTM 45N β a metre-based CRS so padding is in real metres).RESOLUTION = 10.0m/pixel (matches Sentinel-2 native resolution).PADDING = 200.0m added on every side of the polygon's bounding box (gives the model surrounding background context, not just the glacier).
- Outputs:
masks/mask_{idx:04d}.tif(single-banduint8, values {0,1}) andmasks/mask_report.csv(per-mask width/height, foreground/background pixel counts, area). - Behavior to expect: polygons with empty/NaN geometry are skipped (logged). This is why the final pair count (β607) can be lower than the polygon count (626).
β TEST GATE 3 β Masks are valid binary rasters
import glob, rasterio, numpy as np
bad = []
for f in sorted(glob.glob("masks/mask_*.tif")):
with rasterio.open(f) as src:
m = src.read(1)
vals = set(np.unique(m).tolist())
if not vals.issubset({0, 1}): bad.append((f, "values", vals))
if str(src.crs) != "EPSG:32645": bad.append((f, "crs", str(src.crs)))
if m.sum() == 0: bad.append((f, "empty-foreground", 0))
print("masks:", len(glob.glob('masks/mask_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print(" β", b)
PASS criteria:
- Every mask contains only values
{0, 1}. - Every mask CRS is
EPSG:32645. - No mask is entirely background (
m.sum() == 0) β a fully-empty mask means the rasterization missed the polygon (CRS or geometry problem). - Open
masks/mask_report.csv:Foreground_Pixels> 0 for every row, andRasterized_Area_m2should be in the same ballpark asPolygon_Area_m2.
Step 4. Extract the 12-band feature stacks from Google Earth Engine
- Objective: For each polygon, download Sentinel-2 + terrain features over the same padded bounding box used for the mask, stacked into one 12-band GeoTIFF.
- Run:
preprocess/gee_feature_extraction.py(set your GEEproject=ID first). - How the features are built (must match the band table in Β§0.5):
- Sentinel-2: collection
COPERNICUS/S2_SR_HARMONIZED, date range2021-01-01 β 2024-01-01, pixel-level cloud mask via theQA60band (drops opaque + cirrus), reflectance scaled byΓ·10000, then a median composite (clear, summer-biased, cloud-free surface). Bands taken:B2,B3,B4,B8,B11,B12; indices computed:NDVI,NDWI,NDSI. - Terrain:
COPERNICUS/DEM/GLO30. Slope & aspect are computed before mosaicking so the native 30 m projection is preserved, thenElevation, Slope, Aspectare added. - Export:
scale=10,crs=EPSG:32645,file_per_band=Falseβ one 12-bandfeatures/feature_{idx:04d}.tif.
- Sentinel-2: collection
- Behavior to expect: the script skips indices whose
.tifalready exists (safe to re-run after interruptions) and skips empty geometries. GEE downloads can fail transiently β failures are logged; re-run to fill gaps.
β TEST GATE 4 β Feature stacks are complete & well-formed
import glob, rasterio, numpy as np
bad = []
for f in sorted(glob.glob("features/feature_*.tif")):
with rasterio.open(f) as src:
if src.count != 12: bad.append((f, "bands", src.count))
if str(src.crs) != "EPSG:32645": bad.append((f, "crs", str(src.crs)))
if round(src.res[0]) != 10: bad.append((f, "res", src.res))
arr = src.read()
# a band that is entirely no-data means GEE returned nothing here
if np.all(~np.isfinite(arr)) or np.all(arr == 0): bad.append((f, "all-nodata", None))
print("features:", len(glob.glob('features/feature_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print(" β", b)
PASS criteria:
- Each feature file has exactly 12 bands, CRS
EPSG:32645, ~10 m resolution. - No feature stack is entirely no-data/zero.
- Every mask has a matching feature (same
idx). Run the coverage check:
import glob, os
m = {os.path.basename(p).split('_')[1].split('.')[0] for p in glob.glob('masks/mask_*.tif')}
f = {os.path.basename(p).split('_')[1].split('.')[0] for p in glob.glob('features/feature_*.tif')}
print("masks without features:", sorted(m - f)[:20]) # must be empty (or re-run GEE for these)
-
masks - featuresis empty (re-run the GEE script for any missing IDs).
Note on no-data: GEE may write a sentinel value of
-9999(orNaN) outside valid coverage. That is tolerated here β it is cleaned to0automatically during patch extraction (Step 7). The gate above only fails when a whole stack is no-data.
Phase 3 β Harmonize, Split, and Patch
Step 5. Harmonize feature/mask pixel grids
- Objective: GEE export and
rasteriorasterization can differ by one pixel in height/width. This step crops each feature and its mask to their common (min H, min W) and copies the feature's geo-transform onto the mask so the pair is pixel-perfect aligned. - Run:
preprocess/harmonize_dataset.py - Outputs:
preprocess/harmonized_features/feature_{idx}.tif,preprocess/harmonized_masks/mask_{idx}.tif, andharmonization_summary.csv(records original shapes, harmonized shape, and CRS-match per pair).
β TEST GATE 5 β Every pair is identically shaped & georeferenced
import glob, os, rasterio
fdir, mdir = "preprocess/harmonized_features", "preprocess/harmonized_masks"
bad = []
for fp in sorted(glob.glob(f"{fdir}/feature_*.tif")):
idx = os.path.basename(fp).split('_')[1].split('.')[0]
mp = f"{mdir}/mask_{idx}.tif"
if not os.path.exists(mp): bad.append((idx, "no-mask")); continue
with rasterio.open(fp) as fs, rasterio.open(mp) as ms:
if fs.shape != ms.shape: bad.append((idx, "shape", fs.shape, ms.shape))
if fs.crs != ms.crs: bad.append((idx, "crs"))
if fs.transform != ms.transform: bad.append((idx, "transform"))
print("pairs:", len(glob.glob(f'{fdir}/feature_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print(" β", b)
PASS criteria:
- For every pair: identical
shape, identicalcrs, identicaltransform. -
0problems printed. (Original harmonized set: 607 valid pairs.)
Step 6. Split into train / val / test
- Objective: Randomly partition the harmonized pairs into 70 % train / 15 % val / 15 % test, copying matched feature+mask pairs into split folders.
- Run:
preprocess/split_dataset.py - Reproducibility: uses
random.seed(42)β the split is deterministic. Re-running gives the same partition. - Outputs:
dataset/{train,val,test}/{features,masks}/. (Original split: train 424, val 91, test 92 = 607.)
β TEST GATE 6 β Splits are matched and leak-free
import glob, os
def ids(split, kind):
return {os.path.basename(p).split('_')[1].split('.')[0]
for p in glob.glob(f"dataset/{split}/{kind}/*.tif")}
sets = {s: (ids(s,'features'), ids(s,'masks')) for s in ['train','val','test']}
for s,(f,m) in sets.items():
print(f"{s}: feat={len(f)} mask={len(m)} | feat-mask mismatch={f ^ m}")
tr, va, te = (sets['train'][0], sets['val'][0], sets['test'][0])
print("trainβ©val:", tr & va, "| trainβ©test:", tr & te, "| valβ©test:", va & te)
PASS criteria:
- In each split, the feature ID set equals the mask ID set (no orphans).
- No overlap between any two splits (
trainβ©val,trainβ©test,valβ©testall empty). Leakage here inflates validation/test scores and is the most dangerous silent bug. -
train + val + testcounts add up to the harmonized pair total.
Step 7. Extract fixed-size patches
- Objective: Convert variable-sized
.tifimages into fixed 64Γ64.npytensors the model can batch, while controlling class imbalance. - Config:
preprocess/patch_config.py(all knobs live here):patch_size = 64,stride = 32(50 % overlap).background_keep_ratio = 0.25β keep only ~25 % of pure-background patches (the rest are discarded to fight the ~4.5 : 1 background:glacier imbalance).minimum_glacier_percentage = 0.0β keep any patch that contains at least one glacier pixel.padding_mode = "reflect"β features are reflect-padded at the right/bottom edge so the sliding window covers the whole image; masks are always zero-padded (padding is background, never glacier).num_bands = 12,random_seed = 42.
- Run:
python preprocess/extract_patches.py - What it does internally: reads each
.tif, reorders to(H, W, 12)float32, cleans no-data (-9999and any non-finite β0.0), slides the window, filters background patches by the keep-ratio, and saves kept patches as.npy. - Outputs:
patches/{split}/features/{id}_r{row}_c{col}.npyβ shape(64, 64, 12)float32.patches/{split}/masks/{id}_r{row}_c{col}.npyβ shape(64, 64, 1)float32.patches/metadata/config.json(exact config used),patch_metadata.csv(per-patch stats + per-band mean/std),dataset_summary.csv(per-image stats),diagnostics_report.txt(human-readable summary: counts, class balance, coverage histograms, padding stats, expansion factor).- (Original result: train 2081, val 454, test 510 kept patches.)
β TEST GATE 7 β Read the diagnostics before validating
Open patches/metadata/diagnostics_report.txt and confirm:
- Image counts per split match Step 6.
- Pixel class balance is sane (original β 4.5 : 1 background:glacier). If it is wildly
imbalanced (e.g. 50 : 1), revisit
background_keep_ratio. - Padding stats are small β only tiny images should need padding. A high "patches with padding" fraction hints at many sub-64 px images (expected for small glaciers, but worth a glance).
- Kept-patch counts are non-zero for all three splits.
Phase 4 β MANDATORY Dataset Validation (the safety net before training)
This is the most important phase for "nothing bad during training." Run both scripts below and require a clean result. Each guards against a concrete failure mode.
Step 8. Patch alignment & integrity validation
- Run:
python preprocess/validate_alignment.py - What it checks (per split, every patch):
| # | Check | Why it matters |
|---|---|---|
| 1 | File-count match features vs masks | a missing mask = an unlabeled training sample |
| 2 | Name match every feature/<id>.npy has mask/<id>.npy |
mispaired tensors teach the wrong label |
| 3 | Shape features (64,64,12), masks (64,64,1), H/W equal |
shape mismatch crashes the model or mislabels pixels |
| 4 | Dtype features float32; masks float32/uint8/int32/int64 |
wrong dtype breaks loss/normalization |
| 5 | No NaN / Inf in features | a single NaN turns the whole loss into NaN |
| 6 | Mask values β {0, 1} | any other value corrupts BCE/Dice loss & metrics |
| 7 | Zero-bleed glacier pixels are not all-zero across all 12 bands | catches padding/alignment errors where labels point at no-data |
| 8 | Spot-check prints shape/glacier%/min/max for 5 random patches/split | human eyeball sanity |
β TEST GATE 8 β Alignment must be perfectly clean
PASS criteria: the script ends with
OVERALL: ALL CHECKS PASSED
-
Count match : OKandName match : OKfor every split. -
Shape errors : 0,NaN errors : 0,Inf errors : 0. -
Mask value : 0 errors,Zero-bleed : 0 patches. - Spot-check rows look reasonable: feature
min/maxfinite,glacier%between 0β100.
If any count is non-zero, the script prints the first 10 offending patch IDs. Fix the root cause (usually Step 5 harmonization or Step 7 extraction) and re-run β do not hand-delete patches to make the gate pass.
Step 9. Trainability / separability report (recommended)
- Run:
python check_separability.py(operates on the harmonized pairs). - Output:
is_trainable.mdβ global class balance + per-band mean/std for glacier vs background and Cohen's d effect size (how discriminative each feature is). - Use it to confirm the signal is real, not just that files are well-formed:
- Class imbalance is manageable (original β 4.5 : 1 β fine for BCE + Dice loss).
- At least a few bands show meaningful separation (original strongest: Slope dβ0.41, NDSI, NDVI, SWIR-1). If every effect size β 0, the features and labels may be misaligned β go back to Phase 2/3.
Step 10. Visual spot-check (recommended)
- Run:
python preprocess/visualize_patches.py - Outputs in
outputs/patch_viz/:sample_grid_{split}.png(RGB + red glacier overlay),class_distribution.png,patch_size_distribution.png,band_histograms.png. - On the sample grids, the red overlay should land on plausible glacier terrain (debris tongues), not on random background β the ultimate human confirmation that features and masks line up.
End-to-End Run Order (cheat sheet)
# Phase 1 β EDA & split
python EDA/explore_shapefile.py # β Gate 1
python EDA/separate_polygons.py # β Gate 2
# Phase 2 β masks & features
python preprocess/generate_masks.py # β Gate 3
python preprocess/gee_feature_extraction.py # GEE auth needed β Gate 4
# Phase 3 β harmonize, split, patch
python preprocess/harmonize_dataset.py # β Gate 5
python preprocess/split_dataset.py # β Gate 6
python preprocess/extract_patches.py # β Gate 7
# Phase 4 β MANDATORY validation
python preprocess/validate_alignment.py # β Gate 8 (must say ALL CHECKS PASSED)
python check_separability.py # β Gate 9 (is_trainable.md)
python preprocess/visualize_patches.py # β Gate 10 (eyeball outputs/patch_viz/)
Only start model training after Gate 8 prints ALL CHECKS PASSED.
Troubleshooting β failure β likely cause β fix
| Symptom at gate | Likely cause | Fix |
|---|---|---|
Gate 1: CRS: None |
shapefile missing .prj |
obtain the .prj sidecar or set CRS explicitly |
| Gate 3: mask all-background | polygon didn't rasterize (CRS/geometry) | check the polygon's GeoJSON validity; confirm reprojection to EPSG:32645 |
| Gate 4: missing features for some masks | GEE download failed/skipped | re-run gee_feature_extraction.py (it skips existing, retries gaps) |
| Gate 4: band count β 12 | edited band list or export error | restore the band/index list in get_s2_image / get_terrain_image |
| Gate 5: shape/transform mismatch | ran split before harmonize | (re)run harmonize_dataset.py, then split |
| Gate 6: split overlap | re-split with different seed / mixed old files | clear dataset/ and re-run split_dataset.py |
| Gate 8: NaN/Inf errors | no-data not cleaned | confirm Step 7 ran (it converts -9999/non-finite β 0); re-extract |
| Gate 8: mask value errors | mask resampled with interpolation | masks must be nearest-neighbor / untouched uint8 {0,1} |
| Gate 8: zero-bleed > 0 | feature/mask misaligned or padding over real labels | re-check Gate 5; ensure masks are zero-padded, never reflect-padded |
| Gate 9: all effect sizes β 0 | features β masks mismatched | verify idx linkage from Step 2 onward |
Notes for Automated Agents
- Follow the phases linearly. Each
python β¦command corresponds to exactly one reference script; do not reorder. - Authenticate GEE before Phase 2 (
ee.Authenticate()/ set the cloudproject=). - Treat every TEST GATE as a hard precondition for the next stage. On failure, fix the upstream stage; never silently delete data to pass a gate.
- The ID is sacred. The integer
idxassigned in Step 2 linkspolygon_{idx}βmask_{idx}βfeature_{idx}βdatasetβpatches. Never renumber or sort-rename files in a way that breaks this correspondence. - All geometry/config constants (CRS
EPSG:32645, 10 m, 200 m pad, 64-px patch, stride 32, seed 42, 12 bands) must stay identical between mask generation, feature extraction, and patching, or alignment breaks. - Definition of done:
validate_alignment.pyprintsALL CHECKS PASSEDandis_trainable.mdreports a manageable class balance with non-trivial feature separability.