File size: 24,163 Bytes
80e070e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | # 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.
```
|