rockglacierNET / spec.md
kabiriitb's picture
Upload folder using huggingface_hub
80e070e verified
|
Raw History Blame Contribute Delete
24.2 kB
# 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:
1. **What it does** (objective)
2. **Inputs / Outputs** (exact files and folders)
3. **How to run it** (the reference script)
4. **βœ… 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 `.shp` never travels alone β€” make sure the
sidecar files are in the same folder: `.shx`, `.dbf`, and ideally `.prj` and `.cpg`.
- Example used to build the original dataset: `Final_merged01.shp` (626 polygons,
Sikkim Himalayas, CRS `EPSG:4326`).
- **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):
```bash
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):
```python
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**).
- [ ] `CRS` is defined (original: `EPSG:4326`). If `None`, the file is unusable β€” get a
version with a `.prj`.
- [ ] Geometry type is `Polygon` / `MultiPolygon`.
- [ ] `bbox` looks 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 numeric `idx` is the row index and becomes the **ID that links masks ↔ features**
for the rest of the pipeline.
#### βœ… TEST GATE 2 β€” One GeoJSON per polygon
```python
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.0` m/pixel (matches Sentinel-2 native resolution).
- `PADDING = 200.0` m 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-band `uint8`, values {0,1}) and
`masks/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
```python
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, and
`Rasterized_Area_m2` should be in the same ballpark as `Polygon_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 GEE `project=` ID first).
- **How the features are built (must match the band table in Β§0.5):**
- **Sentinel-2:** collection `COPERNICUS/S2_SR_HARMONIZED`, date range
`2021-01-01 β†’ 2024-01-01`, pixel-level cloud mask via the `QA60` band (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, then `Elevation, Slope, Aspect` are added.
- **Export:** `scale=10`, `crs=EPSG:32645`, `file_per_band=False` β†’ one 12-band
`features/feature_{idx:04d}.tif`.
- **Behavior to expect:** the script **skips** indices whose `.tif` already 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
```python
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:
```python
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 - features` is empty (re-run the GEE script for any missing IDs).
> **Note on no-data:** GEE may write a sentinel value of `-9999` (or `NaN`) outside valid
> coverage. That is tolerated here β€” it is cleaned to `0` automatically 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 `rasterio` rasterization 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`, and `harmonization_summary.csv`
(records original shapes, harmonized shape, and CRS-match per pair).
#### βœ… TEST GATE 5 β€” Every pair is identically shaped & georeferenced
```python
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`, identical `crs`, identical `transform`.
- [ ] `0` problems 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
```python
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∩test` all empty).
Leakage here inflates validation/test scores and is the most dangerous silent bug.
- [ ] `train + val + test` counts add up to the harmonized pair total.
---
### Step 7. Extract fixed-size patches
- **Objective:** Convert variable-sized `.tif` images into fixed **64Γ—64** `.npy` tensors 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** (`-9999` and 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 : OK` and `Name match : OK` for 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/max` finite, `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)
```bash
# 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 cloud `project=`).
- **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 `idx` assigned in Step 2 links `polygon_{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.py` prints `ALL CHECKS PASSED` and
`is_trainable.md` reports a manageable class balance with non-trivial feature separability.
```