Multi-spectral trainer: N-channel loader, per-band stats, first-conv surgery (+ nidus-sync API design) #14

Merged
eliribble merged 2 commits from issue-13-multispectral-trainer into main 2026-08-02 04:27:06 +00:00
Member

Trainer work for issue #13, per Eli's go-ahead in comment #1693. Two parts:

1. Multi-spectral training support (real code, unblocks the 3-way ablation):

  • src/data/normalization.pyBandNormalizer: ImageNet (default, unchanged behavior) or per-band corpus stats.
  • src/data/dataset.py — N-channel loader: 4-band (B,G,R,NIR) TIFFs via rasterio (raw reflectance preserved), 8-bit path unchanged; band order defaults to R,G,B,NIR [2,1,0,3].
  • src/models/segmentation.pywiden_first_conv(): first-conv surgery so pretrained ImageNet encoders keep RGB weights when widened to 4ch (SMP reinitializes conv1 randomly otherwise, throwing away ImageNet features).
  • src/inference/predict.py — reconstructs training normalization from checkpoint metadata.
  • scripts/compute_band_stats.py — corpus-wide per-band mean/std → band_stats_file JSON.
  • tests/test_multispectral.py — 9 tests incl. full 4-band dataset + surgery weight preservation.

Verified: 13 tests pass; 4-band end-to-end train smoke test passes (1 epoch, ResNet-34 UNet, per-band norm); ruff clean; mypy 18→8 errors (no new ones; 10 pre-existing in label_studio.py/losses.py remain).

2. nidus-sync training-data API design (doc/nidus-sync-training-data-api.md) — how planet tiles + pool geometries get from nidus-sync onto GPU hardware: /train/corpus, /train/stats, /train/tile (multipart 16-bit per-band PNG or GeoTIFF), /train/mask (from feature_pool geometry, stadia vs confirmed source, dilation), /train/udm, and POST /train/manifest (reproducible, split-aware corpus recipe). Reuses the vision-worker Bearer auth. Rollout order: tile+mask first (unblocks Stadia-on-SkySat geometry check), then manifest+stats (unblocks ablation), then corpus+udm.

Open questions in the doc: auth scoping for a trainer role, manifest storage, frozen eval manifests for the ~1k confirmed set.

