Publish SatlasPretrain reproduction
Browse files- .gitattributes +21 -9
- README.md +181 -0
- conf/config.yaml +21 -0
- config.json +63 -0
- configuration.json +12 -0
- model/satlaspretrain.py +118 -0
- scripts/fake_data.py +49 -0
- scripts/inference.py +52 -0
- scripts/result.py +118 -0
- scripts/train.py +107 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,47 @@
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bin.* filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 11 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 13 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 17 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 20 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 21 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 22 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 24 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.db* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.ark* filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*.gguf* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.ggml filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.llamafile* filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.pt2 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 46 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 47 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: PyTorch
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Remote Sensing
|
| 10 |
+
- Multi-Task Image Understanding
|
| 11 |
+
- SatlasPretrain
|
| 12 |
+
- arxiv:2211.15660
|
| 13 |
+
tasks: []
|
| 14 |
+
datasets:
|
| 15 |
+
- allenai/satlas-pretrain
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">SatlasPretrain</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Introduction
|
| 24 |
+
|
| 25 |
+
SatlasPretrain is a large-scale remote sensing pre-training approach for learning from multi-temporal imagery and seven label types. Its unified SatlasNet model supports semantic segmentation, regression, point detection, polygon instances, polylines, object properties, and image classification for Earth observation and downstream remote sensing applications.
|
| 26 |
+
|
| 27 |
+
Paper: SatlasPretrain: A Large-Scale Dataset for Remote Sensing Image Understanding
|
| 28 |
+
|
| 29 |
+
https://arxiv.org/abs/2211.15660
|
| 30 |
+
|
| 31 |
+
# Model Description
|
| 32 |
+
|
| 33 |
+
SatlasPretrain was proposed by the Allen Institute for AI and trained on Sentinel-2 and NAIP imagery with 302 million labels in 137 categories and seven label types.
|
| 34 |
+
The model is suitable for multi-temporal remote sensing segmentation, regression, detection, property prediction, and classification tasks.
|
| 35 |
+
|
| 36 |
+
# Use Cases
|
| 37 |
+
|
| 38 |
+
| Scenario | Description |
|
| 39 |
+
| :---: | :--- |
|
| 40 |
+
| Remote sensing multi-task training | Train SatlasPretrain with multi-temporal imagery and segmentation, regression, detection, property, and classification labels. |
|
| 41 |
+
| Local quick validation | Use synthetic data to validate loading, multi-task training, inference, evaluation, and qualitative visualization. |
|
| 42 |
+
| Hugging Face / OneCode execution | Download the standalone model package, install dependencies, and run the scripts directly. |
|
| 43 |
+
| Multi-GPU training | Launch distributed multi-process training with `torchrun`. |
|
| 44 |
+
|
| 45 |
+
# Usage Guide
|
| 46 |
+
|
| 47 |
+
## 1. OneCode Usage
|
| 48 |
+
|
| 49 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 50 |
+
|
| 51 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 52 |
+
|
| 53 |
+
## 2. Manual Installation and Usage
|
| 54 |
+
|
| 55 |
+
**Hardware Requirements**
|
| 56 |
+
|
| 57 |
+
- A GPU or DCU is recommended.
|
| 58 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 59 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching the cluster, is recommended.
|
| 60 |
+
|
| 61 |
+
### Download the Model Package
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
hf download OneScience-Group/SatlasPretrain --local-dir ./SatlasPretrain
|
| 65 |
+
cd SatlasPretrain
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Install the Runtime Environment
|
| 69 |
+
|
| 70 |
+
**DCU Environment**
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Please activate DTK and CONDA first
|
| 74 |
+
conda create -n onescience311 python=3.11 -y
|
| 75 |
+
conda activate onescience311
|
| 76 |
+
# uv installation is supported
|
| 77 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**GPU Environment**
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
# Please activate CONDA first
|
| 84 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 85 |
+
conda activate onescience311
|
| 86 |
+
# uv installation is supported
|
| 87 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Training Data Introduction
|
| 91 |
+
|
| 92 |
+
The original SatlasPretrain dataset is published by the Allen Institute for AI and organized as Web-Mercator tiles. Sentinel-2, Sentinel-1, and NAIP imagery and bands are stored as PNG files. Point, polygon, polyline, property, and classification labels are stored in `vector.json`; semantic segmentation and regression labels are grayscale PNG files; split and image-time metadata are JSON files.
|
| 93 |
+
|
| 94 |
+
Official data resources:
|
| 95 |
+
|
| 96 |
+
- https://github.com/allenai/satlas/blob/main/SatlasPretrain.md
|
| 97 |
+
- https://huggingface.co/datasets/allenai/satlas-pretrain
|
| 98 |
+
|
| 99 |
+
The original dataset is not included in this model repository. The provided scripts use synthetic NPZ data to validate the full training, inference, evaluation, and visualization pipeline. This NPZ protocol is only a compact pipeline check and is not the original SatlasPretrain format.
|
| 100 |
+
|
| 101 |
+
Generate synthetic data:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python scripts/fake_data.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
The generated `data/fake_satlaspretrain.npz` contains four samples with `images` in `[B, T, C, H, W]` layout and labels for all seven task heads. Verify that `conf/config.yaml` points to this file before training.
|
| 108 |
+
|
| 109 |
+
### Training
|
| 110 |
+
|
| 111 |
+
Single GPU or CPU:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python scripts/train.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Multi-GPU:
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
Training outputs:
|
| 124 |
+
|
| 125 |
+
```text
|
| 126 |
+
data/checkpoints/best.pth
|
| 127 |
+
data/checkpoints/last.pth
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
`best.pth` stores the checkpoint with the lowest joint multi-task validation loss, while `last.pth` stores the final training epoch. Checkpoints generated from synthetic data validate the pipeline only and are not pretrained weights reproducing the paper results.
|
| 131 |
+
|
| 132 |
+
### Training Weights
|
| 133 |
+
|
| 134 |
+
This repository does not include synthetic, trained, or official pretrained weights. Running the training script generates `best.pth` and `last.pth` under `data/checkpoints/`. For real downstream use, train with appropriately converted SatlasPretrain data or use official AllenAI foundation model weights under their applicable license.
|
| 135 |
+
|
| 136 |
+
### Inference
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
python scripts/inference.py
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Prediction outputs:
|
| 143 |
+
|
| 144 |
+
```text
|
| 145 |
+
result/output/segmentation.npy
|
| 146 |
+
result/output/regression.npy
|
| 147 |
+
result/output/point.npy
|
| 148 |
+
result/output/polygon.npy
|
| 149 |
+
result/output/polyline.npy
|
| 150 |
+
result/output/property.npy
|
| 151 |
+
result/output/classification.npy
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
These files store class probabilities, dense regression values, confidence maps, and category probabilities for the seven compact task heads.
|
| 155 |
+
|
| 156 |
+
### Evaluation and Visualization
|
| 157 |
+
|
| 158 |
+
```bash
|
| 159 |
+
python scripts/result.py
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
Evaluation outputs:
|
| 163 |
+
|
| 164 |
+
```text
|
| 165 |
+
result/output/satlaspretrain_metrics.json
|
| 166 |
+
result/output/qualitative_comparison.ppm
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
The metrics follow the task families reported in the paper: segmentation F1, regression MAE, point mAP, polygon mAP, polyline GEO, property accuracy, and classification accuracy. The qualitative comparison displays input imagery and paired ground-truth/prediction panels. Metrics and figures produced from synthetic data are pipeline checks, not paper results.
|
| 170 |
+
|
| 171 |
+
# Official OneScience Resources
|
| 172 |
+
|
| 173 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 174 |
+
| --- | --- | --- |
|
| 175 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 176 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 177 |
+
|
| 178 |
+
# Citation and License
|
| 179 |
+
|
| 180 |
+
- This repository is a reproduction of the original SatlasPretrain paper.
|
| 181 |
+
- The reproduction code is released under Apache License 2.0. The original SatlasPretrain data and official model weights remain subject to their respective source licenses.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
input_channels: 3
|
| 3 |
+
image_size: 64
|
| 4 |
+
time_steps: 4
|
| 5 |
+
embed_dim: 24
|
| 6 |
+
window_size: 4
|
| 7 |
+
segmentation_classes: 4
|
| 8 |
+
property_classes: 3
|
| 9 |
+
classification_classes: 3
|
| 10 |
+
data:
|
| 11 |
+
path: ./data/fake_satlaspretrain.npz
|
| 12 |
+
training:
|
| 13 |
+
epochs: 2
|
| 14 |
+
batch_size: 2
|
| 15 |
+
learning_rate: 0.0001
|
| 16 |
+
save_dir: ./data/checkpoints
|
| 17 |
+
checkpoint: ./data/checkpoints/best.pth
|
| 18 |
+
runtime:
|
| 19 |
+
device: auto
|
| 20 |
+
seed: 42
|
| 21 |
+
output_dir: ./result/output
|
config.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "SatlasPretrain",
|
| 3 |
+
"model_type": "satlaspretrain",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"SatlasPretrain"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "earth-science",
|
| 9 |
+
"task": "remote-sensing-image-understanding",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/satlaspretrain.py",
|
| 12 |
+
"scope": "compact multi-temporal SatlasNet reproduction"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "multi-temporal multi-task SatlasNet",
|
| 16 |
+
"input_format": "BTCHW",
|
| 17 |
+
"input_channels": 3,
|
| 18 |
+
"image_size": 64,
|
| 19 |
+
"time_steps": 4,
|
| 20 |
+
"embed_dim": 24,
|
| 21 |
+
"window_size": 4,
|
| 22 |
+
"feature_scales": 4,
|
| 23 |
+
"temporal_aggregation": "max_pooling",
|
| 24 |
+
"task_heads": [
|
| 25 |
+
"segmentation",
|
| 26 |
+
"regression",
|
| 27 |
+
"point",
|
| 28 |
+
"polygon",
|
| 29 |
+
"polyline",
|
| 30 |
+
"property",
|
| 31 |
+
"classification"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"data": {
|
| 35 |
+
"dataset": "SatlasPretrain",
|
| 36 |
+
"official_repo": "allenai/satlas-pretrain",
|
| 37 |
+
"image_modalities": [
|
| 38 |
+
"Sentinel-2",
|
| 39 |
+
"NAIP"
|
| 40 |
+
],
|
| 41 |
+
"protocol": "synthetic-satlaspretrain-multitask",
|
| 42 |
+
"synthetic_input_shape": [
|
| 43 |
+
4,
|
| 44 |
+
4,
|
| 45 |
+
3,
|
| 46 |
+
64,
|
| 47 |
+
64
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"metrics": [
|
| 51 |
+
"segmentation_f1",
|
| 52 |
+
"regression_mae",
|
| 53 |
+
"point_map",
|
| 54 |
+
"polygon_map",
|
| 55 |
+
"polyline_geo",
|
| 56 |
+
"property_accuracy",
|
| 57 |
+
"classification_accuracy"
|
| 58 |
+
],
|
| 59 |
+
"configuration_sources": [
|
| 60 |
+
"conf/config.yaml",
|
| 61 |
+
"model/satlaspretrain.py"
|
| 62 |
+
]
|
| 63 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "remote_sensing_image_understanding",
|
| 4 |
+
"model": "SatlasPretrain",
|
| 5 |
+
"input_format": "BTCHW",
|
| 6 |
+
"protocol": "synthetic_satlaspretrain_multitask",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"train": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py",
|
| 11 |
+
"visualization": "scripts/result.py"
|
| 12 |
+
}
|
model/satlaspretrain.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compact SatlasNet reproduction for multi-temporal remote sensing data."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
from torch.nn import functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class WindowBlock(nn.Module):
|
| 9 |
+
"""Window self-attention block used in the compact hierarchical backbone."""
|
| 10 |
+
|
| 11 |
+
def __init__(self, channels, window_size=4, heads=4):
|
| 12 |
+
super().__init__()
|
| 13 |
+
self.window_size = window_size
|
| 14 |
+
self.norm1 = nn.LayerNorm(channels)
|
| 15 |
+
self.attention = nn.MultiheadAttention(channels, heads, batch_first=True)
|
| 16 |
+
self.norm2 = nn.LayerNorm(channels)
|
| 17 |
+
self.mlp = nn.Sequential(
|
| 18 |
+
nn.Linear(channels, channels * 4), nn.GELU(), nn.Linear(channels * 4, channels)
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def forward(self, x):
|
| 22 |
+
batch, channels, height, width = x.shape
|
| 23 |
+
size = self.window_size
|
| 24 |
+
pad_h = (-height) % size
|
| 25 |
+
pad_w = (-width) % size
|
| 26 |
+
x = F.pad(x, (0, pad_w, 0, pad_h))
|
| 27 |
+
padded_h, padded_w = x.shape[-2:]
|
| 28 |
+
windows = x.permute(0, 2, 3, 1).reshape(
|
| 29 |
+
batch, padded_h // size, size, padded_w // size, size, channels
|
| 30 |
+
)
|
| 31 |
+
windows = windows.permute(0, 1, 3, 2, 4, 5).reshape(-1, size * size, channels)
|
| 32 |
+
normalized = self.norm1(windows)
|
| 33 |
+
attended = self.attention(normalized, normalized, normalized, need_weights=False)[0]
|
| 34 |
+
windows = windows + attended
|
| 35 |
+
windows = windows + self.mlp(self.norm2(windows))
|
| 36 |
+
x = windows.reshape(batch, padded_h // size, padded_w // size, size, size, channels)
|
| 37 |
+
x = x.permute(0, 1, 3, 2, 4, 5).reshape(batch, padded_h, padded_w, channels)
|
| 38 |
+
return x[:, :height, :width].permute(0, 3, 1, 2).contiguous()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SatlasPretrain(nn.Module):
|
| 42 |
+
"""SatlasNet with hierarchical features, temporal max pooling, and seven heads."""
|
| 43 |
+
|
| 44 |
+
dense_tasks = ("segmentation", "regression", "point", "polygon", "polyline")
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
input_channels=3,
|
| 49 |
+
embed_dim=24,
|
| 50 |
+
window_size=4,
|
| 51 |
+
segmentation_classes=4,
|
| 52 |
+
property_classes=3,
|
| 53 |
+
classification_classes=3,
|
| 54 |
+
):
|
| 55 |
+
super().__init__()
|
| 56 |
+
dimensions = [embed_dim * (2**index) for index in range(4)]
|
| 57 |
+
self.patch_embed = nn.Conv2d(input_channels, dimensions[0], 4, stride=4)
|
| 58 |
+
self.stages = nn.ModuleList()
|
| 59 |
+
for index, channels in enumerate(dimensions):
|
| 60 |
+
downsample = nn.Identity() if index == 0 else nn.Conv2d(dimensions[index - 1], channels, 2, 2)
|
| 61 |
+
heads = max(1, channels // embed_dim)
|
| 62 |
+
self.stages.append(nn.Sequential(downsample, WindowBlock(channels, window_size, heads)))
|
| 63 |
+
self.lateral = nn.ModuleList(nn.Conv2d(channels, embed_dim, 1) for channels in dimensions)
|
| 64 |
+
dense_channels = {
|
| 65 |
+
"segmentation": segmentation_classes,
|
| 66 |
+
"regression": 1,
|
| 67 |
+
"point": 1,
|
| 68 |
+
"polygon": 1,
|
| 69 |
+
"polyline": 1,
|
| 70 |
+
}
|
| 71 |
+
self.dense_heads = nn.ModuleDict(
|
| 72 |
+
{name: nn.Conv2d(embed_dim, channels, 1) for name, channels in dense_channels.items()}
|
| 73 |
+
)
|
| 74 |
+
self.property_head = nn.Linear(dimensions[-1], property_classes)
|
| 75 |
+
self.classification_head = nn.Linear(dimensions[-1], classification_classes)
|
| 76 |
+
|
| 77 |
+
def encode_image(self, image):
|
| 78 |
+
features = []
|
| 79 |
+
x = self.patch_embed(image)
|
| 80 |
+
for stage in self.stages:
|
| 81 |
+
x = stage(x)
|
| 82 |
+
features.append(x)
|
| 83 |
+
return features
|
| 84 |
+
|
| 85 |
+
def forward(self, images):
|
| 86 |
+
if images.ndim != 5:
|
| 87 |
+
raise ValueError("images must have shape [batch, time, channels, height, width]")
|
| 88 |
+
batch, time, channels, height, width = images.shape
|
| 89 |
+
per_scale = self.encode_image(images.reshape(batch * time, channels, height, width))
|
| 90 |
+
pooled = [feature.reshape(batch, time, *feature.shape[1:]).amax(dim=1) for feature in per_scale]
|
| 91 |
+
pyramid = self.lateral[-1](pooled[-1])
|
| 92 |
+
for index in range(len(pooled) - 2, -1, -1):
|
| 93 |
+
pyramid = F.interpolate(pyramid, size=pooled[index].shape[-2:], mode="nearest")
|
| 94 |
+
pyramid = pyramid + self.lateral[index](pooled[index])
|
| 95 |
+
dense = {
|
| 96 |
+
name: F.interpolate(head(pyramid), size=(height, width), mode="bilinear", align_corners=False)
|
| 97 |
+
for name, head in self.dense_heads.items()
|
| 98 |
+
}
|
| 99 |
+
global_feature = pooled[-1].mean(dim=(-2, -1))
|
| 100 |
+
dense["property"] = self.property_head(global_feature)
|
| 101 |
+
dense["classification"] = self.classification_head(global_feature)
|
| 102 |
+
return dense
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def multitask_loss(outputs, targets):
|
| 106 |
+
losses = {
|
| 107 |
+
"segmentation": F.cross_entropy(outputs["segmentation"], targets["segmentation"].long()),
|
| 108 |
+
"regression": F.l1_loss(outputs["regression"], targets["regression"]),
|
| 109 |
+
"point": F.binary_cross_entropy_with_logits(outputs["point"], targets["point"]),
|
| 110 |
+
"polygon": F.binary_cross_entropy_with_logits(outputs["polygon"], targets["polygon"]),
|
| 111 |
+
"polyline": F.binary_cross_entropy_with_logits(outputs["polyline"], targets["polyline"]),
|
| 112 |
+
"property": F.cross_entropy(outputs["property"], targets["property"].long()),
|
| 113 |
+
"classification": F.cross_entropy(outputs["classification"], targets["classification"].long()),
|
| 114 |
+
}
|
| 115 |
+
return sum(losses.values()), losses
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
__all__ = ["SatlasPretrain", "multitask_loss"]
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
parser = argparse.ArgumentParser(description="Generate compact SatlasPretrain virtual data")
|
| 9 |
+
parser.add_argument("--output", default="data/fake_satlaspretrain.npz")
|
| 10 |
+
parser.add_argument("--samples", type=int, default=4)
|
| 11 |
+
parser.add_argument("--time-steps", type=int, default=4)
|
| 12 |
+
parser.add_argument("--image-size", type=int, default=64)
|
| 13 |
+
args = parser.parse_args()
|
| 14 |
+
rng = np.random.default_rng(42)
|
| 15 |
+
shape = (args.samples, args.image_size, args.image_size)
|
| 16 |
+
images = rng.random((args.samples, args.time_steps, 3, args.image_size, args.image_size), dtype=np.float32)
|
| 17 |
+
yy, xx = np.mgrid[: args.image_size, : args.image_size]
|
| 18 |
+
segmentation = np.empty(shape, dtype=np.int64)
|
| 19 |
+
point = np.zeros((args.samples, 1, args.image_size, args.image_size), dtype=np.float32)
|
| 20 |
+
polygon = np.zeros_like(point)
|
| 21 |
+
polyline = np.zeros_like(point)
|
| 22 |
+
for index in range(args.samples):
|
| 23 |
+
segmentation[index] = ((xx >= args.image_size // 2).astype(int) + 2 * (yy >= args.image_size // 2)).astype(np.int64)
|
| 24 |
+
center = args.image_size // 4 + index
|
| 25 |
+
point[index, 0, center - 1:center + 2, center - 1:center + 2] = 1
|
| 26 |
+
polygon[index, 0, args.image_size // 3:2 * args.image_size // 3, args.image_size // 3:2 * args.image_size // 3] = 1
|
| 27 |
+
polyline[index, 0, np.arange(args.image_size), (np.arange(args.image_size) + index) % args.image_size] = 1
|
| 28 |
+
regression = images[:, -1, :1] * 0.75 + images[:, 0, 1:2] * 0.25
|
| 29 |
+
properties = np.arange(args.samples, dtype=np.int64) % 3
|
| 30 |
+
classification = (images.mean(axis=(1, 2, 3, 4)) * 3).astype(np.int64).clip(0, 2)
|
| 31 |
+
output = Path(args.output)
|
| 32 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
np.savez_compressed(
|
| 34 |
+
output,
|
| 35 |
+
images=images,
|
| 36 |
+
segmentation=segmentation,
|
| 37 |
+
regression=regression.astype(np.float32),
|
| 38 |
+
point=point,
|
| 39 |
+
polygon=polygon,
|
| 40 |
+
polyline=polyline,
|
| 41 |
+
property=properties,
|
| 42 |
+
classification=classification,
|
| 43 |
+
)
|
| 44 |
+
print(f"images: {images.shape}")
|
| 45 |
+
print(f"saved: {output}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 10 |
+
from model.satlaspretrain import SatlasPretrain
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
parser = argparse.ArgumentParser(description="Run compact SatlasNet inference")
|
| 15 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 16 |
+
parser.add_argument("--data")
|
| 17 |
+
parser.add_argument("--checkpoint")
|
| 18 |
+
parser.add_argument("--output")
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
with open(args.config, encoding="utf-8") as handle:
|
| 21 |
+
config = yaml.safe_load(handle)
|
| 22 |
+
model_config = config["model"]
|
| 23 |
+
model = SatlasPretrain(**{key: model_config[key] for key in (
|
| 24 |
+
"input_channels", "embed_dim", "window_size", "segmentation_classes",
|
| 25 |
+
"property_classes", "classification_classes"
|
| 26 |
+
)})
|
| 27 |
+
checkpoint = args.checkpoint or config["training"]["checkpoint"]
|
| 28 |
+
model.load_state_dict(torch.load(checkpoint, map_location="cpu", weights_only=True)["model"])
|
| 29 |
+
data = np.load(args.data or config["data"]["path"])
|
| 30 |
+
model.eval()
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
outputs = model(torch.from_numpy(data["images"]))
|
| 33 |
+
predictions = {}
|
| 34 |
+
for name, value in outputs.items():
|
| 35 |
+
if name == "segmentation":
|
| 36 |
+
predictions[name] = value.softmax(dim=1).numpy().astype(np.float32)
|
| 37 |
+
elif name in ("property", "classification"):
|
| 38 |
+
predictions[name] = value.softmax(dim=1).numpy().astype(np.float32)
|
| 39 |
+
elif name == "regression":
|
| 40 |
+
predictions[name] = value.numpy().astype(np.float32)
|
| 41 |
+
else:
|
| 42 |
+
predictions[name] = value.sigmoid().numpy().astype(np.float32)
|
| 43 |
+
output_dir = Path(args.output or config["runtime"]["output_dir"])
|
| 44 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
for task, prediction in predictions.items():
|
| 46 |
+
output = output_dir / f"{task}.npy"
|
| 47 |
+
np.save(output, prediction)
|
| 48 |
+
print(f"{task}: {prediction.shape} -> {output}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def binary_f1(prediction, target, threshold=0.5):
|
| 9 |
+
prediction = prediction >= threshold
|
| 10 |
+
target = target >= 0.5
|
| 11 |
+
true_positive = np.logical_and(prediction, target).sum()
|
| 12 |
+
return float(2 * true_positive / (prediction.sum() + target.sum() + 1e-8))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def best_f1(scores, target):
|
| 16 |
+
return max(binary_f1(scores, target, threshold) for threshold in np.arange(0.05, 1.0, 0.05))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def average_precision(scores, target):
|
| 20 |
+
order = np.argsort(-scores.reshape(-1))
|
| 21 |
+
labels = target.reshape(-1)[order] >= 0.5
|
| 22 |
+
positives = labels.sum()
|
| 23 |
+
if positives == 0:
|
| 24 |
+
return 0.0
|
| 25 |
+
precision = np.cumsum(labels) / np.arange(1, labels.size + 1)
|
| 26 |
+
return float((precision * labels).sum() / positives)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def dilate(mask, radius=2):
|
| 30 |
+
padded = np.pad(mask, ((0, 0), (0, 0), (radius, radius), (radius, radius)))
|
| 31 |
+
neighborhoods = []
|
| 32 |
+
height, width = mask.shape[-2:]
|
| 33 |
+
for row_shift in range(2 * radius + 1):
|
| 34 |
+
for col_shift in range(2 * radius + 1):
|
| 35 |
+
neighborhoods.append(padded[..., row_shift:row_shift + height, col_shift:col_shift + width])
|
| 36 |
+
return np.logical_or.reduce(neighborhoods)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def geo_accuracy(scores, target):
|
| 40 |
+
prediction = scores >= 0.5
|
| 41 |
+
target = target >= 0.5
|
| 42 |
+
precision = np.logical_and(prediction, dilate(target)).sum() / (prediction.sum() + 1e-8)
|
| 43 |
+
recall = np.logical_and(target, dilate(prediction)).sum() / (target.sum() + 1e-8)
|
| 44 |
+
return float(2 * precision * recall / (precision + recall + 1e-8))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def colorize_segmentation(segmentation):
|
| 48 |
+
palette = np.asarray([[0, 0, 0], [32, 180, 96], [40, 120, 220], [230, 180, 40]], dtype=np.uint8)
|
| 49 |
+
return palette[segmentation % len(palette)]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def colorize_scalar(values):
|
| 53 |
+
values = np.clip(values, 0, 1)
|
| 54 |
+
return np.stack((values * 255, (1 - np.abs(values - 0.5) * 2) * 255, (1 - values) * 255), axis=-1).astype(np.uint8)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def overlay(image, mask, color):
|
| 58 |
+
output = image.copy()
|
| 59 |
+
selected = mask >= 0.5
|
| 60 |
+
output[selected] = (0.35 * output[selected] + 0.65 * np.asarray(color)).astype(np.uint8)
|
| 61 |
+
return output
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def write_qualitative_comparison(path, target, prediction):
|
| 65 |
+
image = np.clip(target["images"][0, -1].transpose(1, 2, 0) * 255, 0, 255).astype(np.uint8)
|
| 66 |
+
panels = [
|
| 67 |
+
image,
|
| 68 |
+
colorize_segmentation(target["segmentation"][0]),
|
| 69 |
+
colorize_segmentation(prediction["segmentation"][0].argmax(axis=0)),
|
| 70 |
+
colorize_scalar(target["regression"][0, 0]),
|
| 71 |
+
colorize_scalar(prediction["regression"][0, 0]),
|
| 72 |
+
overlay(image, target["point"][0, 0], [255, 230, 40]),
|
| 73 |
+
overlay(image, prediction["point"][0, 0], [255, 230, 40]),
|
| 74 |
+
overlay(image, target["polygon"][0, 0], [255, 70, 190]),
|
| 75 |
+
overlay(image, prediction["polygon"][0, 0], [255, 70, 190]),
|
| 76 |
+
overlay(image, target["polyline"][0, 0], [40, 240, 255]),
|
| 77 |
+
overlay(image, prediction["polyline"][0, 0], [40, 240, 255]),
|
| 78 |
+
]
|
| 79 |
+
separator = np.full((image.shape[0], 2, 3), 255, dtype=np.uint8)
|
| 80 |
+
comparison = panels[0]
|
| 81 |
+
for panel in panels[1:]:
|
| 82 |
+
comparison = np.concatenate((comparison, separator, panel), axis=1)
|
| 83 |
+
with open(path, "wb") as handle:
|
| 84 |
+
handle.write(f"P6\n{comparison.shape[1]} {comparison.shape[0]}\n255\n".encode("ascii"))
|
| 85 |
+
handle.write(comparison.tobytes())
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def main():
|
| 89 |
+
parser = argparse.ArgumentParser(description="Evaluate and visualize SatlasNet predictions")
|
| 90 |
+
parser.add_argument("--prediction-dir", default="result/output")
|
| 91 |
+
parser.add_argument("--target", default="data/fake_satlaspretrain.npz")
|
| 92 |
+
args = parser.parse_args()
|
| 93 |
+
prediction_dir = Path(args.prediction_dir)
|
| 94 |
+
tasks = ("segmentation", "regression", "point", "polygon", "polyline", "property", "classification")
|
| 95 |
+
prediction = {task: np.load(prediction_dir / f"{task}.npy") for task in tasks}
|
| 96 |
+
target = np.load(args.target)
|
| 97 |
+
metrics = {
|
| 98 |
+
"Seg_F1": float(np.mean([best_f1(prediction["segmentation"][:, label], target["segmentation"] == label) for label in range(4)])),
|
| 99 |
+
"Reg_MAE": float(np.abs(prediction["regression"] - target["regression"]).mean()),
|
| 100 |
+
"Pt_mAP": average_precision(prediction["point"], target["point"]),
|
| 101 |
+
"Pgon_mAP": average_precision(prediction["polygon"], target["polygon"]),
|
| 102 |
+
"Pline_GEO": geo_accuracy(prediction["polyline"], target["polyline"]),
|
| 103 |
+
"Prop_Accuracy": float((prediction["property"].argmax(1) == target["property"]).mean()),
|
| 104 |
+
"Cls_Accuracy": float((prediction["classification"].argmax(1) == target["classification"]).mean()),
|
| 105 |
+
}
|
| 106 |
+
metrics_path = prediction_dir / "satlaspretrain_metrics.json"
|
| 107 |
+
with open(metrics_path, "w", encoding="utf-8") as handle:
|
| 108 |
+
json.dump({"image_mode": "low-resolution virtual data", "metrics": metrics}, handle, indent=2)
|
| 109 |
+
visualization_path = prediction_dir / "qualitative_comparison.ppm"
|
| 110 |
+
write_qualitative_comparison(visualization_path, target, prediction)
|
| 111 |
+
for key, value in metrics.items():
|
| 112 |
+
print(f"{key}={value:.6f}")
|
| 113 |
+
print(f"saved: {metrics_path}")
|
| 114 |
+
print(f"saved: {visualization_path}")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import yaml
|
| 9 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 10 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 11 |
+
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 13 |
+
from model.satlaspretrain import SatlasPretrain, multitask_loss
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class NpzDataset(Dataset):
|
| 17 |
+
def __init__(self, path):
|
| 18 |
+
data = np.load(path)
|
| 19 |
+
self.data = {key: data[key] for key in data.files}
|
| 20 |
+
|
| 21 |
+
def __len__(self):
|
| 22 |
+
return len(self.data["images"])
|
| 23 |
+
|
| 24 |
+
def __getitem__(self, index):
|
| 25 |
+
return {key: torch.as_tensor(value[index]) for key, value in self.data.items()}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def load_config(path):
|
| 29 |
+
with open(path, encoding="utf-8") as handle:
|
| 30 |
+
return yaml.safe_load(handle)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def build_model(config):
|
| 34 |
+
return SatlasPretrain(**{key: config["model"][key] for key in (
|
| 35 |
+
"input_channels", "embed_dim", "window_size", "segmentation_classes",
|
| 36 |
+
"property_classes", "classification_classes"
|
| 37 |
+
)})
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
parser = argparse.ArgumentParser(description="Train compact SatlasNet")
|
| 42 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 43 |
+
parser.add_argument("--data")
|
| 44 |
+
parser.add_argument("--epochs", type=int)
|
| 45 |
+
args = parser.parse_args()
|
| 46 |
+
config = load_config(args.config)
|
| 47 |
+
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
| 48 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 49 |
+
if distributed:
|
| 50 |
+
torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 51 |
+
requested = config["runtime"]["device"]
|
| 52 |
+
device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() and requested != "cpu" else "cpu")
|
| 53 |
+
torch.manual_seed(config["runtime"]["seed"])
|
| 54 |
+
dataset = NpzDataset(args.data or config["data"]["path"])
|
| 55 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 56 |
+
loader = DataLoader(dataset, batch_size=config["training"]["batch_size"], shuffle=sampler is None, sampler=sampler)
|
| 57 |
+
model = build_model(config).to(device)
|
| 58 |
+
if distributed:
|
| 59 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 60 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=config["training"]["learning_rate"])
|
| 61 |
+
train_history = []
|
| 62 |
+
val_history = []
|
| 63 |
+
best_validation = float("inf")
|
| 64 |
+
save_dir = Path(config["training"]["save_dir"])
|
| 65 |
+
epochs = args.epochs or config["training"]["epochs"]
|
| 66 |
+
for epoch in range(epochs):
|
| 67 |
+
if sampler is not None:
|
| 68 |
+
sampler.set_epoch(epoch)
|
| 69 |
+
model.train()
|
| 70 |
+
epoch_losses = []
|
| 71 |
+
for batch in loader:
|
| 72 |
+
batch = {key: value.to(device) for key, value in batch.items()}
|
| 73 |
+
total, _ = multitask_loss(model(batch["images"]), batch)
|
| 74 |
+
optimizer.zero_grad()
|
| 75 |
+
total.backward()
|
| 76 |
+
optimizer.step()
|
| 77 |
+
epoch_losses.append(total.item())
|
| 78 |
+
train_history.append(float(np.mean(epoch_losses)))
|
| 79 |
+
model.eval()
|
| 80 |
+
with torch.no_grad():
|
| 81 |
+
sample = {key: value[:1].to(device) for key, value in next(iter(loader)).items()}
|
| 82 |
+
validation, _ = multitask_loss(model(sample["images"]), sample)
|
| 83 |
+
val_history.append(validation.item())
|
| 84 |
+
if local_rank == 0:
|
| 85 |
+
print(f"epoch={epoch + 1} train_loss={train_history[-1]:.6f} val_loss={val_history[-1]:.6f}")
|
| 86 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 87 |
+
state = model.module.state_dict() if distributed else model.state_dict()
|
| 88 |
+
checkpoint = {
|
| 89 |
+
"model": state,
|
| 90 |
+
"epoch": epoch + 1,
|
| 91 |
+
"train_loss": train_history[-1],
|
| 92 |
+
"validation_loss": val_history[-1],
|
| 93 |
+
"format": "satlaspretrain-compact-v1",
|
| 94 |
+
}
|
| 95 |
+
torch.save(checkpoint, save_dir / "last.pth")
|
| 96 |
+
if val_history[-1] < best_validation:
|
| 97 |
+
best_validation = val_history[-1]
|
| 98 |
+
torch.save(checkpoint, save_dir / "best.pth")
|
| 99 |
+
if local_rank == 0:
|
| 100 |
+
print(f"saved: {save_dir / 'last.pth'}")
|
| 101 |
+
print(f"best: {save_dir / 'best.pth'}")
|
| 102 |
+
if distributed:
|
| 103 |
+
torch.distributed.destroy_process_group()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|