Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
release: add docs/DATASETS.md
Browse files- docs/DATASETS.md +1159 -101
docs/DATASETS.md
CHANGED
|
@@ -1,162 +1,1220 @@
|
|
| 1 |
-
# Datasets
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
official release, that is stated β not smoothed over.
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
## 1. Summary
|
| 12 |
|
| 13 |
| Task | Dataset | Role | Local state |
|
| 14 |
|---|---|---|---|
|
| 15 |
| `change` | **LEVIR-CD-256** | train / val / test | ACQUIRED β split 7120 / 1024 / 2048 |
|
| 16 |
-
| `change_vqa` | **CDVQA** (+ **SECOND** imagery) | train / val / test | annotations + imagery ACQUIRED; name-verified 2,968/2,968 |
|
| 17 |
-
| `grounding` | **VRSBench** | eval (16,159 records) | ACQUIRED β boxes normalised 0β100 |
|
| 18 |
-
| `optical_sar` | **BigEarthNet** (CLC-19) | train / held-out test | PARTIAL β 28,000-patch local
|
| 19 |
-
| `vlm` | **BigEarthNet**
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
The
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|---|---|
|
| 28 |
-
|
|
| 29 |
-
|
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
| Property | Value |
|
| 35 |
|---|---|
|
| 36 |
-
| n | 2,048 |
|
| 37 |
| images containing change | **935** |
|
| 38 |
-
| mean change fraction | **0.0509** (β 5 % of pixels) |
|
|
|
|
| 39 |
| threshold | 0.50 |
|
| 40 |
| tile size | 256 |
|
| 41 |
| device (eval) | cuda |
|
| 42 |
| total pixels scored | 134,217,728 |
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
per-pixel confusion counts (tp 5,978,997 / fp 523,658 / fn 858,407 / tn 126,856,666) are stored so
|
| 47 |
-
any metric can be recomputed rather than trusted.
|
| 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 |
-
`load_cdvqa('data/cdvqa')`.
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
> `change_vqa` head trains on **cached features**, not on the raw loader. See
|
| 80 |
-
> [`TRAINING.md`](TRAINING.md).
|
| 81 |
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
The
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
Because the box convention is a common source of silent error, grounding is reported under **two
|
| 91 |
-
protocols** (canonical and matched6) and **two decode variants** β see
|
| 92 |
-
[`BENCHMARKS.md`](BENCHMARKS.md) Β§
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
optical-SAR fusion head, and as the **instruction-pair source** for the SmolVLM LoRA adaptation.
|
| 98 |
|
| 99 |
-
**
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
| Property | Value |
|
| 102 |
|---|---|
|
| 103 |
-
|
|
| 104 |
-
|
|
| 105 |
-
|
|
| 106 |
-
|
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
|
| 119 |
| Property | Value |
|
| 120 |
|---|---|
|
| 121 |
| split | test |
|
| 122 |
| n scored | **4,000** |
|
| 123 |
| classes in label space | 19 |
|
| 124 |
-
| classes present in the scored split | 14 |
|
| 125 |
-
| classes absent | 5 |
|
| 126 |
-
| macro-F1 denominator | **all 19 classes (absent classes contribute 0.0)** |
|
| 127 |
-
| accuracy | 0.931 |
|
| 128 |
-
| macro F1 | 0.434161 |
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
together.
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
The
|
| 143 |
-
percentile stretch. This project nevertheless applies percentile normalisation for optical inputs
|
| 144 |
-
(2/98) to match the CROMA contract. That is a deliberate, documented choice, not an upstream fact.
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
|
| 149 |
-
kept whole, and hard-negative families are placed in the **test** split so their accuracy measures
|
| 150 |
-
generalisation rather than memorisation.
|
| 151 |
-
- **Leakage split key: `scene_id`** (declared in `configs/base.yaml`).
|
| 152 |
-
- **Immutable public test: true.** `hidden_data_access: false`. The evaluation config forbids
|
| 153 |
-
touching hidden data.
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|---|---|
|
| 159 |
-
|
|
| 160 |
-
|
|
| 161 |
-
|
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Datasets β deep reference
|
| 2 |
|
| 3 |
+
**Status tags used on every substantive claim:** `IMPLEMENTED` Β· `VERIFIED` Β· `MEASURED` Β· `ATTEMPTED`
|
| 4 |
+
Β· `NOT RUN` Β· `BLOCKED` Β· `DEFERRED` Β· `REJECTED` Β· `OPEN` Β· `RESOLVED` Β· `CLOSED`.
|
|
|
|
| 5 |
|
| 6 |
+
Every figure in this document was **measured from the data on disk**, not copied from a paper or a
|
| 7 |
+
dataset schema. Where a corpus was only partially acquired, or where the local subset differs from the
|
| 8 |
+
official release, that is stated β not smoothed over. Where a fact is not established from the
|
| 9 |
+
available evidence, this document writes
|
| 10 |
+
`UNKNOWN β not established from the available evidence`.
|
| 11 |
|
| 12 |
+
**The corpora this project trains and evaluates on:**
|
|
|
|
|
|
|
| 13 |
|
| 14 |
| Task | Dataset | Role | Local state |
|
| 15 |
|---|---|---|---|
|
| 16 |
| `change` | **LEVIR-CD-256** | train / val / test | ACQUIRED β split 7120 / 1024 / 2048 |
|
| 17 |
+
| `change_vqa` | **CDVQA** (annotations) + **SECOND** (imagery) | train / val / test / test2 | annotations + imagery ACQUIRED; name-verified 2,968 / 2,968 |
|
| 18 |
+
| `grounding` | **VRSBench** | train / eval (16,159 eval records) | ACQUIRED β boxes normalised 0β100 |
|
| 19 |
+
| `optical_sar` | **BigEarthNet** (reBEN v2, CLC-19) | train / held-out test | PARTIAL β 28,000-patch local selection |
|
| 20 |
+
| `vlm` | **BigEarthNet-S2 single-label** | LoRA adaptation | PARTIAL β same 28,000-patch request, 24,732 matched |
|
| 21 |
+
|
| 22 |
+
> **Two different BigEarthNet corpora are in play and must not be conflated.** The optical-SAR fusion
|
| 23 |
+
> head consumes the **reBEN v2** corpus (`data/bigearthnet_v2/reben/`); the VLM LoRA adapter consumes a
|
| 24 |
+
> **BigEarthNet-S2 single-label** subset (`run_manifest.json` β `config.corpus_root`). They share the
|
| 25 |
+
> 28,000-patch request but are different trees with different label sources. Β§6 covers both.
|
| 26 |
+
|
| 27 |
+
**Sibling documents:** [`TRAINING.md`](TRAINING.md) (how each corpus is consumed),
|
| 28 |
+
[`EVALUATION.md`](EVALUATION.md) (protocols and honesty rules),
|
| 29 |
+
[`BENCHMARKS.md`](BENCHMARKS.md) (the numbers), [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md)
|
| 30 |
+
(reproduction).
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Table of contents
|
| 35 |
+
|
| 36 |
+
1. [How these figures were obtained](#1-how-these-figures-were-obtained)
|
| 37 |
+
2. [Summary](#2-summary)
|
| 38 |
+
3. [LEVIR-CD-256 β change detection](#3-levir-cd-256--change-detection)
|
| 39 |
+
4. [CDVQA + SECOND β change-VQA](#4-cdvqa--second--change-vqa)
|
| 40 |
+
5. [VRSBench β grounding](#5-vrsbench--grounding)
|
| 41 |
+
6. [BigEarthNet β optical-SAR fusion and VLM adaptation](#6-bigearthnet--optical-sar-fusion-and-vlm-adaptation)
|
| 42 |
+
7. [Data hygiene and leakage controls](#7-data-hygiene-and-leakage-controls)
|
| 43 |
+
8. [What is NOT available β exhaustive](#8-what-is-not-available--exhaustive)
|
| 44 |
+
9. [What is NOT RUN / OPEN / BLOCKED for this topic](#9-what-is-not-run--open--blocked-for-this-topic)
|
| 45 |
+
10. [Where the evidence lives](#10-where-the-evidence-lives)
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## 1. How these figures were obtained
|
| 50 |
+
|
| 51 |
+
Three rules govern this document.
|
| 52 |
+
|
| 53 |
+
1. **Measured, not quoted.** Every count, fraction and split size was produced by reading the files on
|
| 54 |
+
disk β annotation JSON, label PNGs, a metadata parquet, or a directory listing. Where a paper's
|
| 55 |
+
figure is mentioned, it is mentioned as *corroborated by* the measurement, never as the source.
|
| 56 |
+
2. **Discrepancies are reported, not reconciled.** Where the data contradicts the original plan, or
|
| 57 |
+
where two documents disagree, both are recorded. Silently resolving a discrepancy is how a
|
| 58 |
+
preprocessing change moves the frozen config hash
|
| 59 |
+
(`configs/base.yaml` β `78f1e3700da15aa1`) without anyone noticing.
|
| 60 |
+
3. **A shortfall is a number, not a shrinkage.** When a corpus is only partially acquired, the
|
| 61 |
+
shortfall is surfaced in a run manifest (e.g. `coverage.n_unmatched`), not absorbed by dropping
|
| 62 |
+
examples quietly.
|
| 63 |
+
|
| 64 |
+
The measurement scripts are named inline throughout. The verifier that re-checks the release's headline
|
| 65 |
+
numbers is described in [`BENCHMARKS.md`](BENCHMARKS.md) Β§8.
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
## 2. Summary
|
| 70 |
+
|
| 71 |
+
| Corpus | Task | Local size | Split | Local state |
|
| 72 |
+
|---|---|---|---|---|
|
| 73 |
+
| LEVIR-CD-256 | `change` | 10,192 tiles (A / B / label) | 7120 / 1024 / 2048 | ACQUIRED |
|
| 74 |
+
| CDVQA annotations | `change_vqa` | 12 JSON, 55.0 MB | Train/Val/Test/Test2 | ACQUIRED |
|
| 75 |
+
| SECOND imagery | `change_vqa` | 2,968 pairs β 11,872 PNGs | n/a (paired) | ACQUIRED, name-verified 2,968/2,968 |
|
| 76 |
+
| VRSBench | `grounding` | 16,159 eval records; 142,390 train conversations | eval / train | ACQUIRED |
|
| 77 |
+
| BigEarthNet reBEN v2 | `optical_sar` | 28,000-patch selection of a 549,488-row corpus | 20,000 / 4,000 / 4,000 | PARTIAL |
|
| 78 |
+
| BigEarthNet-S2 single-label | `vlm` | 24,732 matched of 28,000 requested | 17,471 / 3,375 / 3,886 patches | PARTIAL |
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## 3. LEVIR-CD-256 β change detection
|
| 83 |
+
|
| 84 |
+
The standard change-detection benchmark, 256 Γ 256 tiles. It is the corpus behind the release's only
|
| 85 |
+
`VERIFIED` headline (pooled IoU 0.8122).
|
| 86 |
+
|
| 87 |
+
### 3.1 The layout
|
| 88 |
+
|
| 89 |
+
The local corpus uses a **flat** layout (`docs/PHASE9_REAL_DATA_VERIFICATION.md`):
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
<root>/{A,B,label}/<split>_<scene>_<tile>.png
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
`A` is the pre-change image, `B` the post-change image, `label` the change mask. The loader recognises
|
| 96 |
+
both this flat layout and a nested layout (`training/change/dataset.py` β `LAYOUT_NESTED` /
|
| 97 |
+
`LAYOUT_FLAT`), and the verification recorded `layout: flat`.
|
| 98 |
+
|
| 99 |
+
| Directory | PNGs |
|
| 100 |
+
|---|---|
|
| 101 |
+
| `A` | 10,192 |
|
| 102 |
+
| `B` | 10,192 |
|
| 103 |
+
| `label` | 10,192 |
|
| 104 |
+
|
| 105 |
+
(`artifacts/change/levir_real_data_verification.json` β `dir_counts`; the check "A/B/label counts
|
| 106 |
+
agree" passed with detail `10192 / 10192 / 10192`.)
|
| 107 |
+
|
| 108 |
+
### 3.2 The split
|
| 109 |
+
|
| 110 |
+
The split is a **frozen invariant**, not a choice made per run. `docs/PHASE9_FREEZE.md` Β§3 records it:
|
| 111 |
+
|
| 112 |
+
> Scene split: **445 / 64 / 128 scenes = 7120 / 1024 / 2048 tiles**, enforced by `change.levir_split` in
|
| 113 |
+
> `configs/base.yaml`.
|
| 114 |
+
|
| 115 |
+
| Split | Scenes | Tiles |
|
| 116 |
+
|---|---|---|
|
| 117 |
+
| train | 445 | **7,120** |
|
| 118 |
+
| val | 64 | **1,024** |
|
| 119 |
+
| test | **128** | **2,048** |
|
| 120 |
|
| 121 |
+
(`artifacts/change/levir_real_data_verification.json` β `splits`.) The scene counts are the leakage
|
| 122 |
+
boundary; the tile counts are what the trainer sees. `training/change/dataset.py` defines
|
| 123 |
+
`LEVIR_SPLITS = {"train": 445, "val": 64, "test": 128}` and provides `assert_image_disjoint`.
|
| 124 |
|
| 125 |
+
**The split is scene-disjoint, and the disjointness is measured, not assumed.** The verification
|
| 126 |
+
recorded three separate checks β `train/val scene-disjoint`, `train/test scene-disjoint`,
|
| 127 |
+
`val/test scene-disjoint` β each passing with an empty offender list
|
| 128 |
+
(`artifacts/change/levir_real_data_verification.json` β `checks`, and β `scene_overlap` with all three
|
| 129 |
+
counts `0`).
|
| 130 |
|
| 131 |
+
**The declared split agrees with the derived split.** The verification compared the release's declared
|
| 132 |
+
split lists against a split derived from the file names:
|
| 133 |
+
|
| 134 |
+
| Split | declared | derived | agreement | declared-only | derived-only |
|
| 135 |
+
|---|---|---|---|---|---|
|
| 136 |
+
| train | 7,120 | 7,120 | 7,120 | 0 | 0 |
|
| 137 |
+
| val | 1,024 | 1,024 | 1,024 | 0 | 0 |
|
| 138 |
+
| test | 2,048 | 2,048 | 2,048 | 0 | 0 |
|
| 139 |
+
|
| 140 |
+
(`artifacts/change/levir_real_data_verification.json` β `split_list_agreement`.) A disagreement here
|
| 141 |
+
would mean the split lists and the file names told different stories; they do not.
|
| 142 |
+
|
| 143 |
+
### 3.3 The label contract
|
| 144 |
+
|
| 145 |
+
| Property | Value | Source |
|
| 146 |
+
|---|---|---|
|
| 147 |
+
| label values | `0`, `156`, `255` | `artifacts/change/levir_real_data_verification.json` β `sample.label_values` |
|
| 148 |
+
| `LABEL_CHANGE_LEVEL` | **128** | `training/change/train.py` |
|
| 149 |
+
| binary decision | `>= 128` β change | same |
|
| 150 |
+
| label is binary `0/255` | intermediate `156` is negligible | verification check |
|
| 151 |
+
|
| 152 |
+
Measured label-value counts over a 600-tile sample
|
| 153 |
+
(`artifacts/change/levir_real_data_verification.json` β `sample.label_values`):
|
| 154 |
+
|
| 155 |
+
| Value | Pixels |
|
| 156 |
|---|---|
|
| 157 |
+
| 0 | 37,590,438 |
|
| 158 |
+
| 156 | **17** |
|
| 159 |
+
| 255 | 1,731,145 |
|
| 160 |
+
|
| 161 |
+
The `156` pixels are **4.32e-07 of 39,321,600 px** β a negligible anti-aliasing residue. The check
|
| 162 |
+
"labels binary 0/255 (intermediate negligible)" passed, and the check "intermediate labels >=
|
| 163 |
+
LABEL_CHANGE_LEVEL (128)" passed with an empty `below-threshold` set. The threshold matters: it is what
|
| 164 |
+
makes the `156` residue fall on the change side rather than being silently rounded away.
|
| 165 |
+
|
| 166 |
+
Two further checks passed: "T1 and T2 are not identical" (0 identical pairs in the sample β a pair that
|
| 167 |
+
was byte-identical would be a null change example masquerading as a real one) and "label matches image
|
| 168 |
+
size" (0 mismatches).
|
| 169 |
+
|
| 170 |
+
### 3.4 The test split, measured in detail
|
| 171 |
|
| 172 |
+
`artifacts/change/eval_test/eval_result.json`, `split: test`:
|
| 173 |
|
| 174 |
| Property | Value |
|
| 175 |
|---|---|
|
| 176 |
+
| n | **2,048** |
|
| 177 |
| images containing change | **935** |
|
| 178 |
+
| **mean change fraction** | **0.0509** (β 5 % of pixels) |
|
| 179 |
+
| change-fraction quantiles | min 0.0, p50 0.0, p90 0.197205, max 0.684937 |
|
| 180 |
| threshold | 0.50 |
|
| 181 |
| tile size | 256 |
|
| 182 |
| device (eval) | cuda |
|
| 183 |
| total pixels scored | 134,217,728 |
|
| 184 |
+
| eval seconds | 55.359 |
|
| 185 |
+
| `config_drift` | false |
|
| 186 |
|
| 187 |
+
**Per-pixel confusion counts** (pooled; identical for the macro block, which aggregates the same pixels
|
| 188 |
+
per image):
|
|
|
|
|
|
|
| 189 |
|
| 190 |
+
| Count | Value |
|
| 191 |
+
|---|---|
|
| 192 |
+
| tp | 5,978,997 |
|
| 193 |
+
| fp | 523,658 |
|
| 194 |
+
| fn | 858,407 |
|
| 195 |
+
| tn | 126,856,666 |
|
| 196 |
|
| 197 |
+
The confusion counts are stored **so any metric can be recomputed rather than trusted**. The
|
| 198 |
+
per-image change fraction is stored for all 2,048 tiles
|
| 199 |
+
(`per_image_change_fraction`, a 2,048-element list), which is what makes the quantiles above
|
| 200 |
+
recomputable.
|
| 201 |
|
| 202 |
+
### 3.5 Why both pooled and macro are reported
|
| 203 |
|
| 204 |
+
With a **~5 % mean change fraction** the class imbalance is severe, so the pooled and macro metrics
|
| 205 |
+
answer different questions and both are reported
|
| 206 |
+
([`BENCHMARKS.md`](BENCHMARKS.md) Β§10.1):
|
| 207 |
+
|
| 208 |
+
| Metric | pooled | macro |
|
| 209 |
+
|---|---|---|
|
| 210 |
+
| f1 | **0.8964** | 0.7962 |
|
| 211 |
+
| iou | **0.8122** | 0.7180 |
|
| 212 |
+
| miou | 0.9007 | **0.8457** |
|
| 213 |
+
| precision | 0.9195 | 0.8506 |
|
| 214 |
+
| recall | 0.8745 | 0.7757 |
|
| 215 |
+
|
| 216 |
+
**Naming caveat.** The `macro` block carries both `iou` (0.7180, the macro-average of the per-image IoU)
|
| 217 |
+
and `miou` (0.8457, the mean IoU). The headline "macro IoU 0.8457" used across the release docs is the
|
| 218 |
+
**`miou`** field. Both key paths are named here so the two cannot be conflated.
|
| 219 |
+
|
| 220 |
+
**A threshold sweep was performed and the lever was CLOSED.** It was **val-only by construction**:
|
| 221 |
+
`scripts/sweep_change_threshold.py` refuses any `--split` other than `val` and exits `4` before loading
|
| 222 |
+
config, checkpoint, or data; its artifact records `test_split_touched: false` and its sha256 is
|
| 223 |
+
`34e20f62bc1dd7810f5ef5f26838213d59368cf1352a804d38cb0e140eaf97f2`. The best available gain was
|
| 224 |
+
**0.0007** pooled IoU against a **0.0019** epoch-to-epoch spread β 0.37Γ the noise. **0.50 is retained;
|
| 225 |
+
this hypothesis is eliminated, not deferred** (`docs/PHASE9_FREEZE.md` Β§5).
|
| 226 |
+
|
| 227 |
+
### 3.6 The three real-data defects found and fixed
|
| 228 |
+
|
| 229 |
+
`docs/PHASE9_REAL_DATA_VERIFICATION.md` records three defects that the verification caught before any
|
| 230 |
+
number was trusted. They are included here because each is a *silent* failure mode a reader could
|
| 231 |
+
re-introduce:
|
| 232 |
+
|
| 233 |
+
| # | Defect | Consequence if unfixed |
|
| 234 |
+
|---|---|---|
|
| 235 |
+
| 1 | **loader returned 0 items** | a training run on an empty dataset |
|
| 236 |
+
| 2 | **vacuous leakage guard** | a scene-disjointness check that could not fail, because it compared nothing |
|
| 237 |
+
| 3 | **the test split was folded into training** | a test number that was partly a training number |
|
| 238 |
+
|
| 239 |
+
Defect 3 is the dangerous one: it is exactly the failure the scene-disjointness rule exists to prevent,
|
| 240 |
+
and it would have *raised* the reported test IoU rather than lowering it. The verification's three
|
| 241 |
+
`scene-disjoint` checks (Β§3.2) are the pin that now prevents it.
|
| 242 |
+
|
| 243 |
+
### 3.7 The registration gate β an `OPEN` serving-path defect
|
| 244 |
+
|
| 245 |
+
The change specialist's registration gate produces false positives: **1,202 / 2,048 test tiles
|
| 246 |
+
(58.7 %)** flagged as mis-registered, with the flag rate climbing 42 % β 75 % β 98 % β 100 % as
|
| 247 |
+
ground-truth change fraction rises; **5,679 region claims suppressed** on 638 flagged tiles that do
|
| 248 |
+
contain change. An independent NCC check found **0 / 30** credible large offsets on the tiles the gate
|
| 249 |
+
blames. **`OPEN`** (`docs/PHASE9_FREEZE.md` Β§7). This affects the *serving path*, not the 0.8122
|
| 250 |
+
benchmark, which is computed offline from the mask directly.
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## 4. CDVQA + SECOND β change-VQA
|
| 255 |
+
|
| 256 |
+
### 4.1 The two-corpus arrangement
|
| 257 |
+
|
| 258 |
+
**The CDVQA repository publishes annotations only β no imagery.** The imagery is publicly available as
|
| 259 |
+
**SECOND** (semantic change detection). The local corpus is therefore assembled from two sources:
|
| 260 |
+
|
| 261 |
+
```
|
| 262 |
+
data/cdvqa/
|
| 263 |
+
annotations/ 12 JSON files (55.0 MB) <- CDVQA repo (annotations only)
|
| 264 |
+
im1/ im2/ pre / post imagery 2,968 PNGs each <- SECOND (archive order)
|
| 265 |
+
label1/ label2/ pre / post label maps 2,968 PNGs each <- SECOND
|
| 266 |
+
second.zip 2,406,111,691 B (kept, not deleted) <- the raw SECOND archive
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
Source of the annotations: `https://github.com/YZHJessica/CDVQA`.
|
| 270 |
+
Source of the imagery: the **SECOND** semantic-change-detection dataset, downloaded and extracted
|
| 271 |
+
locally. The paper states the arrangement verbatim (`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§6):
|
| 272 |
+
|
| 273 |
+
> *"we choose the existing semantic change detection dataset SECOND as the basic data to automatically
|
| 274 |
+
> generate a CDVQA dataset."* β¦ *"Thus, we use the 2,968 openly available pairs as our basic data."*
|
| 275 |
+
|
| 276 |
+
### 4.2 SECOND β acquisition and verification
|
| 277 |
+
|
| 278 |
+
The imagery acquisition is a story with three findings, all recorded rather than hidden
|
| 279 |
+
(`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§6).
|
| 280 |
+
|
| 281 |
+
**a. The archive is RAR, not zip.** It is *named* `second.zip` but the magic bytes are
|
| 282 |
+
`Rar!\x1a\x07\x01\x00` β a **RAR5** archive. The overlap checker originally assumed zip and reported
|
| 283 |
+
*"not a readable zip"* on a perfectly good download. It now detects the format by magic bytes and reads
|
| 284 |
+
RAR via libarchive. **Do not trust a `.zip` extension.**
|
| 285 |
+
|
| 286 |
+
**b. The names match β 2,968 / 2,968.** CDVQA's `file_name` values are a **sparse global identifier**,
|
| 287 |
+
not a compact index: min `00003`, max `24649`, **not contiguous** (21,679 gaps in a 24,647-wide span).
|
| 288 |
+
Whether SECOND used the same filenames was not established and had to be settled by download, not
|
| 289 |
+
assumption. It was:
|
| 290 |
+
|
| 291 |
+
```
|
| 292 |
+
SECOND PNG files : 2,968
|
| 293 |
+
CDVQA unique names : 2,968 (expected 2,968)
|
| 294 |
+
intersection : 2,968
|
| 295 |
+
CDVQA not in SECOND: 0
|
| 296 |
+
SECOND not in CDVQA: 0
|
| 297 |
+
VERDICT: MATCH
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
(`artifacts/cdvqa/second_overlap.json`: `verdict: "MATCH"`, `intersection: 2968`,
|
| 301 |
+
`cdvqa_not_in_second: 0`, `second_not_in_cdvqa: 0`, `cdvqa_count_matches_expectation: true`.) The checker
|
| 302 |
+
`scripts/check_cdvqa_second_overlap.py` exited **0**.
|
| 303 |
+
|
| 304 |
+
| SECOND archive property | Value |
|
| 305 |
+
|---|---|
|
| 306 |
+
| bytes | **2,406,111,691** |
|
| 307 |
+
| sha256 | `5ee2a82b5824b3f5e3c5bfaf018835862e7623993ff9ac5ce57fd11fcfad2b4e` |
|
| 308 |
+
| container | **RAR5** (magic `Rar!\x1a\x07\x01\x00`) |
|
| 309 |
+
| image format | 512 Γ 512, 8-bit, **PNG colour type 2** (truecolour) |
|
| 310 |
+
| pairs | 2,968 β 5,936 imagery PNGs + 5,936 label PNGs |
|
| 311 |
+
|
| 312 |
+
**c. The archive is a temporal PAIR.** SECOND's layout is `im1/ im2/ label1/ label2/`, and `im1` and
|
| 313 |
+
`im2` **share basenames** (`im1/00003.png` and `im2/00003.png` both exist). So one CDVQA `file_name`
|
| 314 |
+
denotes a **pair**, not an image. This is why the adapter is pair-aware and why
|
| 315 |
+
`imagery_verification.json` includes the check *"flat image_path() does NOT resolve (pair layout is what
|
| 316 |
+
is on disk)"* β the flat layout is a real, loadable-looking layout that is wrong for this corpus.
|
| 317 |
+
|
| 318 |
+
The extraction is verified end to end (`artifacts/cdvqa/imagery_verification.json`,
|
| 319 |
+
`all_checks_passed: true`): each of the four archive dirs holds 2,968 PNGs; `load_cdvqa(
|
| 320 |
+
require_images=True)` succeeds; every declared name exists in `im1`, `im2` and both label dirs; all
|
| 321 |
+
**153,130** examples' `image_path` **and** `image_paths` entries resolve on disk (153,130 / 153,130);
|
| 322 |
+
the layout is `second_pairs`; and `summarize` reports 2,968 declared / 2,968 present.
|
| 323 |
+
|
| 324 |
+
### 4.3 The annotations, measured
|
| 325 |
+
|
| 326 |
+
| File | Bytes |
|
| 327 |
+
|---|---|
|
| 328 |
+
| `Train_images.json` | 3,358,035 |
|
| 329 |
+
| `Train_questions.json` | 13,201,947 |
|
| 330 |
+
| `Train_answers.json` | 6,931,041 |
|
| 331 |
+
| `Val_images.json` | 826,107 |
|
| 332 |
+
| `Val_questions.json` | 3,259,814 |
|
| 333 |
+
| `Val_answers.json` | 1,710,345 |
|
| 334 |
+
| `Test_images.json` | 2,021,288 |
|
| 335 |
+
| `Test_questions.json` | 7,921,042 |
|
| 336 |
+
| `Test_answers.json` | 4,160,913 |
|
| 337 |
+
| `Test2_images.json` | 1,961,840 |
|
| 338 |
+
| `Test2_questions.json` | 6,353,771 |
|
| 339 |
+
| `Test2_answers.json` | 3,278,618 |
|
| 340 |
+
|
| 341 |
+
**Total 55.0 MB** (`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§1).
|
| 342 |
+
|
| 343 |
+
**Schema.** Each JSON has exactly one top-level key β `images`, `questions`, `answers`:
|
| 344 |
+
|
| 345 |
+
```
|
| 346 |
+
images {id, res_x, res_y, questions_ids, file_name, active}
|
| 347 |
+
questions {id, date_added, img_id, type, question, answers_ids, active}
|
| 348 |
+
answers {id, date_added, question_id, answer, active}
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
Two type traps a reader gets wrong by guessing: `date_added` is a **float unix timestamp** (e.g.
|
| 352 |
+
`1631214265.2322025`), and `res_x` / `res_y` are the **strings** `".1524m"` on every row of every split β
|
| 353 |
+
metadata, not a usable resolution figure.
|
| 354 |
+
|
| 355 |
+
**`images` has one row per question, not per image.** Train carries **25,600 image rows** but only
|
| 356 |
+
**1,600 distinct `file_name` values**. Deduplicating on `file_name` and unioning `questions_ids` is
|
| 357 |
+
mandatory; a reader that counts image rows reports a corpus **16Γ too large**.
|
| 358 |
+
|
| 359 |
+
### 4.4 The measured split sizes
|
| 360 |
+
|
| 361 |
+
| Split | image rows | **unique `file_name`** | questions | answers |
|
| 362 |
+
|---|---|---|---|---|
|
| 363 |
+
| Train | 25,600 | **1,600** | **65,967** | 65,967 |
|
| 364 |
+
| Val | 6,400 | **400** | **16,441** | 16,441 |
|
| 365 |
+
| Test | 15,488 | **968** | **39,686** | 39,686 |
|
| 366 |
+
| Test2 | 15,488 | **968** | **31,036** | 31,036 |
|
| 367 |
+
| **union** | | **2,968** | **153,130** | 153,130 |
|
| 368 |
+
|
| 369 |
+
This independently confirms the paper's 1,600 / 400 / 968 image pairs and 65,967 / 16,441 / 39,686 /
|
| 370 |
+
31,036 question-answer pairs (`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§3). The same figures are asserted as
|
| 371 |
+
constants in `training/change_vqa/dataset.py` (`EXPECTED_SPLIT_SIZES`, `EXPECTED_TOTAL_SCENES: 2968`,
|
| 372 |
+
`EXPECTED_MANIFEST_RECORDS: 3936`) and re-measured by the notebook's integrity gate.
|
| 373 |
+
|
| 374 |
+
### 4.5 Finding 1 β Test and Test2 are the same 968 images
|
| 375 |
+
|
| 376 |
+
Measured cross-split `file_name` overlap, all 16 cells
|
| 377 |
+
(`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§4):
|
| 378 |
+
|
| 379 |
+
| | Train | Val | Test | Test2 |
|
| 380 |
+
|---|---|---|---|---|
|
| 381 |
+
| **Train** | 1600 | 0 | 0 | 0 |
|
| 382 |
+
| **Val** | 0 | 400 | 0 | 0 |
|
| 383 |
+
| **Test** | 0 | 0 | 968 | **968** |
|
| 384 |
+
| **Test2** | 0 | 0 | **968** | 968 |
|
| 385 |
+
|
| 386 |
+
Train / Val / Test are **cleanly disjoint** β no leakage. But **Test and Test2 share 100 % of their
|
| 387 |
+
images.** They are two different question sets asked over the **same 968 scenes**, not two independent
|
| 388 |
+
held-out samples. The paper says this outright: *"Note that there is an overlap between the two test
|
| 389 |
+
sets."* The measurement confirms it exactly.
|
| 390 |
+
|
| 391 |
+
> **Consequence.** Pooling Test and Test2, or reporting them as two independent evaluations, is
|
| 392 |
+
> statistically wrong and inflates apparent confidence. Any metric computed over `Test βͺ Test2` is a
|
| 393 |
+
> single evaluation of 968 images with ~70k questions, not two evaluations of 968 each.
|
| 394 |
+
|
| 395 |
+
This is why the release quotes change-VQA under **two test sets** β Test 0.697626 / 0.378373 and Test2
|
| 396 |
+
0.651469 / 0.372309 β and never pools them ([`BENCHMARKS.md`](BENCHMARKS.md) Β§4.4,
|
| 397 |
+
[`EVALUATION.md`](EVALUATION.md) rule 3). The allowed shared pair is recorded in the run record:
|
| 398 |
+
`dataset.integrity.allowed_shared_pairs: [["Test", "Test2"]]`
|
| 399 |
+
(`artifacts/change_vqa/run/run_record.json`).
|
| 400 |
+
|
| 401 |
+
### 4.6 Finding 2 β `change_ratio_types` has a different vocabulary in every split
|
| 402 |
+
|
| 403 |
+
The eight question types, with total counts across all four splits and their answer vocabularies
|
| 404 |
+
(`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§5):
|
| 405 |
+
|
| 406 |
+
| Question type | Questions | Answer vocabulary |
|
| 407 |
+
|---|---|---|
|
| 408 |
+
| `change_or_not` | 47,807 | 2 β `yes`, `no` |
|
| 409 |
+
| `change_ratio_types` | 29,576 | **7β9, split-dependent** |
|
| 410 |
+
| `increase_or_not` | 16,040 | 2 β `yes`, `no` |
|
| 411 |
+
| `decrease_or_not` | 15,986 | 2 β `yes`, `no` |
|
| 412 |
+
| `change_to_what` | 12,233 | 6 β change classes |
|
| 413 |
+
| `smallest_change` | 11,808 | 6 β change classes |
|
| 414 |
+
| `largest_change` | 11,808 | 6 β change classes |
|
| 415 |
+
| `change_ratio` | 7,872 | 11 β `0` + ten 10 % bins |
|
| 416 |
+
|
| 417 |
+
**`change_ratio_types` observed values differ per split:**
|
| 418 |
+
|
| 419 |
+
| Split | observed values |
|
| 420 |
+
|---|---|
|
| 421 |
+
| Train | 9 β `0` β¦ `70_to_80` |
|
| 422 |
+
| Val | 7 β `0` β¦ `50_to_60` (no `60_to_70`, no `70_to_80`) |
|
| 423 |
+
| Test | 8 β `0` β¦ `60_to_70` (no `70_to_80`) |
|
| 424 |
+
| Test2 | 8 β same as Test |
|
| 425 |
+
|
| 426 |
+
Val can never produce `60_to_70`. Train can never produce `80_to_90` or `90_to_100`. **A single global
|
| 427 |
+
label space for this question type would score the model on bins its split does not contain**, and
|
| 428 |
+
per-type confusion matrices for `change_ratio_types` are **not comparable across splits**.
|
| 429 |
+
|
| 430 |
+
**`change_ratio` and `change_ratio_types` are different question types with different vocabularies**
|
| 431 |
+
despite the similar names β `change_ratio` has 11 values including `0`; `change_ratio_types` has a
|
| 432 |
+
9-value union. They must not share a constant. The remaining six types are stable across all four
|
| 433 |
+
splits.
|
| 434 |
+
|
| 435 |
+
**The corpus uses underscore tokens** (`0_to_10`); the paper's prose writes them as percentages
|
| 436 |
+
(`0%-10%`). Same bins, different rendering β do not string-match the paper against the corpus.
|
| 437 |
+
|
| 438 |
+
### 4.7 Finding 3 β the imagery is not shipped with the annotations
|
| 439 |
+
|
| 440 |
+
Covered in Β§4.1βΒ§4.2. The measurement that ties it together: the union of unique `file_name` across
|
| 441 |
+
Train/Val/Test is **exactly 2,968**, which is exactly the number of SECOND's publicly available pairs β
|
| 442 |
+
consistent with the paper's statement that CDVQA is generated from SECOND's 2,968 pairs.
|
| 443 |
+
|
| 444 |
+
### 4.8 Temporal and label semantics β established, with honest uncertainty
|
| 445 |
+
|
| 446 |
+
This is the part of the corpus where the difference between **established** and **supported** matters
|
| 447 |
+
most, and the release keeps them apart
|
| 448 |
+
(`artifacts/cdvqa/temporal_order_evidence_v2.json` β `note`, verbatim):
|
| 449 |
+
|
| 450 |
+
> *"label1=pre / label2=post is established from the annotations against the FIXED SECOND palette (no
|
| 451 |
+
> fitting) β the reverse is directionally empty. im1=pre / im2=post is SUPPORTED by a content test
|
| 452 |
+
> (water darkness / vegetation greenness vs a white control), not proven. Nothing is inferred from
|
| 453 |
+
> directory names."*
|
| 454 |
+
|
| 455 |
+
| Mapping | State | Basis |
|
| 456 |
+
|---|---|---|
|
| 457 |
+
| **`label1` = pre, `label2` = post** | **ESTABLISHED** | the annotations, against the **fixed** SECOND palette, with **no fitting** |
|
| 458 |
+
| **`im1` = pre, `im2` = post** | **SUPPORTED β NOT PROVEN** | a content test (water darkness / vegetation greenness) vs a white control |
|
| 459 |
+
|
| 460 |
+
**The label-order evidence.** Two hypotheses were tested against the **fixed** SECOND palette:
|
| 461 |
+
|
| 462 |
+
| Hypothesis | overall agreement | n | classes collapsed to background |
|
| 463 |
+
|---|---|---|---|
|
| 464 |
+
| **h1 (label1 = pre, label2 = post)** | **1.0** | 18,724 | **none** |
|
| 465 |
+
| h2 (the reverse) | 0.7602 | 29,024 | `trees`, `water`, `playgrounds` |
|
| 466 |
+
|
| 467 |
+
Under h1 every class matches the palette with agreement **1.0** and **no class collapses**. Under h2
|
| 468 |
+
three classes collapse to background and agreement is 0.7602. The reverse is **directionally empty** β
|
| 469 |
+
this is the "established" basis.
|
| 470 |
+
|
| 471 |
+
**Why v2 supersedes v1.** The supersession reason is recorded verbatim: v1 *"fit the palette in-sample
|
| 472 |
+
to maximise H1 and computed the reversed rate as 1 - h1_rate, so the perfect split was guaranteed by
|
| 473 |
+
construction. v2 measures BOTH directions against the FIXED SECOND palette and reports directional vs
|
| 474 |
+
tie evidence separately."* A perfect separation produced by in-sample fitting is not evidence; v2
|
| 475 |
+
removes the fitting.
|
| 476 |
+
|
| 477 |
+
**A corroborating annotation signal.** `change_to_what` (a *change* question) agrees far better with an
|
| 478 |
+
answer derived from **label2** than from label1 β 0.9469 (n 9,104) vs 0.4079 (n 7,179) β and the gap is
|
| 479 |
+
stable across subsamples of 500 / 800 / 1,200 / all. A "what did it change *to*" question is answered
|
| 480 |
+
by the post-change map; the measurement says label2 is the post map.
|
| 481 |
+
|
| 482 |
+
**The image-order evidence β supportive, statistical, not exact.** `image_label_pairing`:
|
| 483 |
+
|
| 484 |
+
| Test | Value | n |
|
| 485 |
+
|---|---|---|
|
| 486 |
+
| `water_label1_im1_darker` (mean) | **0.826** | 500 |
|
| 487 |
+
| `water_label2_im2_darker` (mean) | 0.7076 | 472 |
|
| 488 |
+
| **white control: `im1` darker rate** | **0.4356** | 2,968 |
|
| 489 |
+
| `veg_label1_im1_greener` (mean) | 0.7139 | 2,415 |
|
| 490 |
+
| `veg_label2_im2_greener` (mean) | 0.7508 | 2,392 |
|
| 491 |
+
|
| 492 |
+
The water-mask test is the strongest: where `label1` marks water, `im1` is darker than `im2` in **82.6 %**
|
| 493 |
+
of cases, against a **white-pixel control rate of 43.6 %** β a ~39-point gap. The check is recorded as
|
| 494 |
+
`strength: "supportive, statistical (not exact)"`. **It is not a proof, and this document does not call
|
| 495 |
+
it one.** Nothing is inferred from directory names: `imagery_verification.json` records
|
| 496 |
+
`temporal_order_claimed: false`, and the adapter returns `image_paths()` in **archive order** by
|
| 497 |
+
construction, which does not itself establish order.
|
| 498 |
+
|
| 499 |
+
**Label palette** (fixed, from SECOND; `fixed_palette` and the `label_order` per-class RGB):
|
| 500 |
+
|
| 501 |
+
| Class | RGB |
|
| 502 |
|---|---|
|
| 503 |
+
| `NVG_surface` | (128, 128, 128) |
|
| 504 |
+
| `buildings` | (128, 0, 0) |
|
| 505 |
+
| `low_vegetation` | (0, 128, 0) |
|
| 506 |
+
| `playgrounds` | (255, 0, 0) |
|
| 507 |
+
| `trees` | (0, 255, 0) |
|
| 508 |
+
| `water` | (0, 0, 255) |
|
| 509 |
+
|
| 510 |
+
`white_semantics`: `white_share: 0.8006`, `nonwhite_where_label1_eq_label2: 0.0423` β the white pixels
|
| 511 |
+
are the background/unchanged class, which is why the white control is the right null for the darkness
|
| 512 |
+
test.
|
| 513 |
+
|
| 514 |
+
### 4.9 The corpus-root contract
|
| 515 |
|
| 516 |
+
**Corpus root:** `data/cdvqa` β the directory that *contains* `annotations/`.
|
| 517 |
|
| 518 |
+
Passing `data/cdvqa/annotations` is **rejected** by `_resolve_root`. Verified by execution
|
| 519 |
+
(`docs/PHASE10_CDVQA_DATA_STATUS.md`, header correction):
|
| 520 |
+
|
| 521 |
+
```
|
| 522 |
+
load_cdvqa('data/cdvqa/annotations')
|
| 523 |
+
-> raises: no 'annotations' directory under data\cdvqa\annotations
|
| 524 |
+
|
| 525 |
+
load_cdvqa('data/cdvqa')
|
| 526 |
+
-> loads (Val: 400 images, 16,441 questions)
|
| 527 |
+
```
|
| 528 |
+
|
| 529 |
+
The correct call is `load_cdvqa('data/cdvqa')`. This is a contract, not a preference: the loader looks
|
| 530 |
+
for `annotations/` **under** the root it is given.
|
| 531 |
+
|
| 532 |
+
### 4.10 The answer space and the question-type resolver
|
| 533 |
+
|
| 534 |
+
The change-VQA head answers in a **closed** space of **19** answers
|
| 535 |
+
(`training/change_vqa/vocab.py`; `artifacts/change_vqa/run/run_record.json` β
|
| 536 |
+
`dataset.statistics`):
|
| 537 |
+
|
| 538 |
+
| Group | Members |
|
| 539 |
+
|---|---|
|
| 540 |
+
| binary (2) | `yes`, `no` |
|
| 541 |
+
| change classes (6) | `NVG_surface`, `buildings`, `low_vegetation`, `trees`, `water`, `playgrounds` |
|
| 542 |
+
| ratio bins (11) | `0`, `0_to_10`, `10_to_20`, `20_to_30`, `30_to_40`, `40_to_50`, `50_to_60`, `60_to_70`, `70_to_80`, `80_to_90`, `90_to_100` |
|
| 543 |
+
|
| 544 |
+
`n_distinct_answers: 19`; `answers_outside_frozen_vocabulary: []`;
|
| 545 |
+
`answers_illegal_for_their_type: 0`. The measured answer distribution over the 82,408 Train+Val records
|
| 546 |
+
is led by `no` (25,612) and `yes` (22,396), with the rarest answers being `50_to_60` (175) and
|
| 547 |
+
`playgrounds` (202).
|
| 548 |
+
|
| 549 |
+
**Eight question types** map to the answer space through `resolve_question_type`
|
| 550 |
+
(`training/change_vqa/vocab.py`, with 8 types plus an `unknown` slot β 9 resolver slots and 3 temporal
|
| 551 |
+
slots). Measured counts over Train+Val:
|
| 552 |
+
|
| 553 |
+
| Question type | Count |
|
| 554 |
|---|---|
|
| 555 |
+
| `change_or_not` | 28,799 |
|
| 556 |
+
| `change_ratio_types` | 12,149 |
|
| 557 |
+
| `increase_or_not` | 9,635 |
|
| 558 |
+
| `decrease_or_not` | 9,574 |
|
| 559 |
+
| `change_to_what` | 6,251 |
|
| 560 |
+
| `largest_change` | 6,000 |
|
| 561 |
+
| `smallest_change` | 6,000 |
|
| 562 |
+
| `change_ratio` | 4,000 |
|
| 563 |
+
|
| 564 |
+
The resolver's agreement rate with the corpus's own type labels is **0.9472** (`type_resolver_agreement:
|
| 565 |
+
78059`, `type_resolver_agreement_rate: 0.9472`) β i.e. the resolver disagrees with the corpus on ~5 % of
|
| 566 |
+
questions, which is recorded rather than rounded to 1.0. `validate_vocabulary()` raises if one ratio bin
|
| 567 |
+
or one change class is removed from the ontology.
|
| 568 |
+
|
| 569 |
+
### 4.11 The split-integrity gate and the F1 defect
|
| 570 |
|
| 571 |
+
**The corpus has a per-split question-id space, and this is a measured trap.**
|
| 572 |
+
`question_id` restarts at 0 in every split: Train `0..65966`, Val `0..16440`, Test `0..39685`, Test2
|
| 573 |
+
`0..31035` β all measured, **0 gaps** (`R02_CHANGE_VQA_IMPLEMENTATION_STATUS.md` Β§5, F1).
|
|
|
|
| 574 |
|
| 575 |
+
The original `build_cdvqa_examples` indexed its questionβscene map on the **bare `question_id`**, so a
|
| 576 |
+
multi-split call let a later split overwrite an earlier one. Measured:
|
|
|
|
|
|
|
| 577 |
|
| 578 |
+
| Call | questions | scenes | correct? |
|
| 579 |
+
|---|---|---|---|
|
| 580 |
+
| `splits=("Train",)` | 65,967 | 1,600 | yes, 0 pairing errors |
|
| 581 |
+
| `splits=("Val",)` | 16,441 | 400 | yes, 0 pairing errors |
|
| 582 |
+
| `splits=("Train","Val")` | 82,408 | **1,601** | **no** β correct is 2,000 |
|
| 583 |
+
|
| 584 |
+
A mis-paired question still trained, still evaluated, and still reported a number β which is why it
|
| 585 |
+
survived. **Fixed at the root**: the key is now `(split, question_id)`, a **no-op for a single split**,
|
| 586 |
+
preserving the function signature, the 10-key output schema, the split identity and Test/Test2
|
| 587 |
+
semantics. It fixes Test/Test2 as well, where Test2's 31,036 ids used to clobber Test's over the whole
|
| 588 |
+
overlap and the wrong pairing was invisible because the two splits share their 968 scenes.
|
| 589 |
|
| 590 |
+
**The gate.** `training/change_vqa/dataset.py` provides `verify_split_integrity` and
|
| 591 |
+
`assert_split_integrity`; the latter raises when a record set has two splits sharing scenes. The
|
| 592 |
+
trainer's `FORBIDDEN_SPLITS` are `("Test", "Test2")`, and the notebook refuses to continue unless the
|
| 593 |
+
four measured split counts match exactly. The smoke test proves Test/Test2 are unreachable **the strong
|
| 594 |
+
way**: their annotation files are written as **invalid JSON**, so a run that completes *is* the proof
|
| 595 |
+
they were never parsed, and a Test-only sentinel string must not appear in the serialised run record.
|
| 596 |
|
| 597 |
+
### 4.12 What the raw loader does *not* do
|
| 598 |
+
|
| 599 |
+
The CDVQA adapter is pair-aware and `require_images=True` succeeds β but the **raw corpus has no
|
| 600 |
+
training loop of its own**. The shipped `change_vqa` head trains on **cached features**, not on the raw
|
| 601 |
+
loader. These are different code paths and are not conflated (`TRAINING.md` Β§8). The R-02 status
|
| 602 |
+
document states the starting point was "worse than adapter-only": `training/data/cdvqa.py` decodes no
|
| 603 |
+
imagery, builds no tensors, and has no reasoning path β R-02 is greenfield for the reasoning layer.
|
| 604 |
+
|
| 605 |
+
---
|
| 606 |
+
|
| 607 |
+
## 5. VRSBench β grounding
|
| 608 |
+
|
| 609 |
+
VRSBench is the grounding training and evaluation source. The schema was **VERIFIED 2026-09-16 against
|
| 610 |
+
the real dataset files** downloaded from the official mirror (HF `xiang709/VRSBench`, the dataset
|
| 611 |
+
author's own repo, 4,669 downloads, **cc-by-4.0**). Published figures, current in the repo: 29,614
|
| 612 |
+
images, 52,472 object references, 1,232,221 VQA pairs, human-verified captions
|
| 613 |
+
(`training/data/vrsbench.py`).
|
| 614 |
+
|
| 615 |
+
### 5.1 What the verified files actually contain
|
| 616 |
+
|
| 617 |
+
`VRSBench_EVAL_referring.json` β **16,159 records**:
|
| 618 |
+
|
| 619 |
+
```
|
| 620 |
+
{
|
| 621 |
+
"image_id": "P0003_0002.png", # bare filename, lives in Images_val.zip
|
| 622 |
+
"question": "The large yellow vehicle situated closest to the green area.",
|
| 623 |
+
"ground_truth": "{<25><40><33><60>}", # 0-100 xyxy as a token STRING, not a list
|
| 624 |
+
"obj_corner": [x1,y1, x2,y2, x3,y3, x4,y4], # 0-1 polygon (metadata; GT string is authority)
|
| 625 |
+
"obj_cls", "dataset", "question_id", "type", "size_group"
|
| 626 |
+
}
|
| 627 |
+
```
|
| 628 |
+
|
| 629 |
+
`VRSBench_train.json` β **142,390 conversations**, with tasks interleaved
|
| 630 |
+
(`[caption]`, `[refer]`, `[vqa]`; first-20k census 3,089 / 4,762 / 12,149).
|
| 631 |
+
|
| 632 |
+
`n_eval_records: 16159` is recorded in `artifacts/grounding/remoteclip_grounding_v001/
|
| 633 |
+
eval_result_canonical.json`.
|
| 634 |
+
|
| 635 |
+
### 5.2 The box convention β 0β100 vs 0β1
|
| 636 |
+
|
| 637 |
+
**VRSBench stores boxes normalised to 0β100.** This project stores boxes normalised to **0β1**. The
|
| 638 |
+
conversion is declared explicitly as `VRSBENCH_BOX_SCALE = 100.0` (`training/data/vrsbench.py`) and
|
| 639 |
+
`grounding.benchmark_box_scale: 100.0` (`configs/base.yaml`), never an inline divide β so it cannot be
|
| 640 |
+
applied twice or forgotten.
|
| 641 |
+
|
| 642 |
+
The module docstring states the failure mode verbatim:
|
| 643 |
+
|
| 644 |
+
> *"Treating 0-100 values as 0-1 would produce boxes a hundred times too large β an error that would
|
| 645 |
+
> show up as a suspiciously high IoU, not as a crash."*
|
| 646 |
+
|
| 647 |
+
`geospatial/transform.py` provides the named conversion helpers
|
| 648 |
+
(`benchmark_boxes_to_normalized`, `normalized_boxes_to_benchmark`, both taking `scale`), with the same
|
| 649 |
+
finding recorded in their docstring: *"Finding from Phase 0: VRSBench states verbatim that 'all box
|
| 650 |
+
coordinates are normalized to 0-100'. Treating those numbers as pixels is a silent, catastrophic
|
| 651 |
+
bug β this function exists so that mistake can only be made once, explicitly."*
|
| 652 |
|
| 653 |
Because the box convention is a common source of silent error, grounding is reported under **two
|
| 654 |
+
protocols** (canonical and matched6) and **two decode variants** β see Β§5.4 and
|
| 655 |
+
[`BENCHMARKS.md`](BENCHMARKS.md) Β§4.2.
|
| 656 |
+
|
| 657 |
+
### 5.3 The train split β and its noisy ground truths
|
| 658 |
+
|
| 659 |
+
> **TRAIN GROUND TRUTHS ARE NOISY. EVAL GROUND TRUTHS ARE CLEAN.**
|
| 660 |
+
|
| 661 |
+
This is stated verbatim in `training/data/vrsbench.py`, and the numbers matter:
|
| 662 |
+
|
| 663 |
+
| Split | Records | Ground-truth box range | Out-of-range |
|
| 664 |
+
|---|---|---|---|
|
| 665 |
+
| **eval** (`VRSBench_EVAL_referring.json`) | 16,159 | all within **[0, 100]** | **0** |
|
| 666 |
+
| **train** (`VRSBench_train.json`) | 142,390 conversations (tasks interleaved: `[caption]` / `[refer]` / `[vqa]`; first-20k census 3,089 / 4,762 / 12,149) | **36,313 refer records examined**, values reach **β73 β¦ 196** | **8,238 of 36,313** |
|
| 667 |
+
|
| 668 |
+
Out-of-range train boxes are **rejected** by the loader, which filters that noise rather than training
|
| 669 |
+
on it. The loader keeps a legacy "unknown schema β fail loudly" path: any JSON file that matches neither
|
| 670 |
+
the verified layout nor the older key-fallback table raises with the keys it actually saw.
|
| 671 |
+
|
| 672 |
+
### 5.4 The two protocols Γ two decode variants
|
| 673 |
+
|
| 674 |
+
| Protocol / variant | Value | Artifact |
|
| 675 |
+
|---|---|---|
|
| 676 |
+
| canonical, `head_threshold` | **0.2838** | `eval_result_canonical.json` |
|
| 677 |
+
| canonical, `head_argmax` | **0.1215** | `eval_result_canonical.json` |
|
| 678 |
+
| matched6, `head_threshold` (top_k 6) | **0.2566** | `eval_result_matched6.json` |
|
| 679 |
+
| zero-shot baseline | **0.0972** | `run_record.json` β `baseline_mean_best_iou` |
|
| 680 |
+
|
| 681 |
+
**Never quote one protocol alone.** The training-time validation number is different again: best val IoU
|
| 682 |
+
**0.0946**, which **did not beat** the zero-shot baseline of 0.0972 (`beats_baseline: false`). That
|
| 683 |
+
negative result is preserved ([`TRAINING.md`](TRAINING.md) Β§5.8).
|
| 684 |
+
|
| 685 |
+
---
|
| 686 |
+
|
| 687 |
+
## 6. BigEarthNet β optical-SAR fusion and VLM adaptation
|
| 688 |
+
|
| 689 |
+
BigEarthNet is used in **two** places, from **two different corpora**:
|
| 690 |
+
|
| 691 |
+
1. as the **label space and evaluation benchmark** for the optical-SAR fusion head β the **reBEN v2**
|
| 692 |
+
corpus at `data/bigearthnet_v2/reben/`;
|
| 693 |
+
2. as the **instruction-pair source** for the SmolVLM LoRA adaptation β a **BigEarthNet-S2 single-label**
|
| 694 |
+
subset.
|
| 695 |
+
|
| 696 |
+
### 6.1 The reBEN v2 corpus (optical-SAR fusion)
|
| 697 |
+
|
| 698 |
+
**The label source is the official manifest, not per-patch metadata.** The local corpus has **no
|
| 699 |
+
`metadata.json`** in any patch directory; every one of the 28,000 S2 patch directories holds only band
|
| 700 |
+
TIFFs. `training/data/bigearthnet.py`'s `_read_metadata` therefore returns `{}` and
|
| 701 |
+
`discover_patches(require_labels=True)` filters out **every** patch. The labels exist in the release's
|
| 702 |
+
official manifest, keyed by `patch_id`:
|
| 703 |
+
|
| 704 |
+
```
|
| 705 |
+
data/bigearthnet_v2/metadata.parquet 480,038 rows
|
| 706 |
+
patch_id, labels, split, country, s1_name, s2v1_name,
|
| 707 |
+
contains_seasonal_snow, contains_cloud_or_shadow
|
| 708 |
+
```
|
| 709 |
+
|
| 710 |
+
(`training/data/bigearthnet_labels.py`; `DEFAULT_METADATA_PARQUET =
|
| 711 |
+
"data/bigearthnet_v2/metadata.parquet"`; `_REQUIRED_COLUMNS = ("patch_id", "labels", "split")`.)
|
| 712 |
+
|
| 713 |
+
**Measured corpus figures** (`artifacts/phase12_selection/feasibility_report.json`):
|
| 714 |
+
|
| 715 |
+
| Quantity | Value |
|
| 716 |
+
|---|---|
|
| 717 |
+
| `metadata.parquet` rows | **480,038** |
|
| 718 |
+
| `metadata_for_patches_with_snow_cloud_or_shadow.parquet` rows | 69,450 |
|
| 719 |
+
| rows total (the two files together) | **549,488** |
|
| 720 |
+
| **tiles** | **54** |
|
| 721 |
+
| ground cells | **253,957** |
|
| 722 |
+
| impure cells | **0** |
|
| 723 |
+
| `tile_derivable_rows` | 549,488 |
|
| 724 |
+
| `tile_mismatch_patch_id_vs_s1_name` | 0 |
|
| 725 |
+
| `s2v1_name_rows_with_a_tile_token` | 0 |
|
| 726 |
+
| blocks total | **184** (train 63 / val 65 / test 56) |
|
| 727 |
+
| eligible patches by split | train 53,245 / val 22,303 / test 20,989 |
|
| 728 |
+
| official split patches | train 272,544 / val 139,577 / test 137,367 |
|
| 729 |
+
| skipped multi/zero-label by split | train 219,299 / val 117,274 / test 116,378 |
|
| 730 |
+
| patches-per-cell histogram | {1: 96,670, 2: 58,498, 3: 59,334, 4: 39,455} |
|
| 731 |
+
|
| 732 |
+
> **Tile-count discrepancy β flagged, not silently reconciled.** The verified corpus figure is
|
| 733 |
+
> **54 tiles** (`artifacts/phase12_selection/feasibility_report.json` β `corpus.tiles = 54`;
|
| 734 |
+
> `training/data/bigearthnet.py` β "the corpus spans 54 tiles"; `training/data/bigearthnet_blocks.py`
|
| 735 |
+
> β "52 of 54 tiles straddle two or more official [partitions]"). The sibling document
|
| 736 |
+
> [`MODELS.md`](MODELS.md) Β§3.3 states **"98 tiles"**, and the previous revision of this file repeated
|
| 737 |
+
> it. **No artifact read for this document reproduces 98.** The figure this document states is **54**,
|
| 738 |
+
> with its key path. The 98 figure is recorded here as a **documentation discrepancy** rather than
|
| 739 |
+
> quietly adopted or quietly deleted.
|
| 740 |
|
| 741 |
+
**The tile is a coarse leakage boundary; the block is the official unit.** The tile key is the
|
| 742 |
+
repository-wide primitive that `evaluation.leakage` uses everywhere else, but the official split's own
|
| 743 |
+
atomic unit is finer: every seasonal acquisition of a ground location `(tile, row, col)` carries the same
|
| 744 |
+
split, and all **253,957** ground cells are split-pure (**0 impure cells in 549,488 patches**). Those
|
| 745 |
+
locations then group into connected blocks that **nest inside a tile** (train frame, validation ring,
|
| 746 |
+
test core) β which is exactly why **52 of the 54 tiles straddle two or more partitions** (47 appear in
|
| 747 |
+
all three; only **2** are partition-pure). Using the tile key means `_official_split_scene_disjoint`
|
| 748 |
+
**rejects** the official split, and that rejection is a real measurement, not a formality
|
| 749 |
+
(`training/data/bigearthnet.py` β `scene_id` docstring; `docs/PHASE12_LABEL_POLICY_DECISION.md` Β§6).
|
| 750 |
|
| 751 |
+
### 6.2 The 28,000-patch selection
|
|
|
|
| 752 |
|
| 753 |
+
Training and evaluation do not use all 549,488 rows. A **28,000-patch selection** was drawn from the
|
| 754 |
+
single-label subset, scene-disjoint by T2 blocks
|
| 755 |
+
(`artifacts/phase12_selection/selection_manifest_seed10.jsonl`; the feasibility report records 20
|
| 756 |
+
per-seed manifests):
|
| 757 |
|
| 758 |
| Property | Value |
|
| 759 |
|---|---|
|
| 760 |
+
| selection manifest | `artifacts/phase12_selection/selection_manifest_seed10.jsonl` |
|
| 761 |
+
| manifest bytes / sha256 | 12,264,103 B / `fb4d8b4b024a17a842d351e9cfdbb3904648f1883406a5953a41449b375a7843` |
|
| 762 |
+
| records | **28,000** |
|
| 763 |
+
| T2 scene blocks | 61 |
|
| 764 |
+
| **tiles in the shipped selection** | **47** (12 of the 47 carry blocks in more than one split) |
|
| 765 |
+
| targets | train **20,000** / validation **4,000** / test **4,000** |
|
| 766 |
+
| scene key | `ben_<tile>:<tile-global ordinal>` (4-connected components of the (row, col) grid, per (tile, official split)) |
|
| 767 |
+
| selection order | `sha256('<seed>:<scene_id>')` ascending |
|
| 768 |
+
| label policy | `skip_ambiguous` |
|
| 769 |
+
| `all_seeds_reached_targets` / `all_seeds_scene_disjoint` | true / true |
|
| 770 |
+
|
| 771 |
+
The **tile key is too coarse** for this selection: `PairedSample.scene_id` defaults to `ben_<tile>`, but
|
| 772 |
+
12 of the 47 tiles carry blocks in more than one split, so the tile key would raise a spurious "the split
|
| 773 |
+
leaks" abort and kill the run. The manifest's `scene_id` (`ben_<tile>:<block ordinal>`) is therefore
|
| 774 |
+
passed through explicitly via `PairedSample.scene_key`
|
| 775 |
+
(`training/fusion/reben_adapter.py`).
|
| 776 |
+
|
| 777 |
+
### 6.3 The label policy β `require_single_label`
|
| 778 |
+
|
| 779 |
+
The optical-SAR fusion training and the 11.5 metric use **exactly-one-label patches only**. The policy
|
| 780 |
+
is `skip_ambiguous` (`feasibility_report.json` β `corpus.label_policy`), and
|
| 781 |
+
`training/fusion/extract.py` implements it as `require_single_label`, counting skipped patches in
|
| 782 |
+
`n_skipped_by_policy`.
|
| 783 |
+
|
| 784 |
+
The label policy was **ruled by the owner** (`docs/PHASE12_LABEL_POLICY_DECISION.md`, rulings 1β6), and
|
| 785 |
+
the arithmetic is measured:
|
| 786 |
+
|
| 787 |
+
| Quantity | Value | Basis |
|
| 788 |
+
|---|---|---|
|
| 789 |
+
| **Mean labels per patch** | **2.9540** | full corpus |
|
| 790 |
+
| Mean labels per patch | 2.9495 | clean subset |
|
| 791 |
+
| ***f* over the full corpus** | **0.175685** (17.5685 %) | official metadata, 549,488 rows |
|
| 792 |
+
| ***f* over the clean subset** | **0.179934** (17.9934 %) | official metadata, 480,038 rows |
|
| 793 |
+
| **Single-label patches** | **96,537** | official metadata |
|
| 794 |
+
| Multi-label patches | **452,951** (82.43 %) | official metadata |
|
| 795 |
+
| Retained by `require_single_label` | 96,537 (17.57 %) = 53,245 train / 22,303 val / 20,989 test | β |
|
| 796 |
+
| Discarded by `require_single_label` | 452,951 patches (82.43 %) | β |
|
| 797 |
+
|
| 798 |
+
So the corpus carries **β 2.95 classes per patch on average**, and only **17.57 %** of patches carry
|
| 799 |
+
exactly one label. The *f* β₯ 7.28 % threshold the plan set is cleared with room to spare (17.57 % vs
|
| 800 |
+
7.28 %), but the cost is explicit: the single-label policy **discards ~1.95 of every 2.95 labels per
|
| 801 |
+
patch (β 66 %)**.
|
| 802 |
+
|
| 803 |
+
### 6.4 The single-label caveat β the figure that governs how these metrics may be used
|
| 804 |
|
| 805 |
+
> ### **The local subset is 100 % single-label, against the official 1β11 multi-label scheme.**
|
| 806 |
+
>
|
| 807 |
+
> Metrics computed on this subset are therefore **not comparable** to published multi-label BigEarthNet
|
| 808 |
+
> numbers. Any statement of the form "BigEarthNet mAP = X" is **false** for this subset.
|
| 809 |
+
> (`run_manifest.json` β `corpus.coverage.single_label_fraction: 1.0`, `n_multi_label: 0`;
|
| 810 |
+
> `corpus.warnings`.)
|
| 811 |
|
| 812 |
+
The corpus-level figures the caveat is measured against: **β 2.95 labels per patch on average (max 11)**,
|
| 813 |
+
with only **17.57 %** of patches single-label (Β§6.3). The VLM run manifest states the consequence
|
| 814 |
+
verbatim:
|
| 815 |
|
| 816 |
+
> *"every one of the 24732 matched patches is single-label; the BigEarthNet corpus at large averages
|
| 817 |
+
> ~2.95 labels per patch (max 11), so this subset cannot support multi-label enumeration questions"*
|
| 818 |
|
| 819 |
+
### 6.5 The fusion evaluation detail
|
| 820 |
+
|
| 821 |
+
`artifacts/optical_sar/fusion_head_production_v001/pre_registered_115_metric.json`:
|
| 822 |
|
| 823 |
| Property | Value |
|
| 824 |
|---|---|
|
| 825 |
| split | test |
|
| 826 |
| n scored | **4,000** |
|
| 827 |
| classes in label space | 19 |
|
| 828 |
+
| classes present in the scored split | **14** β `[0,2,3,4,5,6,7,8,9,10,12,13,17,18]` |
|
| 829 |
+
| classes absent | **5** β `[1,11,14,15,16]` |
|
| 830 |
+
| **macro-F1 denominator** | **"all 19 classes (absent classes contribute 0.0)"** |
|
| 831 |
+
| accuracy | **0.931** |
|
| 832 |
+
| **macro F1** | **0.434161** |
|
| 833 |
+
| loss | 0.254592 |
|
| 834 |
+
| `is_deciding_statistic` | **false** |
|
| 835 |
+
|
| 836 |
+
The macro-F1 denominator is recorded explicitly so a reader can see that absent classes drag the macro
|
| 837 |
+
score down **by construction**. This is why accuracy (0.931) and macro-F1 (0.434161) must be read
|
| 838 |
+
together, and why the ruling is `OPEN`. Classes 5 and 6 are *present* in the scored split and still
|
| 839 |
+
score `0.0` (`docs/PHASE12_115_METRIC_COMPUTED.md`); the alternative denominator (14 classes) would give
|
| 840 |
+
macro-F1 **0.589218** instead of **0.434161**.
|
| 841 |
+
|
| 842 |
+
**The 19-class label space** is `CLC19_CLASSES`, an ordered 19-name tuple in
|
| 843 |
+
`training/data/bigearthnet.py`. The per-class test label distribution recorded in
|
| 844 |
+
`docs/PHASE12_115_METRIC_COMPUTED.md` is
|
| 845 |
+
`[13,0,63,39,846,4,1,6,453,246,4,0,9,18,0,0,0,34,2264]` β a strongly skewed distribution dominated by
|
| 846 |
+
class 18.
|
| 847 |
+
|
| 848 |
+
> **The BigEarthNet documentation β its uses, mentions, or endorsements β does not specify a percentile
|
| 849 |
+
> stretch.** This project nevertheless applies percentile normalisation for optical inputs (2/98) to
|
| 850 |
+
> match the CROMA contract. That is a **deliberate, documented choice**, not an upstream fact
|
| 851 |
+
> (`docs/RESEARCH_NOTES.md`).
|
| 852 |
+
|
| 853 |
+
### 6.6 The VLM subset β a different BigEarthNet corpus
|
| 854 |
+
|
| 855 |
+
The VLM LoRA adapter consumes a **BigEarthNet-S2 single-label** subset
|
| 856 |
+
(`run_manifest.json` β `config.corpus_root`:
|
| 857 |
+
`/kaggle/input/datasets/creatorballs/bigearth-net-s2-single-label/BigEarthNet-S2`), **not** the reBEN v2
|
| 858 |
+
fusion corpus.
|
| 859 |
+
|
| 860 |
+
| Property | Value | Key path |
|
| 861 |
+
|---|---|---|
|
| 862 |
+
| instruction families | `["presence"]` | `corpus.families` |
|
| 863 |
+
| n samples (questions) | 49,464 | `corpus.n_samples` |
|
| 864 |
+
| n requested patches | 28,000 | `corpus.coverage.n_requested` |
|
| 865 |
+
| n matched | **24,732** | `corpus.coverage.n_matched` |
|
| 866 |
+
| **coverage fraction** | **0.883286** | `corpus.coverage.coverage_fraction` |
|
| 867 |
+
| n unmatched | **3,268** (11.7 %) | `corpus.coverage.n_unmatched` |
|
| 868 |
+
| single-label fraction | **1.0** | `corpus.coverage.single_label_fraction` |
|
| 869 |
+
| scene blocks (train / val / test) | 1,160 / 482 / 202 | `corpus.scene_counts` |
|
| 870 |
+
| patches (train / val / test) | 17,471 / 3,375 / 3,886 | `corpus.split_info.patches_by_split` |
|
| 871 |
+
| samples (train / val / test) | 34,942 / 6,750 / 7,772 | `corpus.split_counts` |
|
| 872 |
+
| scene key | `ben_<tile>:<k>` | `corpus.split_info.scene_key` |
|
| 873 |
+
| render | RGB `[B04, B03, B02]`, per-band percentile stretch 2/98, uint8 | `corpus.render` |
|
| 874 |
+
|
| 875 |
+
**The unmatched 3,268 patches are excluded, not guessed.** `corpus.warnings`, verbatim:
|
| 876 |
|
| 877 |
+
> *"3268 of 28000 patches have no row in the manifest and carry NO labels; they are excluded rather than
|
| 878 |
+
> guessed (88.3% coverage)"*
|
|
|
|
| 879 |
|
| 880 |
+
`training/data/bigearthnet_labels.py` records the same rule: a patch with no manifest row gets **no
|
| 881 |
+
labels** β not a default, not a nearest-neighbour, not a guessed one. It is reported as unlabelled and
|
| 882 |
+
excluded, and the count is surfaced by `coverage()` so the shortfall is **a number in the run manifest
|
| 883 |
+
rather than a silent shrinkage of the training set**. `LabelCoverage` carries `n_requested`,
|
| 884 |
+
`n_matched`, `n_unmatched`, `n_multi_label`, `n_single_label` and `single_label_fraction`.
|
| 885 |
|
| 886 |
+
**The render is per-patch, not corpus-global** β `corpus.render.note` records that this is deliberate,
|
| 887 |
+
"so inference needs no training-set statistics". The split policy is
|
| 888 |
+
`release_partition_keyed_by_T2_blocks`, with a leakage check calling
|
| 889 |
+
`evaluation.leakage.assert_no_scene_overlap`, and the block reconstruction is conservative:
|
| 890 |
+
*"blocks are reconstructed from this corpus's patches, not the full release; that can only split a block
|
| 891 |
+
further, never merge across a partition."*
|
| 892 |
|
| 893 |
+
### 6.7 The format contradiction β reported, not silently resolved
|
|
|
|
|
|
|
| 894 |
|
| 895 |
+
> **PARTIALLY β reported, not silently resolved.** The BigEarthNet data format contradicts the original
|
| 896 |
+
> plan. This was reported rather than quietly patched, because silently changing the preprocessing would
|
| 897 |
+
> move the frozen config hash (`78f1e3700da15aa1`). See [`RESEARCH_NOTES.md`](RESEARCH_NOTES.md).
|
| 898 |
|
| 899 |
+
The concrete contradictions the project hit, each measured:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 900 |
|
| 901 |
+
| Contradiction | What the plan/release implied | What the data showed |
|
| 902 |
+
|---|---|---|
|
| 903 |
+
| per-patch `metadata.json` | a per-patch label source exists | **no `metadata.json` in any of the 28,000 patch dirs**; labels live only in the parquet (`training/data/bigearthnet_labels.py`) |
|
| 904 |
+
| S1/S2 co-located | one patch dir holds both modalities | **separate trees with disjoint names** β the S1 and S2 name sets intersect in **0 of 28,000**; the parquet's `s1_name` is the only join key (`training/fusion/reben_adapter.py`) |
|
| 905 |
+
| uniform band resolution | bands share one pixel size | **mixed native resolutions** β 120Γ120 (10 m), 60Γ60 (20 m), 20Γ20 (60 m) inside the *same* patch; resampling onto the 10 m grid is well defined because all bands share the same geographic bounds |
|
| 906 |
+
| a tile-level split is geographical | "one tile does not straddle train/test" | **FALSE** β 52 of 54 tiles straddle; the official unit is the ground block (`training/data/bigearthnet.py`) |
|
| 907 |
|
| 908 |
+
Each contradiction is handled by an **additive** module rather than by editing the existing one:
|
| 909 |
+
`training/fusion/reben_adapter.py` "is ADDITIVE. It does not modify `pair_patches`, `discover_patches` or
|
| 910 |
+
the extraction core." This is why the frozen config hash did not move when the v2 corpus was integrated.
|
| 911 |
+
|
| 912 |
+
---
|
| 913 |
+
|
| 914 |
+
## 7. Data hygiene and leakage controls
|
| 915 |
+
|
| 916 |
+
This section covers the rules that apply to **every** corpus above, and the code that enforces them.
|
| 917 |
+
|
| 918 |
+
### 7.1 Splits are by scene (or group), never by example
|
| 919 |
+
|
| 920 |
+
The classic remote-sensing leakage bug is slicing a large scene into tiles and then splitting the
|
| 921 |
+
**tiles** randomly. Adjacent tiles are near-duplicates, so a random tile split leaks the test set into
|
| 922 |
+
training. Every split in this project is at the **scene** (or **group**) level.
|
| 923 |
+
|
| 924 |
+
| Corpus | Split key | Definition |
|
| 925 |
+
|---|---|---|
|
| 926 |
+
| LEVIR-CD | scene | the LEVIR scene name inside `<split>_<scene>_<tile>.png`; 445 / 64 / 128 scenes |
|
| 927 |
+
| CDVQA | scene (`file_name`) | the CDVQA scene; Train/Val/Test disjoint, Test β© Test2 = 968 (allowed pair) |
|
| 928 |
+
| VRSBench | image | `training/grounding/dataset.py` splits by image |
|
| 929 |
+
| BigEarthNet (fusion) | **block** | `ben_<tile>:<block ordinal>` β 4-connected components of the (row, col) grid, per (tile, official split) |
|
| 930 |
+
| BigEarthNet (VLM) | block | `ben_<tile>:<k>` |
|
| 931 |
+
| Router | **group** | template family / hard-negative family (`router/dataset.py::split_by_group`) |
|
| 932 |
+
|
| 933 |
+
### 7.2 The leakage module β five rules, enforced in code
|
| 934 |
+
|
| 935 |
+
`evaluation/leakage.py` opens with the rules and states that they are "non-negotiable and are enforced in
|
| 936 |
+
code, not by convention":
|
| 937 |
+
|
| 938 |
+
1. **Split by SCENE, never by sample/tile.**
|
| 939 |
+
2. **No scene may appear in more than one split.**
|
| 940 |
+
3. **Duplicate content (same sha256) is rejected.**
|
| 941 |
+
4. **Public test sets are immutable and off-limits to training code.**
|
| 942 |
+
5. **Hidden data must never influence thresholds, prompts, or routing.**
|
| 943 |
+
|
| 944 |
+
And the module's own operating principle, verbatim:
|
| 945 |
+
|
| 946 |
+
> *"Every function here either returns a clean result or raises. None of them 'warn and continue' β a
|
| 947 |
+
> leakage warning that scrolls past is indistinguishable from no check."*
|
| 948 |
+
|
| 949 |
+
The enforcement surface:
|
| 950 |
+
|
| 951 |
+
| Function | What it does |
|
| 952 |
|---|---|
|
| 953 |
+
| `assign_splits_by_scene(records, *, train_ratio, val_ratio, seed)` | assigns splits at the **scene** level, deterministically. Scene keys are **sorted first**, then shuffled with a seeded RNG, so the assignment is reproducible and independent of input order. **A scene's records all receive the same split β enforced, not hoped for.** Emits in sorted scene order (the seed controls *which* scene goes to which split, never the output order). |
|
| 954 |
+
| `audit_manifest(manifest, *, require_all_splits)` | full audit; **raises `LeakageError`** on a scene in >1 split or a duplicate content hash. Adds advisory warnings: not all splits populated; and `scene_key` falling back to `sample_id` (which makes scene-level isolation **vacuous** for those samples). |
|
| 955 |
+
| `assert_no_scene_overlap(train, test)` | hard assertion that two collections share no scene; raises with the overlap count and up to five example scene keys. |
|
| 956 |
+
| `PublicTestFirewall(root)` | two-layer defence (Β§7.4). |
|
| 957 |
+
| `assert_no_hidden_access(config)` | guards the hidden-evaluation contract (Β§7.5). |
|
| 958 |
+
| `deduplicate(records)` | drops duplicate-content records, returning `(kept, dropped_sample_ids)`. |
|
| 959 |
+
| `content_hash_file(path)` | streamed SHA-256, delegating to `preprocessing/raster.py::file_sha256`. |
|
| 960 |
+
|
| 961 |
+
### 7.3 `leakage_split_key: scene_id`
|
| 962 |
+
|
| 963 |
+
Declared in `configs/base.yaml` Β§`evaluation`. The `scene_key` property resolves, in order, an explicit
|
| 964 |
+
`scene_key`, then `scene_id`, then a geographic hash, then the source scene, then falls back to
|
| 965 |
+
`sample_id`. The `audit_manifest` advisory explicitly flags the fallback case, because a sample whose
|
| 966 |
+
`scene_key` has fallen all the way back to its `sample_id` has **no** scene-level isolation at all.
|
| 967 |
+
|
| 968 |
+
### 7.4 The public-test firewall
|
| 969 |
+
|
| 970 |
+
`evaluation/leakage.py::PublicTestFirewall` (`PUBLIC_TEST_DIRNAME = "public_test"`) blocks training-time
|
| 971 |
+
reads of the public test tree with **two layers**:
|
| 972 |
+
|
| 973 |
+
| Layer | Method | Behaviour |
|
| 974 |
+
|---|---|---|
|
| 975 |
+
| **path inspection** | `check_read(path)` | any read whose **resolved** path is under the public test root is refused while the firewall is armed |
|
| 976 |
+
| **manifest inspection** | `check_manifest(manifest)` | a manifest containing test-split records may not be handed to a training loop |
|
| 977 |
+
|
| 978 |
+
The firewall is **armed by default in training scripts** and disarmed only by the evaluation runner,
|
| 979 |
+
which is a separate entry point. `arm()` / `disarm()` / `.armed` are exposed so the state is inspectable.
|
| 980 |
+
Both checks raise `LeakageError` with the offending path or record count in the context.
|
| 981 |
+
|
| 982 |
+
### 7.5 Hidden-set discipline
|
| 983 |
+
|
| 984 |
+
`assert_no_hidden_access(config)` enforces the hidden-evaluation contract at runtime:
|
| 985 |
+
|
| 986 |
+
| Condition | Behaviour |
|
| 987 |
+
|---|---|
|
| 988 |
+
| `evaluation.hidden_data_access is not False` | **raises** `LeakageError` |
|
| 989 |
+
| `evaluation.official_aggregate_weights is not None` | **raises** `LeakageError` |
|
| 990 |
+
|
| 991 |
+
The second is why `configs/base.yaml` records `official_aggregate_weights: null` β inventing an aggregate
|
| 992 |
+
formula is prohibited ([`BENCHMARKS.md`](BENCHMARKS.md) Β§10.7 and
|
| 993 |
+
[`EVALUATION.md`](EVALUATION.md) rule 8). `immutable_public_test: true` and `hidden_data_access: false`
|
| 994 |
+
are the two config values that make the firewall and the hidden guard meaningful.
|
| 995 |
+
|
| 996 |
+
### 7.6 Deduplication
|
| 997 |
+
|
| 998 |
+
`deduplicate()` removes records whose content sha256 has already been seen, keeping the first
|
| 999 |
+
occurrence and returning the dropped sample ids. Duplicates are treated as both a leakage vector **and**
|
| 1000 |
+
a silent train/test contamination path, so they are removed, not tolerated. The dedup key is the
|
| 1001 |
+
**content** hash (streamed SHA-256), not the file name β two differently-named copies of the same pixels
|
| 1002 |
+
are the same record.
|
| 1003 |
+
|
| 1004 |
+
### 7.7 The GeoTIFF contract
|
| 1005 |
+
|
| 1006 |
+
The raster input contract is `preprocessing/raster.py`, and it implements the frozen validation chain
|
| 1007 |
+
from `docs/ARCHITECTURE_FREEZE.md` Β§2.6:
|
| 1008 |
+
|
| 1009 |
+
```
|
| 1010 |
+
file -> dimensions -> bands -> dtype -> CRS -> transform -> bounds -> nodata
|
| 1011 |
+
-> modality -> temporal metadata
|
| 1012 |
+
```
|
| 1013 |
+
|
| 1014 |
+
Design rules, verbatim from the module docstring:
|
| 1015 |
+
|
| 1016 |
+
> *"Never raise a bare exception. Every failure is a typed SatQueryError. Never silently drop geospatial
|
| 1017 |
+
> metadata. If the source had a CRS, the returned AssetMetadata says so. A missing CRS degrades to
|
| 1018 |
+
> non-geospatial mode; it does not abort."*
|
| 1019 |
+
|
| 1020 |
+
`inspect_raster(path, *, max_pixels, explicit_modality, compute_hash, sensor)` **describes a raster
|
| 1021 |
+
without loading its pixel data** and returns `AssetMetadata` with full geospatial detail preserved. The
|
| 1022 |
+
recorded fields include `crs`, `transform` (the first 6 affine coefficients), `bounds`, `width`,
|
| 1023 |
+
`height`, `band_count`, `dtype`, `nodata`, `resolution`, `has_crs`, `is_georeferenced`, `driver`,
|
| 1024 |
+
`is_tiled`.
|
| 1025 |
+
|
| 1026 |
+
| Guard | Behaviour |
|
| 1027 |
+
|---|---|
|
| 1028 |
+
| file missing / not a regular file | `RasterReadError` |
|
| 1029 |
+
| rasterio unavailable | `RasterReadError` |
|
| 1030 |
+
| degenerate dimensions (`<= 0`) | `RasterReadError` |
|
| 1031 |
+
| `max_pixels` exceeded | `OversizedImageError` (**recoverable** β the caller may downscale) |
|
| 1032 |
+
| `band_count <= 0` | `UnsupportedBandsError` |
|
| 1033 |
+
| unreadable raster | `RasterReadError` |
|
| 1034 |
+
|
| 1035 |
+
**Modality inference** is a heuristic and is labelled as one: `_OPTICAL_BAND_COUNTS = {3,4,8,11,12,13}`
|
| 1036 |
+
and `_SAR_BAND_COUNTS = {1,2}`, with an explicit label honoured first. The docstring is explicit that
|
| 1037 |
+
these are "heuristics, not ground truth β the sensor adapter is authoritative when a sensor descriptor
|
| 1038 |
+
is supplied".
|
| 1039 |
+
|
| 1040 |
+
**Georeferencing is preserved through preprocessing, never silently stripped.** `read_bands` returns
|
| 1041 |
+
`(bands, H, W)` plus a profile that retains `crs`/`transform`/`bounds`/`nodata`; `write_raster` writes
|
| 1042 |
+
using that profile so evidence artifacts (change maps, masks) stay georeferenced. `preprocessing/
|
| 1043 |
+
imagery.py::load_image_array` converts a raster to a displayable uint8 `(H, W, 3)` array with a
|
| 1044 |
+
**deterministic** 2/98 percentile stretch (over finite values only, so a nodata sentinel does not crush
|
| 1045 |
+
the dynamic range) β and it **does not resample, crop, or reproject**, because the grounding specialist
|
| 1046 |
+
converts normalised boxes to pixel coordinates using the **original** raster's dimensions, and a silent
|
| 1047 |
+
resize here would put every box in the wrong place.
|
| 1048 |
+
|
| 1049 |
+
### 7.8 CRS comparison and pair compatibility
|
| 1050 |
+
|
| 1051 |
+
"Can these two rasters be compared at all?" is answerable **without touching pixel arithmetic** β
|
| 1052 |
+
`geospatial/crs.py` is kept separate from `geospatial/transform.py` for exactly that reason.
|
| 1053 |
+
|
| 1054 |
+
`compare_crs(left, right)` returns a `CRSCompatibility`:
|
| 1055 |
+
|
| 1056 |
+
| Case | `compatible` | `identical` | reason |
|
| 1057 |
+
|---|---|---|---|
|
| 1058 |
+
| identical CRS | true | true | "identical CRS" |
|
| 1059 |
+
| different but same kind (both projected or both geographic) | true | false | "reprojection required (A -> B)" |
|
| 1060 |
+
| mixed projected/geographic | true | false | "mixed projected/geographic CRS β¦; reprojection required and should be verified" |
|
| 1061 |
+
| **either side missing a CRS** | **false** | false | "one or both rasters lack a CRS; spatial comparison is unsafe" |
|
| 1062 |
+
|
| 1063 |
+
A missing CRS on either side is **not** compatible for spatial comparison β but it is **not fatal for
|
| 1064 |
+
non-spatial tasks** (a VQA question about a single image does not need a CRS). `is_metric(crs)` returns
|
| 1065 |
+
`True` only when the CRS is projected and its axis unit is metres, so `pixel_area_m2()` returns `None`
|
| 1066 |
+
for a geographic CRS rather than guessing.
|
| 1067 |
+
|
| 1068 |
+
**Temporal metadata and pair compatibility.** The `AssetMetadata` carries the geospatial detail, and the
|
| 1069 |
+
pair-compatibility question ("are these two rasters a usable change pair?") is answered by the change
|
| 1070 |
+
specialist's preconditions rather than by a single function: the pair must share a scene, and `T1 != T2`
|
| 1071 |
+
(the two acquisitions must differ). The LEVIR verification measures this directly β the check "T1 and T2
|
| 1072 |
+
are not identical" passed with 0 identical pairs in a 600-tile sample
|
| 1073 |
+
(`artifacts/change/levir_real_data_verification.json`). For CDVQA the temporal question is the harder
|
| 1074 |
+
one and is handled by the evidence engine in Β§4.8, because the pair is identified by a **shared
|
| 1075 |
+
basename** in the SECOND layout, not by embedded temporal metadata.
|
| 1076 |
+
|
| 1077 |
+
### 7.9 The pixel β geo conversion is first-class and unit-tested
|
| 1078 |
+
|
| 1079 |
+
`geospatial/transform.py` makes the `coordinate_system` field real. Every spatial result the system
|
| 1080 |
+
produces passes through here at least once, so this is the single place a normalised 0β1 box, a pixel
|
| 1081 |
+
box, and a geographic box are converted between. The non-negotiable rules, verbatim:
|
| 1082 |
+
|
| 1083 |
+
> *"CRS and affine transform are preserved, never silently stripped. Conversions are explicit about
|
| 1084 |
+
> their source and target frames. A conversion that cannot be performed raises, rather than returning a
|
| 1085 |
+
> plausible lie."*
|
| 1086 |
+
|
| 1087 |
+
| Function | Contract |
|
| 1088 |
+
|---|---|
|
| 1089 |
+
| `normalized_to_pixel(box, width, height)` | values are **not clipped** β a box slightly outside the frame is preserved so the caller can decide whether that is a bug or a legitimate edge case |
|
| 1090 |
+
| `pixel_to_normalized(window, width, height)` | inverse; raises on non-positive dimensions |
|
| 1091 |
+
| `pixel_to_geo(window, transform)` | uses the affine; the geographic `miny` comes from the **bottom** row, because the y axis usually points down in pixel space |
|
| 1092 |
+
| `geo_to_pixel(bounds, transform)` | inverts the affine; raises `CoordinateError` if the transform is not invertible |
|
| 1093 |
+
| `to_normalized` / `to_pixel` / `to_geo` | schema-aware `Box` conversions; each requires enough context for the source frame and **raises** rather than guessing |
|
| 1094 |
+
| `intersect` / `iou` | axis-aligned intersection and IoU, used by grounding NMS and change-region overlap |
|
| 1095 |
+
| `benchmark_boxes_to_normalized` / `normalized_boxes_to_benchmark` | the explicit 0β100 β 0β1 conversion (Β§5.2) |
|
| 1096 |
+
| `affine_from_metadata(geo)` | reconstructs an `Affine` from a `GeoMetadata.transform` list of length 6 |
|
| 1097 |
+
|
| 1098 |
+
`PixelWindow.__post_init__` raises `CoordinateError` on an **inverted** window (`col_max < col_min` or
|
| 1099 |
+
`row_max < row_min`), so a swapped-corner box cannot propagate.
|
| 1100 |
+
|
| 1101 |
+
### 7.10 The deterministic input-quality gate
|
| 1102 |
+
|
| 1103 |
+
`preprocessing/quality.py` exists because of finding **F5-5** (`docs/PHASE5_VLM_CONTRACT.md`): a loaded
|
| 1104 |
+
SmolVLM-500M-Instruct, given 512Γ512 **uniform random noise** and a prompt that explicitly says "answer
|
| 1105 |
+
only from what is visible" and "if the image does not contain enough information, say so plainly",
|
| 1106 |
+
produced:
|
| 1107 |
+
|
| 1108 |
+
> *"A black and white photograph of a man and a woman, who appear to be in a room, with a table in front
|
| 1109 |
+
> of them."*
|
| 1110 |
+
|
| 1111 |
+
That is a fluent, specific, entirely fabricated scene description. The prompt cannot prevent it β a
|
| 1112 |
+
500M-parameter VLM will describe *something* for any input, and asking it to self-assess reliably is
|
| 1113 |
+
asking it to do the thing it just failed at.
|
| 1114 |
+
|
| 1115 |
+
So the gate is **deterministic and upstream of the model**. The discriminating signal is **lag-1 spatial
|
| 1116 |
+
autocorrelation**, not variance:
|
| 1117 |
+
|
| 1118 |
+
| Input | Autocorrelation |
|
| 1119 |
+
|---|---|
|
| 1120 |
+
| real remote-sensing imagery | 0.6 β 0.99 |
|
| 1121 |
+
| uniform random noise | ~ 0.00 |
|
| 1122 |
+
| a constant (blank) image | undefined; variance ~ 0 |
|
| 1123 |
+
|
| 1124 |
+
Variance alone cannot separate these: a flat desert scene and a flat "all-zero" tile both have
|
| 1125 |
+
near-zero variance, but the desert has strong neighbour correlation and the zero tile does not. The gate
|
| 1126 |
+
classifies an array as `STRUCTURED`, `FLAT`, `NOISE`, `TOO_SMALL` or `INVALID_VALUES`;
|
| 1127 |
+
`BLOCKING_VERDICTS = {NOISE, INVALID_VALUES}` must block a VLM call. The thresholds and their measured
|
| 1128 |
+
margins:
|
| 1129 |
+
|
| 1130 |
+
| Threshold | Value | Measured margin |
|
| 1131 |
+
|---|---|---|
|
| 1132 |
+
| `MIN_AUTOCORRELATION` | 0.10 | structured 0.952 vs noise β0.008 β a **0.96 margin** |
|
| 1133 |
+
| `FLAT_STD_EPSILON` | 1e-6 | a constant array β `FLAT` |
|
| 1134 |
+
| `NOISE_ENTROPY_BITS` | 7.8 | structured ramp+texture 7.581 vs noise 7.988 β a **0.22-bit gap**, thin by design |
|
| 1135 |
+
|
| 1136 |
+
The module is explicit that **autocorrelation carries the gate** and entropy is a redundant second
|
| 1137 |
+
signal: *"It is kept because a future change to either metric should not silently disable the gate, but
|
| 1138 |
+
do not rely on it alone."* `assess_image_quality` has **no tolerance** for non-finite values β a fixed
|
| 1139 |
+
fraction would be size-dependent, so one NaN in a 64Γ64 array (0.99976) would pass while one NaN in a
|
| 1140 |
+
10Γ10 array (0.99) would fail; the same defect must not be tolerated or rejected depending on image
|
| 1141 |
+
dimensions. Known limitation, recorded deliberately: a float GeoTIFF whose nodata sentinel is NaN is
|
| 1142 |
+
refused, because the alternative is computing correlation over NaN.
|
| 1143 |
+
|
| 1144 |
+
**Nothing here is learned, sampled, or probabilistic. Same input, same verdict.**
|
| 1145 |
+
|
| 1146 |
+
---
|
| 1147 |
+
|
| 1148 |
+
## 8. What is NOT available β exhaustive
|
| 1149 |
+
|
| 1150 |
+
| Corpus / resource | State |
|
| 1151 |
+
|---|---|
|
| 1152 |
+
| **BigEarthNet S1 + S2 full corpus (549,488 rows)** | **NOT DOWNLOADED** β only the 28,000-patch selection is materialised locally |
|
| 1153 |
+
| **BigEarthNet multi-label (reBEN) results** | **NOT PRODUCED** β the local subset is single-label; metrics are not comparable to published numbers (Β§6.4) |
|
| 1154 |
+
| **BigEarthNet-S2 full single-label corpus** | **NOT DOWNLOADED** β the VLM run matched 24,732 of 28,000 requested (Β§6.6) |
|
| 1155 |
+
| **Cross-dataset generalisation sets** | **not used** |
|
| 1156 |
+
| **Any private / hidden evaluation data** | **not accessed** (`hidden_data_access: false`) |
|
| 1157 |
+
| **The public test split, for training** | **blocked by the firewall** (Β§7.4) |
|
| 1158 |
+
| **CDVQA imagery from the CDVQA repo** | **does not exist** β the repo publishes annotations only; imagery comes from SECOND (Β§4.1) |
|
| 1159 |
+
| **CDVQA label-map semantics** | **partially decoded** β the palette is fixed and the order is established (Β§4.8), but "semantics **not decoded**" is the state recorded for `label1/`/`label2/` at acquisition (`docs/PHASE10_CDVQA_DATA_STATUS.md` Β§1) |
|
| 1160 |
+
| **VRSBench caption / VQA benchmark scoring** | **NOT RUN** β the loader verifies the schema; captioning metrics are governed by R-16, which is `OPEN` |
|
| 1161 |
+
| **VRSBench `obj_corner` polygons as training targets** | **not used** β the GT token string is the authority; `obj_corner` is metadata |
|
| 1162 |
+
| **A `tiles` figure for the VLM BigEarthNet-S2 subset** | `UNKNOWN β not established from the available evidence` |
|
| 1163 |
+
| **The authoritative router parameter count (50,822 vs 51,725)** | `UNKNOWN β not established from the available evidence` ([`TRAINING.md`](TRAINING.md) Β§4.1) |
|
| 1164 |
+
| **A licence file for this repository** | **does not exist** β `OPEN` |
|
| 1165 |
+
| **Kaggle runtime for the external runs** | **not measured** β the guides give measured *local* figures and label the Kaggle figures as estimates |
|
| 1166 |
+
|
| 1167 |
+
---
|
| 1168 |
+
|
| 1169 |
+
## 9. What is NOT RUN / OPEN / BLOCKED for this topic
|
| 1170 |
+
|
| 1171 |
+
**NOT RUN**
|
| 1172 |
+
|
| 1173 |
+
- Any **multi-label** BigEarthNet evaluation (the local subset is single-label).
|
| 1174 |
+
- Any **cross-dataset** generalisation evaluation.
|
| 1175 |
+
- The **VRSBench captioning / VQA** benchmarks (R-16 `OPEN`).
|
| 1176 |
+
- The **change-VQA** evaluation on any split beyond Test and Test2 (no third held-out set exists).
|
| 1177 |
+
- The **VLM adapter on a second instruction family** β `instruction_families: ["presence"]` only.
|
| 1178 |
+
- A **full-corpus** BigEarthNet download or extraction.
|
| 1179 |
+
|
| 1180 |
+
**OPEN**
|
| 1181 |
+
|
| 1182 |
+
- The **BigEarthNet tile-count discrepancy** (54 verified vs 98 in `MODELS.md`) β reported, not resolved
|
| 1183 |
+
(Β§6.1).
|
| 1184 |
+
- The **BigEarthNet format contradiction** β reported, not silently resolved (Β§6.7).
|
| 1185 |
+
- The **CDVQA `im1`/`im2` temporal order** β `SUPPORTED β NOT PROVEN` (Β§4.8).
|
| 1186 |
+
- The **CDVQA label-map semantics** β the palette is fixed and the order is established, but full
|
| 1187 |
+
semantic decoding of the label maps is not claimed.
|
| 1188 |
+
- The **optical-SAR ruling** β accuracy 0.931 / macro-F1 0.434161, `is_deciding_statistic: false`
|
| 1189 |
+
(Β§6.5).
|
| 1190 |
+
- The **change-VQA ruling** β `metric_ruling: OPEN` on both test sets.
|
| 1191 |
+
- The **change registration gate** β 58.7 % false positives on the serving path (Β§3.7).
|
| 1192 |
+
- The **licence** β no LICENSE file exists.
|
| 1193 |
+
- **R-03** β calibration; nothing is fitted.
|
| 1194 |
+
|
| 1195 |
+
**BLOCKED**
|
| 1196 |
+
|
| 1197 |
+
- Nothing blocks any training or evaluation run. The one recorded external blocker that *was* crossed is
|
| 1198 |
+
the optical-SAR Arm-B feature cache (a ~2.7 h CPU extraction), which was completed
|
| 1199 |
+
([`TRAINING.md`](TRAINING.md) Β§7.4).
|
| 1200 |
+
|
| 1201 |
+
---
|
| 1202 |
+
|
| 1203 |
+
## 10. Where the evidence lives
|
| 1204 |
+
|
| 1205 |
+
| Corpus | Artifact(s) |
|
| 1206 |
+
|---|---|
|
| 1207 |
+
| LEVIR-CD-256 | `artifacts/change/levir_real_data_verification.json`, `artifacts/change/eval_test/eval_result.json`, `artifacts/change/levir_change_v001/{run_record,training_metadata}.json`, `artifacts/change/threshold_sweep_val.json`, `training/change/dataset.py`, `training/change/train.py`, `docs/PHASE9_REAL_DATA_VERIFICATION.md`, `docs/PHASE9_FREEZE.md` |
|
| 1208 |
+
| CDVQA + SECOND | `artifacts/cdvqa/{second_overlap,imagery_verification,temporal_order_evidence_v2}.json`, `docs/PHASE10_CDVQA_DATA_STATUS.md`, `docs/ARCHITECTURE_CHANGE_CDVQA_IMAGE_PAIRS.md`, `docs/PHASE10_ENTRY_GATE.md`, `artifacts/change_vqa/run/run_record.json`, `training/change_vqa/{dataset,vocab}.py`, `training/data/cdvqa.py` |
|
| 1209 |
+
| VRSBench | `training/data/vrsbench.py`, `artifacts/grounding/remoteclip_grounding_v001/{run_record,training_metadata,eval_result_canonical,eval_result_matched6}.json` |
|
| 1210 |
+
| BigEarthNet (fusion) | `artifacts/phase12_selection/feasibility_report.json`, `artifacts/phase12_selection/selection_manifest_seed10.jsonl`, `artifacts/optical_sar/fusion_head_production_v001/{pre_registered_115_metric,production_head_record}.json`, `training/data/bigearthnet.py`, `training/data/bigearthnet_labels.py`, `training/data/bigearthnet_blocks.py`, `training/fusion/reben_adapter.py`, `docs/PHASE12_LABEL_POLICY_DECISION.md`, `docs/PHASE12_115_METRIC_COMPUTED.md`, `docs/PHASE12_ENTRY_GATE.md` |
|
| 1211 |
+
| BigEarthNet (VLM) | `.scratch/phase6_real_adapter/phase6_adapter/run_manifest.json`, `scripts/prepare_bigearthnet.py` |
|
| 1212 |
+
| Leakage / firewall | `evaluation/leakage.py`, `evaluation/manifest_freeze.json`, `evaluation/public_test/`, `tests/leakage/` |
|
| 1213 |
+
| GeoTIFF contract / pair compatibility | `preprocessing/raster.py`, `preprocessing/imagery.py`, `preprocessing/quality.py`, `geospatial/crs.py`, `geospatial/transform.py`, `docs/ARCHITECTURE_FREEZE.md` Β§2.6 |
|
| 1214 |
+
| Quality gate | `preprocessing/quality.py`, `docs/PHASE5_VLM_CONTRACT.md` (finding F5-5) |
|
| 1215 |
+
|
| 1216 |
+
**Sibling documents:** [`TRAINING.md`](TRAINING.md) (how each corpus is consumed),
|
| 1217 |
+
[`EVALUATION.md`](EVALUATION.md) (protocols), [`BENCHMARKS.md`](BENCHMARKS.md) (the numbers),
|
| 1218 |
+
[`REPRODUCIBILITY.md`](REPRODUCIBILITY.md) (reproduction),
|
| 1219 |
+
[`RESEARCH_NOTES.md`](RESEARCH_NOTES.md) (the format contradiction),
|
| 1220 |
+
[`LIMITATIONS.md`](LIMITATIONS.md) (the honest counterweight).
|