zhangrenchao commited on
Commit
cf7a05c
·
verified ·
1 Parent(s): 09d9c43

Publish SatlasPretrain reproduction

Browse files
.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