Trainer work for issue #13, per Eli's go-ahead in comment #1693. Two parts: **1. Multi-spectral training support (real code, unblocks the 3-way ablation):** - `src/data/normalization.py` — `BandNormalizer`: ImageNet (default, unchanged behavior) or per-band corpus stats. - `src/data/dataset.py` — N-channel loader: 4-band (B,G,R,NIR) TIFFs via rasterio (raw reflectance preserved), 8-bit path unchanged; band order defaults to R,G,B,NIR `[2,1,0,3]`. - `src/models/segmentation.py` — `widen_first_conv()`: first-conv surgery so pretrained ImageNet encoders keep RGB weights when widened to 4ch (SMP reinitializes conv1 randomly otherwise, throwing away ImageNet features). - `src/inference/predict.py` — reconstructs training normalization from checkpoint metadata. - `scripts/compute_band_stats.py` — corpus-wide per-band mean/std → `band_stats_file` JSON. - `tests/test_multispectral.py` — 9 tests incl. full 4-band dataset + surgery weight preservation. Verified: 13 tests pass; 4-band end-to-end train smoke test passes (1 epoch, ResNet-34 UNet, per-band norm); ruff clean; mypy 18→8 errors (no new ones; 10 pre-existing in label_studio.py/losses.py remain). **2. nidus-sync training-data API design** (`doc/nidus-sync-training-data-api.md`) — how planet tiles + pool geometries get from nidus-sync onto GPU hardware: `/train/corpus`, `/train/stats`, `/train/tile` (multipart 16-bit per-band PNG or GeoTIFF), `/train/mask` (from feature_pool geometry, stadia vs confirmed source, dilation), `/train/udm`, and `POST /train/manifest` (reproducible, split-aware corpus recipe). Reuses the vision-worker Bearer auth. Rollout order: tile+mask first (unblocks Stadia-on-SkySat geometry check), then manifest+stats (unblocks ablation), then corpus+udm. Open questions in the doc: auth scoping for a trainer role, manifest storage, frozen eval manifests for the ~1k confirmed set.
Add multi-spectral training support: N-channel loader, per-band stats, first-conv surgery
Some checks failed
CI / container-image (push) Successful in 7m52s
CI / lint-and-test (pull_request) Successful in 35s
CI / type-check (push) Failing after 1m11s
CI / type-check (pull_request) Failing after 1m9s
CI / lint-and-test (push) Successful in 10s
CI / container-image (pull_request) Successful in 17s
a47ccdf13d
Pool detection is pivoting to Planet/SkySat imagery (issue #13). The old
pipeline hard-coded RGB loading and ImageNet normalization, which cannot
represent NIR or surface-reflectance products.

Why this design:
- 4-band TIFFs (B,G,R,NIR) are loaded raw via rasterio and normalized
  with corpus-wide per-band stats computed by compute_band_stats.py, so
  cross-band ratios (NDWI, blue excess) survive preprocessing. ImageNet
  stats remain the default so existing RGB configs/checkpoints are
  untouched.
- Band order defaults to R,G,B,NIR ([2,1,0,3]) so the first three
  channels keep ImageNet semantics for the weight-copy below.
- SMP reinitializes conv1 randomly when in_channels != 3 with pretrained
  weights, discarding ImageNet features. widen_first_conv() instead keeps
  the RGB kernels and seeds the new NIR channel with their mean — the
  standard first-conv surgery for multispectral fine-tuning.
- Checkpoint metadata now records normalization scheme, stats file, and
  band order so inference reproduces training preprocessing.

Also includes the nidus-sync training-data API design (manifest-based
tile+mask export) that will feed this loader on GPU hardware.
Author
Member

CI run 220 status: container-image , lint-and-test (13/13 tests), type-check — same pre-existing mypy errors in src/label_studio.py and src/geojson.py that also fail on main (runs 1693/1690/1673/1670). Verified locally against commit a47ccdf: 8 errors, all in files untouched by this PR; no new errors introduced. Ready for review.

CI run 220 status: container-image ✅, lint-and-test ✅ (13/13 tests), type-check ❌ — same pre-existing mypy errors in `src/label_studio.py` and `src/geojson.py` that also fail on main (runs 1693/1690/1673/1670). Verified locally against commit `a47ccdf`: 8 errors, all in files untouched by this PR; no new errors introduced. Ready for review.
Fix type-check CI: allow float tile coords in geometry functions
All checks were successful
CI / lint-and-test (push) Successful in 10s
CI / container-image (pull_request) Successful in 18s
CI / container-image (push) Successful in 8m27s
CI / lint-and-test (pull_request) Successful in 5s
CI / type-check (push) Successful in 1m41s
CI / type-check (pull_request) Successful in 1m9s
2e94d50724
The offset-cache change (7d01fe9) passes half-tile coordinates
(tile_x + 0.5) to geometry conversion when a task requests an offset
tile, but _submit_geometry/geojson_feature_collection and the
mercator helpers still declared int parameters. mypy rejects the
float|int union, so every push since has failed type-check CI
(runs 218+ on main included).

The conversion math already handles fractional tile coordinates
pixel_to_mercator only adds px/tile_size to tx — so widen the
annotations to float|int rather than truncating, which would shift
offset-tile geometry by half a tile.

This unblocks the multispectral trainer PR (#14) whose CI inherits
the main-branch failure.
eliribble approved these changes 2026-08-02 04:26:52 +00:00
eliribble deleted branch issue-13-multispectral-trainer 2026-08-02 04:27:06 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Gleipnir/nemish!14
No description provided.