PSRGAN v1.0
Browse files- .gitattributes +3 -0
- LICENSE +30 -0
- README.md +187 -0
- config.json +9 -0
- eval/metrics.json +247 -0
- images/psrgan_city.png +3 -0
- images/psrgan_uncertainty.png +3 -0
- model.safetensors +3 -0
- notebooks/PSRGAN_dataset.ipynb +1334 -0
- notebooks/PSRGAN_training.ipynb +0 -0
- psrgan.py +216 -0
- psrgan_grid.jpg +3 -0
- psrgan_x4_psnr.safetensors +3 -0
- psrgan_x4_swag.safetensors +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* 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
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
+
images/psrgan_city.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
images/psrgan_uncertainty.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
psrgan_grid.jpg filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026, Mark-Daniel Leupold
|
| 4 |
+
Copyright (c) 2021, Xintao Wang (Real-ESRGAN, on whose weights and RRDBNet architecture PSRGAN is based)
|
| 5 |
+
All rights reserved.
|
| 6 |
+
|
| 7 |
+
Redistribution and use in source and binary forms, with or without
|
| 8 |
+
modification, are permitted provided that the following conditions are met:
|
| 9 |
+
|
| 10 |
+
1. Redistributions of source code must retain the above copyright notice, this
|
| 11 |
+
list of conditions and the following disclaimer.
|
| 12 |
+
|
| 13 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 14 |
+
this list of conditions and the following disclaimer in the documentation
|
| 15 |
+
and/or other materials provided with the distribution.
|
| 16 |
+
|
| 17 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 18 |
+
contributors may be used to endorse or promote products derived from
|
| 19 |
+
this software without specific prior written permission.
|
| 20 |
+
|
| 21 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 22 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 23 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 24 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 25 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 26 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 27 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 28 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 29 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 30 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: bsd-3-clause
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
pipeline_tag: image-to-image
|
| 5 |
+
language:
|
| 6 |
+
- en
|
| 7 |
+
tags:
|
| 8 |
+
- super-resolution
|
| 9 |
+
- sentinel-2
|
| 10 |
+
- remote-sensing
|
| 11 |
+
- earth-observation
|
| 12 |
+
- uncertainty
|
| 13 |
+
- bayesian-deep-learning
|
| 14 |
+
- swag
|
| 15 |
+
- esrgan
|
| 16 |
+
- gan
|
| 17 |
+
datasets:
|
| 18 |
+
- marklpd/PSRGAN-Sentinel-2
|
| 19 |
+
base_model: xinntao/Real-ESRGAN
|
| 20 |
+
metrics:
|
| 21 |
+
- psnr
|
| 22 |
+
- ssim
|
| 23 |
+
- lpips
|
| 24 |
+
model-index:
|
| 25 |
+
- name: PSRGAN
|
| 26 |
+
results:
|
| 27 |
+
- task:
|
| 28 |
+
type: image-to-image
|
| 29 |
+
name: 4x super-resolution
|
| 30 |
+
dataset:
|
| 31 |
+
type: marklpd/PSRGAN-Sentinel-2
|
| 32 |
+
name: PSRGAN Sentinel-2 Super-Resolution (test split)
|
| 33 |
+
split: test
|
| 34 |
+
metrics:
|
| 35 |
+
- type: psnr
|
| 36 |
+
value: 21.28
|
| 37 |
+
name: PSNR (dB)
|
| 38 |
+
- type: ssim
|
| 39 |
+
value: 0.5694
|
| 40 |
+
name: SSIM
|
| 41 |
+
- type: lpips
|
| 42 |
+
value: 0.1879
|
| 43 |
+
name: LPIPS (AlexNet)
|
| 44 |
+
---
|
| 45 |
+

|
| 46 |
+
|
| 47 |
+
*Test patches from regions the model never saw during training. Error and uncertainty share one color scale per row.*
|
| 48 |
+
|
| 49 |
+
`PSRGAN` is a probabilistic 4× super-resolution model for Sentinel-2 RGB imagery. Next to the sharpened image it predicts a per-pixel uncertainty map, so you can see where the model is guessing. It is a Bayesian Real-ESRGAN with 16.7 million parameters, trained on the Alpine region with [PSRGAN Sentinel-2](https://huggingface.co/datasets/marklpd/PSRGAN-Sentinel-2).
|
| 50 |
+
|
| 51 |
+
# Key Features
|
| 52 |
+
|
| 53 |
+
1. Every output comes with an uncertainty map that combines aleatoric uncertainty (missing information in the input) and epistemic uncertainty (uncertainty of the model itself, from SWAG weight samples).
|
| 54 |
+
2. The uncertainty tracks the error. Pixel-wise uncertainty and absolute error correlate with Pearson r = 0.59 (Spearman ρ = 0.70), and the 10% most uncertain pixels have 14.3× the error of the 10% most confident ones.
|
| 55 |
+
3. On 1,056 test patches from unseen regions, PSRGAN reaches an LPIPS of 0.188, against 0.348 for Real-ESRGAN x4plus and 0.599 for bicubic upsampling. It also beats Real-ESRGAN in PSNR (21.28 vs. 20.22 dB) and SSIM (0.569 vs. 0.475).
|
| 56 |
+
4. Two sets of weights. The final adversarial weights have the lowest LPIPS, the weights after the likelihood stage the highest PSNR (21.94 dB) and SSIM (0.598) of all four methods tested.
|
| 57 |
+
5. Trained on randomized degradations (Gaussian, motion or defocus blur, then bicubic, bilinear or area downsampling), so it learns super-resolution instead of inverting one fixed downscaling operation.
|
| 58 |
+
|
| 59 |
+
# How the uncertainty map is made
|
| 60 |
+
|
| 61 |
+
A 64×64 input does not contain the detail of a 256×256 image, so every super-resolution model has to guess. PSRGAN models each output pixel as a Gaussian instead of a single value, and the uncertainty map is its standard deviation σ.
|
| 62 |
+
|
| 63 |
+
**Aleatoric uncertainty** comes from the data. The last convolution of Real-ESRGAN is replaced by two Bayesian 3×3 heads: the μ-head outputs the image, the s-head the log-variance s = log σ² of every pixel. A Gaussian negative log-likelihood loss punishes large errors and confident errors alike, so σ_aleatoric = exp(s / 2) grows where the input is ambiguous.
|
| 64 |
+
|
| 65 |
+
**Epistemic uncertainty** comes from the model. During the last 8 adversarial epochs, SWAG stores 32 weight snapshots and fits a Gaussian over the weights. Sampling 16 weight sets from it gives 16 slightly different images, and their per-pixel spread is σ_epistemic. Both parts combine to σ_total = √(σ²_aleatoric + 10 · σ²_epistemic).
|
| 66 |
+
|
| 67 |
+
<center>
|
| 68 |
+
<img src="images/psrgan_uncertainty.png" width="900px">
|
| 69 |
+
<br><em>From prediction to uncertainty map, each map on its own color scale.</em>
|
| 70 |
+
</center>
|
| 71 |
+
|
| 72 |
+
In the patches shown here, forests, water and shaded slopes stay dark in both the error and the uncertainty map, while ridges, snow edges, roads, buildings and field borders light up in both.
|
| 73 |
+
|
| 74 |
+
# Usage
|
| 75 |
+
|
| 76 |
+
The model code is a single file, [`psrgan.py`](psrgan.py), that only needs `torch`, `huggingface_hub` and `safetensors`.
|
| 77 |
+
|
| 78 |
+
```shell
|
| 79 |
+
pip install torch huggingface_hub safetensors
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
import torch
|
| 84 |
+
from huggingface_hub import hf_hub_download
|
| 85 |
+
|
| 86 |
+
hf_hub_download("marklpd/PSRGAN", "psrgan.py", local_dir=".")
|
| 87 |
+
from psrgan import PSRGAN
|
| 88 |
+
|
| 89 |
+
model = PSRGAN.from_pretrained("marklpd/PSRGAN").eval()
|
| 90 |
+
model.load_swag(hf_hub_download("marklpd/PSRGAN", "psrgan_x4_swag.safetensors"))
|
| 91 |
+
|
| 92 |
+
lr = torch.rand(1, 3, 64, 64) # RGB (B04, B03, B02), stretched to [0, 1]
|
| 93 |
+
out = model.super_resolve(lr, swag_samples=16)
|
| 94 |
+
out["sr"] # (1, 3, 256, 256) super-resolved image
|
| 95 |
+
out["sigma_total"] # (1, 3, 256, 256) uncertainty map
|
| 96 |
+
out["sigma_aleatoric"], out["sigma_epistemic"]
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Without `load_swag`, `model.super_resolve(lr)` returns the image and the aleatoric map in one forward pass. `mc_samples=8` samples the Bayesian heads instead of SWAG, which is cheaper but only covers the last layer.
|
| 100 |
+
|
| 101 |
+
The model expects Sentinel-2 L2A RGB (B04, B03, B02) after a per-image 1 to 99% percentile stretch, the same preprocessing as in the dataset. `psrgan.stretch(reflectance)` applies it. The network is fully convolutional, so any input size works. Large scenes should be processed in overlapping tiles.
|
| 102 |
+
|
| 103 |
+
## Files
|
| 104 |
+
|
| 105 |
+
| file | content |
|
| 106 |
+
|---|---|
|
| 107 |
+
| `model.safetensors` | final weights after adversarial training (use this) |
|
| 108 |
+
| `psrgan_x4_psnr.safetensors` | weights after the likelihood stage, smoother and higher PSNR |
|
| 109 |
+
| `psrgan_x4_swag.safetensors` | SWAG mean and variance (32 snapshots) |
|
| 110 |
+
| `config.json`, `psrgan.py` | model hyperparameters and code |
|
| 111 |
+
| `eval/metrics.json` | every number on this page |
|
| 112 |
+
| `notebooks/` | dataset creation and training notebooks |
|
| 113 |
+
|
| 114 |
+
# Evaluation
|
| 115 |
+
|
| 116 |
+
All numbers are on the test split of [PSRGAN Sentinel-2](https://huggingface.co/datasets/marklpd/PSRGAN-Sentinel-2) (1,056 patches), whose tiles lie in 0.5° blocks that share no pixel with training or validation. PSNR, SSIM and LPIPS are averaged per image (± is the 95% confidence interval), VGG is the perceptual loss used in training and OT the patchwise Sinkhorn distance between pixel color distributions.
|
| 117 |
+
|
| 118 |
+
| method | PSNR (dB) ↑ | SSIM ↑ | LPIPS ↓ | VGG ↓ | OT ↓ |
|
| 119 |
+
|---|---:|---:|---:|---:|---:|
|
| 120 |
+
| Bicubic | 21.56 ± 0.17 | 0.5428 ± 0.0077 | 0.5987 ± 0.0050 | 8.135 | 0.00321 |
|
| 121 |
+
| Real-ESRGAN | 20.22 ± 0.15 | 0.4753 ± 0.0068 | 0.3482 ± 0.0043 | 8.640 | 0.00399 |
|
| 122 |
+
| PSRGAN (PSNR) | **21.94 ± 0.17** | **0.5981 ± 0.0063** | 0.2082 ± 0.0027 | **4.877** | 0.00267 |
|
| 123 |
+
| **PSRGAN** | 21.28 ± 0.17 | 0.5694 ± 0.0063 | **0.1879 ± 0.0026** | 5.859 | **0.00135** |
|
| 124 |
+
|
| 125 |
+
PSRGAN beats Real-ESRGAN in PSNR on 97% of test patches and in LPIPS on 100%. Compared with the likelihood stage, the adversarial stage gives up 0.66 dB PSNR and changes LPIPS from 0.208 to 0.188 (lower is better). Bicubic upsampling keeps a higher PSNR than the final model on 70% of patches, because PSNR rewards smooth averages over sharp but slightly misplaced detail. LPIPS, which follows perceived similarity, is lower for PSRGAN than for bicubic on 100% of patches.
|
| 126 |
+
|
| 127 |
+
On the 128 test patches from Herzogenaurach, Wolfsburg and München:
|
| 128 |
+
|
| 129 |
+
| method | PSNR (dB) ↑ | SSIM ↑ | LPIPS ↓ | VGG ↓ | OT ↓ |
|
| 130 |
+
|---|---:|---:|---:|---:|---:|
|
| 131 |
+
| Bicubic | 21.15 ± 0.25 | 0.5522 ± 0.0206 | 0.6825 ± 0.0142 | 9.868 | 0.00321 |
|
| 132 |
+
| Real-ESRGAN | 20.24 ± 0.23 | 0.5147 ± 0.0190 | 0.3988 ± 0.0150 | 9.620 | 0.00308 |
|
| 133 |
+
| PSRGAN (PSNR) | **21.30 ± 0.28** | **0.5960 ± 0.0199** | 0.2118 ± 0.0086 | **5.983** | 0.00310 |
|
| 134 |
+
| **PSRGAN** | 20.61 ± 0.26 | 0.5620 ± 0.0188 | **0.1938 ± 0.0087** | 7.124 | **0.00146** |
|
| 135 |
+
|
| 136 |
+
<center>
|
| 137 |
+
<img src="images/psrgan_city.png" width="900px">
|
| 138 |
+
<br><em>A test patch from Herzogenaurach.</em>
|
| 139 |
+
</center>
|
| 140 |
+
|
| 141 |
+
## Calibration
|
| 142 |
+
|
| 143 |
+
| | ECE (σ vs. RMSE) | error within 1σ | error within 2σ |
|
| 144 |
+
|---|---:|---:|---:|
|
| 145 |
+
| Aleatoric + SWAG | 0.0240 | 86.6% | 98.4% |
|
| 146 |
+
| Aleatoric + Bayesian heads | 0.0289 | 88.4% | 98.7% |
|
| 147 |
+
|
| 148 |
+
A perfectly calibrated Gaussian would cover 68.3% and 95.4%. Both variants cover more, and the predicted σ is larger than the observed RMSE in every calibration bin, so the uncertainty is conservative. The uncertainty-error correlation is stronger in flat areas (Pearson r = 0.59) than on edges (r = 0.37).
|
| 149 |
+
|
| 150 |
+
# Training
|
| 151 |
+
|
| 152 |
+
PSRGAN starts from the Real-ESRGAN x4plus generator (RRDBNet, 23 blocks) and its U-Net discriminator.
|
| 153 |
+
|
| 154 |
+
1. **Likelihood stage (768 epochs of 64 batches, batch size 16).** 0.6 · Gaussian NLL + 0.7 · (1 − SSIM) + 1.0 · VGG19 perceptual loss. All parameters loaded from Real-ESRGAN stay frozen for the first 168 epochs, so the new heads can settle first.
|
| 155 |
+
2. **Adversarial stage (64 epochs).** Relativistic GAN loss (weight 0.01) plus contextual bilateral loss (weight 0.2), Adam with a learning rate of 2·10⁻⁴ halved every 32 epochs. SWAG collects a weight snapshot every 16 batches during the last 8 epochs.
|
| 156 |
+
|
| 157 |
+
Augmentation (flips, 90° rotations) is applied to the training split only, and the test split is only used for the final evaluation. Training ran on one NVIDIA A100-SXM4-40GB in Google Colab.
|
| 158 |
+
|
| 159 |
+
# Limitations
|
| 160 |
+
|
| 161 |
+
- The model is trained on synthetically degraded Sentinel-2 images. It sharpens Sentinel-2 data at its own resolution and does not recover detail from a finer sensor. Details beyond what the input contains are estimates, not measurements.
|
| 162 |
+
- Its training data are 98.6% Alpine tiles (Switzerland, Austria, northern Italy, south-eastern France, southern Germany, Slovenia) from late September and early October 2025. Expect worse results on lowland farmland, other climates, other seasons, deserts and dense city centres.
|
| 163 |
+
- The uncertainty maps rank pixels by expected error, but the absolute σ is too large: 87% of pixel errors fall within 1σ instead of 68%. Treat σ as a conservative bound, or recalibrate it on your own data.
|
| 164 |
+
- The final model trades pixel accuracy for sharpness. Its PSNR is 0.29 dB below bicubic upsampling, so use `psrgan_x4_psnr.safetensors` when pixel accuracy matters more than texture.
|
| 165 |
+
- The training pixels are not square (about 10 m east-west, 15.6 m north-south), because tiles were requested on a square WGS84 grid.
|
| 166 |
+
|
| 167 |
+
# Out-of-Scope Use
|
| 168 |
+
|
| 169 |
+
- Measuring, mapping or deciding about individual parcels, buildings or people from super-resolved pixels.
|
| 170 |
+
- Replacing actual higher-resolution imagery in legal, insurance, military or safety-critical decisions.
|
| 171 |
+
- Presenting super-resolved images as original satellite observations.
|
| 172 |
+
|
| 173 |
+
# License
|
| 174 |
+
|
| 175 |
+
The code and weights are released under the BSD 3-Clause License, the same license as Real-ESRGAN, on which the weights are based. Training data contains modified Copernicus Sentinel data (2025).
|
| 176 |
+
|
| 177 |
+
# Citation
|
| 178 |
+
|
| 179 |
+
```
|
| 180 |
+
@misc{leupold2026psrgan,
|
| 181 |
+
author = {Leupold, Mark-Daniel},
|
| 182 |
+
title = {PSRGAN: Probabilistic Super-Resolution for Sentinel-2 with Uncertainty Estimates},
|
| 183 |
+
year = {2026},
|
| 184 |
+
publisher = {Hugging Face},
|
| 185 |
+
url = {https://huggingface.co/marklpd/PSRGAN}
|
| 186 |
+
}
|
| 187 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"num_feat": 64,
|
| 3 |
+
"num_block": 23,
|
| 4 |
+
"num_grow_ch": 32,
|
| 5 |
+
"scale": 4,
|
| 6 |
+
"bayesian_heads": true,
|
| 7 |
+
"logvar_min": -10.0,
|
| 8 |
+
"logvar_max": 10.0
|
| 9 |
+
}
|
eval/metrics.json
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"test": {
|
| 3 |
+
"n_patches": 1056,
|
| 4 |
+
"results": {
|
| 5 |
+
"Bicubic": {
|
| 6 |
+
"PSNR": 21.5607197952993,
|
| 7 |
+
"PSNR \u00b1": 0.1711068225145145,
|
| 8 |
+
"SSIM": 0.5427891769813318,
|
| 9 |
+
"SSIM \u00b1": 0.007653797755572996,
|
| 10 |
+
"LPIPS": 0.5987126496479367,
|
| 11 |
+
"LPIPS \u00b1": 0.004994828619425444,
|
| 12 |
+
"VGG": 8.134999072912967,
|
| 13 |
+
"OT": 0.003209914177811394
|
| 14 |
+
},
|
| 15 |
+
"Real-ESRGAN": {
|
| 16 |
+
"PSNR": 20.215020929322097,
|
| 17 |
+
"PSNR \u00b1": 0.15033991375476158,
|
| 18 |
+
"SSIM": 0.47526671260482434,
|
| 19 |
+
"SSIM \u00b1": 0.006763881811721902,
|
| 20 |
+
"LPIPS": 0.34822498551202996,
|
| 21 |
+
"LPIPS \u00b1": 0.004308759976577521,
|
| 22 |
+
"VGG": 8.63969765287457,
|
| 23 |
+
"OT": 0.00399009935614286
|
| 24 |
+
},
|
| 25 |
+
"PSRGAN (PSNR)": {
|
| 26 |
+
"PSNR": 21.936194533651527,
|
| 27 |
+
"PSNR \u00b1": 0.17383839192396056,
|
| 28 |
+
"SSIM": 0.5981000966646455,
|
| 29 |
+
"SSIM \u00b1": 0.006328675695824681,
|
| 30 |
+
"LPIPS": 0.20817970013895043,
|
| 31 |
+
"LPIPS \u00b1": 0.0027403351497943132,
|
| 32 |
+
"VGG": 4.8766192811908144,
|
| 33 |
+
"OT": 0.002672349598442176
|
| 34 |
+
},
|
| 35 |
+
"PSRGAN": {
|
| 36 |
+
"PSNR": 21.275709489981335,
|
| 37 |
+
"PSNR \u00b1": 0.16942986974481397,
|
| 38 |
+
"SSIM": 0.5693713306929126,
|
| 39 |
+
"SSIM \u00b1": 0.006270623736088413,
|
| 40 |
+
"LPIPS": 0.18790546653679374,
|
| 41 |
+
"LPIPS \u00b1": 0.002612060048786746,
|
| 42 |
+
"VGG": 5.858746337168144,
|
| 43 |
+
"OT": 0.0013482042025619496
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"psrgan_win_rate": {
|
| 47 |
+
"Bicubic": {
|
| 48 |
+
"PSNR": 0.29829545454545453,
|
| 49 |
+
"SSIM": 0.6903409090909091,
|
| 50 |
+
"LPIPS": 1.0
|
| 51 |
+
},
|
| 52 |
+
"Real-ESRGAN": {
|
| 53 |
+
"PSNR": 0.9706439393939394,
|
| 54 |
+
"SSIM": 1.0,
|
| 55 |
+
"LPIPS": 1.0
|
| 56 |
+
},
|
| 57 |
+
"PSRGAN (PSNR)": {
|
| 58 |
+
"PSNR": 0.000946969696969697,
|
| 59 |
+
"SSIM": 0.002840909090909091,
|
| 60 |
+
"LPIPS": 0.9261363636363636
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"test_city": {
|
| 65 |
+
"n_patches": 128,
|
| 66 |
+
"results": {
|
| 67 |
+
"Bicubic": {
|
| 68 |
+
"PSNR": 21.145912691950798,
|
| 69 |
+
"PSNR \u00b1": 0.2489663205630983,
|
| 70 |
+
"SSIM": 0.5521608623676002,
|
| 71 |
+
"SSIM \u00b1": 0.020581239817082063,
|
| 72 |
+
"LPIPS": 0.6825457969680429,
|
| 73 |
+
"LPIPS \u00b1": 0.014193458656650901,
|
| 74 |
+
"VGG": 9.868039011955261,
|
| 75 |
+
"OT": 0.003211126459063962
|
| 76 |
+
},
|
| 77 |
+
"Real-ESRGAN": {
|
| 78 |
+
"PSNR": 20.24102108180523,
|
| 79 |
+
"PSNR \u00b1": 0.22995863706651176,
|
| 80 |
+
"SSIM": 0.5146662218030542,
|
| 81 |
+
"SSIM \u00b1": 0.01896918156259728,
|
| 82 |
+
"LPIPS": 0.39876320981420577,
|
| 83 |
+
"LPIPS \u00b1": 0.014998464281860064,
|
| 84 |
+
"VGG": 9.620419383049011,
|
| 85 |
+
"OT": 0.003078749228734523
|
| 86 |
+
},
|
| 87 |
+
"PSRGAN (PSNR)": {
|
| 88 |
+
"PSNR": 21.299197003245354,
|
| 89 |
+
"PSNR \u00b1": 0.27709311095960687,
|
| 90 |
+
"SSIM": 0.5960347084328532,
|
| 91 |
+
"SSIM \u00b1": 0.019889227648886978,
|
| 92 |
+
"LPIPS": 0.21178198768757284,
|
| 93 |
+
"LPIPS \u00b1": 0.00864762965393086,
|
| 94 |
+
"VGG": 5.9829049706459045,
|
| 95 |
+
"OT": 0.003099578752880916
|
| 96 |
+
},
|
| 97 |
+
"PSRGAN": {
|
| 98 |
+
"PSNR": 20.610211864113808,
|
| 99 |
+
"PSNR \u00b1": 0.2630898367096838,
|
| 100 |
+
"SSIM": 0.5620126791764051,
|
| 101 |
+
"SSIM \u00b1": 0.018756582941474018,
|
| 102 |
+
"LPIPS": 0.19378441805019975,
|
| 103 |
+
"LPIPS \u00b1": 0.008681403045909196,
|
| 104 |
+
"VGG": 7.124138653278351,
|
| 105 |
+
"OT": 0.001463380001951009
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"calibration": {
|
| 110 |
+
"swag": {
|
| 111 |
+
"ECE": 0.02397674263321663,
|
| 112 |
+
"coverage_1sigma": 0.8664602438608805,
|
| 113 |
+
"coverage_2sigma": 0.9841360251108805,
|
| 114 |
+
"reliability": {
|
| 115 |
+
"pred_std": [
|
| 116 |
+
0.020123129710555077,
|
| 117 |
+
0.042232051491737366,
|
| 118 |
+
0.06536165624856949,
|
| 119 |
+
0.08905356377363205,
|
| 120 |
+
0.11278203874826431,
|
| 121 |
+
0.1365380734205246,
|
| 122 |
+
0.16020965576171875,
|
| 123 |
+
0.18388722836971283,
|
| 124 |
+
0.2074737250804901,
|
| 125 |
+
0.2310594618320465,
|
| 126 |
+
0.2546865940093994,
|
| 127 |
+
0.2783450484275818,
|
| 128 |
+
0.3019865155220032,
|
| 129 |
+
0.32563266158103943,
|
| 130 |
+
0.3493105471134186
|
| 131 |
+
],
|
| 132 |
+
"rmse": [
|
| 133 |
+
0.014095807448029518,
|
| 134 |
+
0.02998480387032032,
|
| 135 |
+
0.04914991185069084,
|
| 136 |
+
0.0686262995004654,
|
| 137 |
+
0.08680041134357452,
|
| 138 |
+
0.1045115739107132,
|
| 139 |
+
0.12249297648668289,
|
| 140 |
+
0.14000065624713898,
|
| 141 |
+
0.1562192142009735,
|
| 142 |
+
0.17163224518299103,
|
| 143 |
+
0.18531100451946259,
|
| 144 |
+
0.19668403267860413,
|
| 145 |
+
0.20926719903945923,
|
| 146 |
+
0.21913963556289673,
|
| 147 |
+
0.2296137809753418
|
| 148 |
+
]
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"bayesian_heads": {
|
| 152 |
+
"ECE": 0.02886166655259949,
|
| 153 |
+
"coverage_1sigma": 0.8842587471008301,
|
| 154 |
+
"coverage_2sigma": 0.986927350362142,
|
| 155 |
+
"reliability": {
|
| 156 |
+
"pred_std": [
|
| 157 |
+
0.02026936411857605,
|
| 158 |
+
0.04286534711718559,
|
| 159 |
+
0.0662725418806076,
|
| 160 |
+
0.08985785394906998,
|
| 161 |
+
0.11369309574365616,
|
| 162 |
+
0.1376895308494568,
|
| 163 |
+
0.16160856187343597,
|
| 164 |
+
0.18553175032138824,
|
| 165 |
+
0.20935258269309998,
|
| 166 |
+
0.233138307929039,
|
| 167 |
+
0.25700125098228455,
|
| 168 |
+
0.28088292479515076,
|
| 169 |
+
0.30468234419822693,
|
| 170 |
+
0.32858404517173767,
|
| 171 |
+
0.35245150327682495
|
| 172 |
+
],
|
| 173 |
+
"rmse": [
|
| 174 |
+
0.012933187186717987,
|
| 175 |
+
0.027884116396307945,
|
| 176 |
+
0.045200906693935394,
|
| 177 |
+
0.0637541264295578,
|
| 178 |
+
0.08282162249088287,
|
| 179 |
+
0.10149621963500977,
|
| 180 |
+
0.1198882907629013,
|
| 181 |
+
0.13830198347568512,
|
| 182 |
+
0.15485383570194244,
|
| 183 |
+
0.17070721089839935,
|
| 184 |
+
0.1843496859073639,
|
| 185 |
+
0.19707782566547394,
|
| 186 |
+
0.2094695121049881,
|
| 187 |
+
0.21937879920005798,
|
| 188 |
+
0.23037448525428772
|
| 189 |
+
]
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"uncertainty_error_correlation": {
|
| 194 |
+
"pearson": 0.5869906544685364,
|
| 195 |
+
"spearman": 0.7000287473727114,
|
| 196 |
+
"by_region": {
|
| 197 |
+
"edges": {
|
| 198 |
+
"pearson": 0.3714364469051361,
|
| 199 |
+
"spearman": 0.37498411900628265
|
| 200 |
+
},
|
| 201 |
+
"flat": {
|
| 202 |
+
"pearson": 0.5925312638282776,
|
| 203 |
+
"spearman": 0.6675135785054067
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
"error_by_uncertainty_decile": [
|
| 208 |
+
0.009789826348423958,
|
| 209 |
+
0.016854647547006607,
|
| 210 |
+
0.024157894775271416,
|
| 211 |
+
0.03273877874016762,
|
| 212 |
+
0.04215969517827034,
|
| 213 |
+
0.052353471517562866,
|
| 214 |
+
0.06426148116588593,
|
| 215 |
+
0.07902297377586365,
|
| 216 |
+
0.09887537360191345,
|
| 217 |
+
0.13998572528362274
|
| 218 |
+
],
|
| 219 |
+
"training": {
|
| 220 |
+
"train_patches": 8171,
|
| 221 |
+
"val_patches": 1008,
|
| 222 |
+
"test_patches": 1056,
|
| 223 |
+
"batch_size": 16,
|
| 224 |
+
"batches_per_epoch": 64,
|
| 225 |
+
"pretrain_epochs": 768,
|
| 226 |
+
"freeze_rrdb_epochs": 168,
|
| 227 |
+
"adversarial_epochs": 64,
|
| 228 |
+
"lr_G": 0.0002,
|
| 229 |
+
"lr_D": 0.0002,
|
| 230 |
+
"loss_weights_pretrain": {
|
| 231 |
+
"NLL": 0.6,
|
| 232 |
+
"SSIM": 0.7,
|
| 233 |
+
"VGG": 1.0
|
| 234 |
+
},
|
| 235 |
+
"loss_weights_adversarial": {
|
| 236 |
+
"ADV": 0.01,
|
| 237 |
+
"CX": 0.2
|
| 238 |
+
},
|
| 239 |
+
"swag": {
|
| 240 |
+
"snapshots": 32,
|
| 241 |
+
"start_epoch": 56,
|
| 242 |
+
"collect_every_batches": 16
|
| 243 |
+
},
|
| 244 |
+
"gpu": "NVIDIA A100-SXM4-40GB",
|
| 245 |
+
"seed": 42
|
| 246 |
+
}
|
| 247 |
+
}
|
images/psrgan_city.png
ADDED
|
Git LFS Details
|
images/psrgan_uncertainty.png
ADDED
|
Git LFS Details
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8708e583a031ef01cd9d3cb29a6bdce842f1ba1a64ac5ed9e9ee68e995c17f58
|
| 3 |
+
size 66892468
|
notebooks/PSRGAN_dataset.ipynb
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# PSRGAN – Datensatz\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"`Sentinel-2 Kacheln laden, Splits auf Kachelebene festlegen und LR/HR-Patches für Hugging Face erzeugen.`\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"Ablauf: Kacheln (alt + neu) → Split pro Kachel (räumlich getrennt, 0,5°-Blöcke) → Patches → Parquet in `hf/dataset`.\n",
|
| 12 |
+
"Läuft auf CPU-Runtime, keine GPU nötig."
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"cell_type": "markdown",
|
| 17 |
+
"metadata": {},
|
| 18 |
+
"source": [
|
| 19 |
+
"# 0. Setup"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cell_type": "markdown",
|
| 24 |
+
"metadata": {},
|
| 25 |
+
"source": [
|
| 26 |
+
"### Installationen"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"cell_type": "code",
|
| 31 |
+
"metadata": {},
|
| 32 |
+
"source": [
|
| 33 |
+
"!pip install rasterio cartopy --quiet"
|
| 34 |
+
],
|
| 35 |
+
"execution_count": null,
|
| 36 |
+
"outputs": []
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"cell_type": "markdown",
|
| 40 |
+
"metadata": {},
|
| 41 |
+
"source": [
|
| 42 |
+
"### Imports"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"cell_type": "code",
|
| 47 |
+
"metadata": {},
|
| 48 |
+
"source": [
|
| 49 |
+
"import os\n",
|
| 50 |
+
"import io\n",
|
| 51 |
+
"import csv\n",
|
| 52 |
+
"import json\n",
|
| 53 |
+
"import math\n",
|
| 54 |
+
"import time\n",
|
| 55 |
+
"import random\n",
|
| 56 |
+
"import shutil\n",
|
| 57 |
+
"import threading\n",
|
| 58 |
+
"import warnings\n",
|
| 59 |
+
"import logging\n",
|
| 60 |
+
"from glob import glob\n",
|
| 61 |
+
"from io import BytesIO\n",
|
| 62 |
+
"from concurrent.futures import ThreadPoolExecutor\n",
|
| 63 |
+
"\n",
|
| 64 |
+
"import numpy as np\n",
|
| 65 |
+
"import pandas as pd\n",
|
| 66 |
+
"import cv2\n",
|
| 67 |
+
"import requests\n",
|
| 68 |
+
"import rasterio\n",
|
| 69 |
+
"from rasterio.env import Env\n",
|
| 70 |
+
"from PIL import Image\n",
|
| 71 |
+
"from tqdm import tqdm\n",
|
| 72 |
+
"\n",
|
| 73 |
+
"import matplotlib.pyplot as plt\n",
|
| 74 |
+
"\n",
|
| 75 |
+
"import datasets\n",
|
| 76 |
+
"from datasets import Dataset, Features, Value, Array2D, Array3D\n",
|
| 77 |
+
"from datasets import Image as HFImage\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"print(\"datasets:\", datasets.__version__)"
|
| 80 |
+
],
|
| 81 |
+
"execution_count": null,
|
| 82 |
+
"outputs": []
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"cell_type": "markdown",
|
| 86 |
+
"metadata": {},
|
| 87 |
+
"source": [
|
| 88 |
+
"### Google Drive Mounten"
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"cell_type": "code",
|
| 93 |
+
"metadata": {},
|
| 94 |
+
"source": [
|
| 95 |
+
"from google.colab import drive\n",
|
| 96 |
+
"drive.mount(\"/content/drive\")"
|
| 97 |
+
],
|
| 98 |
+
"execution_count": null,
|
| 99 |
+
"outputs": []
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"cell_type": "code",
|
| 103 |
+
"metadata": {},
|
| 104 |
+
"source": [
|
| 105 |
+
"logging.getLogger(\"rasterio\").setLevel(logging.ERROR)\n",
|
| 106 |
+
"logging.getLogger(\"rasterio._env\").setLevel(logging.ERROR)\n",
|
| 107 |
+
"\n",
|
| 108 |
+
"gdal_env = Env(GDAL_DISABLE_READDIR_ON_OPEN=\"YES\")\n",
|
| 109 |
+
"gdal_env.__enter__()\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"warnings.filterwarnings(\"ignore\", category=rasterio.errors.NotGeoreferencedWarning)\n",
|
| 112 |
+
"warnings.filterwarnings(\"ignore\", message=\".*TIFFReadDirectory.*ExtraSamples.*\")"
|
| 113 |
+
],
|
| 114 |
+
"execution_count": null,
|
| 115 |
+
"outputs": []
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"cell_type": "markdown",
|
| 119 |
+
"metadata": {},
|
| 120 |
+
"source": [
|
| 121 |
+
"# 1. Konfiguration"
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"cell_type": "code",
|
| 126 |
+
"metadata": {},
|
| 127 |
+
"source": [
|
| 128 |
+
"config = {\n",
|
| 129 |
+
" # Sentinel-2\n",
|
| 130 |
+
" \"BANDS\": [\"B4\", \"B3\", \"B2\"],\n",
|
| 131 |
+
"\n",
|
| 132 |
+
" # Geometrie\n",
|
| 133 |
+
" \"PATCH_SIZE\": 256,\n",
|
| 134 |
+
" \"UPSCALE\": 4,\n",
|
| 135 |
+
" \"LR_PATCH_SIZE\": 64,\n",
|
| 136 |
+
"\n",
|
| 137 |
+
" # AOI\n",
|
| 138 |
+
" \"AOI_KM\": 8.0,\n",
|
| 139 |
+
" \"REQUEST_PIXELS\": 512,\n",
|
| 140 |
+
"\n",
|
| 141 |
+
" # Sampling\n",
|
| 142 |
+
" \"TARGET_PATCHES\": 10000,\n",
|
| 143 |
+
" \"PATCHES_PER_TILE\": 16,\n",
|
| 144 |
+
" \"MAX_CLOUD_TILE\": 0.25,\n",
|
| 145 |
+
" \"MAX_CLOUD_PATCH\": 0.15,\n",
|
| 146 |
+
" \"MAX_WATER_TILE\": 0.5, # neu: Meer/Seen raus\n",
|
| 147 |
+
" \"MAX_NODATA_TILE\": 0.01, # neu: Swath-Rand raus\n",
|
| 148 |
+
"\n",
|
| 149 |
+
" # Region (alt, erste 192 Kacheln)\n",
|
| 150 |
+
" \"REGION_V1\": {\n",
|
| 151 |
+
" \"lon_min\": 7.5,\n",
|
| 152 |
+
" \"lon_max\": 13.5,\n",
|
| 153 |
+
" \"lat_min\": 46.5,\n",
|
| 154 |
+
" \"lat_max\": 51.5,\n",
|
| 155 |
+
" },\n",
|
| 156 |
+
"\n",
|
| 157 |
+
" # Region (neu, Mitteleuropa)\n",
|
| 158 |
+
" \"REGION\": {\n",
|
| 159 |
+
" \"lon_min\": 4.0,\n",
|
| 160 |
+
" \"lon_max\": 19.5,\n",
|
| 161 |
+
" \"lat_min\": 45.5,\n",
|
| 162 |
+
" \"lat_max\": 54.5,\n",
|
| 163 |
+
" },\n",
|
| 164 |
+
"\n",
|
| 165 |
+
" # Zeitfenster (wie bisher)\n",
|
| 166 |
+
" \"TIME_FROM\": \"2025-09-20T00:00:00Z\",\n",
|
| 167 |
+
" \"TIME_TO\": \"2025-10-07T23:59:59Z\",\n",
|
| 168 |
+
"\n",
|
| 169 |
+
" # Splits\n",
|
| 170 |
+
" \"SPLIT_FRACTIONS\": {\"train\": 0.8, \"validation\": 0.1, \"test\": 0.1},\n",
|
| 171 |
+
" \"BLOCK_DEG\": 0.5,\n",
|
| 172 |
+
"\n",
|
| 173 |
+
" # Download\n",
|
| 174 |
+
" \"N_WORKERS\": 6,\n",
|
| 175 |
+
"\n",
|
| 176 |
+
" # Zufall\n",
|
| 177 |
+
" \"SEED\": 42,\n",
|
| 178 |
+
"}\n",
|
| 179 |
+
"\n",
|
| 180 |
+
"config[\"AOI_SIZE_DEG\"] = config[\"AOI_KM\"] / 111.0\n",
|
| 181 |
+
"config[\"LR_PATCH_SIZE\"] = config[\"PATCH_SIZE\"] // config[\"UPSCALE\"]\n",
|
| 182 |
+
"\n",
|
| 183 |
+
"for k, v in config.items():\n",
|
| 184 |
+
" print(f\"{k:22s}: {v}\")"
|
| 185 |
+
],
|
| 186 |
+
"execution_count": null,
|
| 187 |
+
"outputs": []
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"cell_type": "code",
|
| 191 |
+
"metadata": {},
|
| 192 |
+
"source": [
|
| 193 |
+
"def set_seed(seed: int):\n",
|
| 194 |
+
" random.seed(seed)\n",
|
| 195 |
+
" np.random.seed(seed)\n",
|
| 196 |
+
" print(f\"Seed set to {seed}\")\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"set_seed(config[\"SEED\"])"
|
| 199 |
+
],
|
| 200 |
+
"execution_count": null,
|
| 201 |
+
"outputs": []
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"cell_type": "markdown",
|
| 205 |
+
"metadata": {},
|
| 206 |
+
"source": [
|
| 207 |
+
"### Pfade"
|
| 208 |
+
]
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"cell_type": "code",
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"source": [
|
| 214 |
+
"DATA_ROOT = \"/content/drive/MyDrive/OT_SRGAN/data\"\n",
|
| 215 |
+
"DOWNLOAD_DIR = os.path.join(DATA_ROOT, \"s2_aoi_tiles\")\n",
|
| 216 |
+
"CUSTOM_DOWNLOAD_DIR = os.path.join(DATA_ROOT, \"custom_s2_tiles\")\n",
|
| 217 |
+
"SPLITS_PATH = os.path.join(DATA_ROOT, \"tile_splits.csv\")\n",
|
| 218 |
+
"\n",
|
| 219 |
+
"HF_DATASET_DIR = \"/content/drive/MyDrive/OT_SRGAN/hf/dataset\"\n",
|
| 220 |
+
"HF_DATA_DIR = os.path.join(HF_DATASET_DIR, \"data\")\n",
|
| 221 |
+
"HF_IMAGES_DIR = os.path.join(HF_DATASET_DIR, \"images\")\n",
|
| 222 |
+
"HF_META_DIR = os.path.join(HF_DATASET_DIR, \"metadata\")\n",
|
| 223 |
+
"\n",
|
| 224 |
+
"# Lokal bauen, dann auf Drive kopieren\n",
|
| 225 |
+
"LOCAL_OUT = \"/content/hf_dataset_build\"\n",
|
| 226 |
+
"LOCAL_CACHE = \"/content/hf_cache\"\n",
|
| 227 |
+
"\n",
|
| 228 |
+
"for d in [DOWNLOAD_DIR, CUSTOM_DOWNLOAD_DIR, HF_DATA_DIR, HF_IMAGES_DIR, HF_META_DIR, LOCAL_OUT, LOCAL_CACHE]:\n",
|
| 229 |
+
" os.makedirs(d, exist_ok=True)\n",
|
| 230 |
+
"\n",
|
| 231 |
+
"PROCESS_URL = \"https://sh.dataspace.copernicus.eu/api/v1/process\""
|
| 232 |
+
],
|
| 233 |
+
"execution_count": null,
|
| 234 |
+
"outputs": []
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"cell_type": "markdown",
|
| 238 |
+
"metadata": {},
|
| 239 |
+
"source": [
|
| 240 |
+
"# 2. Kacheln laden"
|
| 241 |
+
]
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"cell_type": "markdown",
|
| 245 |
+
"metadata": {},
|
| 246 |
+
"source": [
|
| 247 |
+
"### Token"
|
| 248 |
+
]
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"cell_type": "code",
|
| 252 |
+
"metadata": {},
|
| 253 |
+
"source": [
|
| 254 |
+
"from google.colab import userdata\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"CLIENT_ID = userdata.get(\"COPERNICUS_CLIENT_ID\")\n",
|
| 257 |
+
"CLIENT_SECRET = userdata.get(\"COPERNICUS_CLIENT_SECRET\")\n",
|
| 258 |
+
"\n",
|
| 259 |
+
"token_url = (\n",
|
| 260 |
+
" \"https://identity.dataspace.copernicus.eu/auth/\"\n",
|
| 261 |
+
" \"realms/CDSE/protocol/openid-connect/token\"\n",
|
| 262 |
+
")\n",
|
| 263 |
+
"\n",
|
| 264 |
+
"HEADERS = {\"Content-Type\": \"application/json\"}\n",
|
| 265 |
+
"token_lock = threading.Lock()\n",
|
| 266 |
+
"\n",
|
| 267 |
+
"def refresh_token():\n",
|
| 268 |
+
" # Token läuft nach ~10 min ab\n",
|
| 269 |
+
" resp = requests.post(\n",
|
| 270 |
+
" token_url,\n",
|
| 271 |
+
" data={\n",
|
| 272 |
+
" \"grant_type\": \"client_credentials\",\n",
|
| 273 |
+
" \"client_id\": CLIENT_ID,\n",
|
| 274 |
+
" \"client_secret\": CLIENT_SECRET,\n",
|
| 275 |
+
" },\n",
|
| 276 |
+
" )\n",
|
| 277 |
+
" resp.raise_for_status()\n",
|
| 278 |
+
" HEADERS[\"Authorization\"] = f\"Bearer {resp.json()['access_token']}\"\n",
|
| 279 |
+
"\n",
|
| 280 |
+
"refresh_token()\n",
|
| 281 |
+
"print(\"Access token acquired.\")"
|
| 282 |
+
],
|
| 283 |
+
"execution_count": null,
|
| 284 |
+
"outputs": []
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
"cell_type": "markdown",
|
| 288 |
+
"metadata": {},
|
| 289 |
+
"source": [
|
| 290 |
+
"### Request"
|
| 291 |
+
]
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"cell_type": "code",
|
| 295 |
+
"metadata": {},
|
| 296 |
+
"source": [
|
| 297 |
+
"EVALSCRIPT = \"\"\"\n",
|
| 298 |
+
"//VERSION=3\n",
|
| 299 |
+
"function setup() {\n",
|
| 300 |
+
" return {\n",
|
| 301 |
+
" input: [{bands:[\"B02\",\"B03\",\"B04\",\"B08\",\"SCL\"]}],\n",
|
| 302 |
+
" output: {bands: 5, sampleType: \"FLOAT32\"}\n",
|
| 303 |
+
" };\n",
|
| 304 |
+
"}\n",
|
| 305 |
+
"\n",
|
| 306 |
+
"function evaluatePixel(s) {\n",
|
| 307 |
+
" let ndvi = (s.B08 - s.B04) / (s.B08 + s.B04 + 1e-6);\n",
|
| 308 |
+
" return [s.B04, s.B03, s.B02, ndvi, s.SCL];\n",
|
| 309 |
+
"}\n",
|
| 310 |
+
"\"\"\""
|
| 311 |
+
],
|
| 312 |
+
"execution_count": null,
|
| 313 |
+
"outputs": []
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"cell_type": "code",
|
| 317 |
+
"metadata": {},
|
| 318 |
+
"source": [
|
| 319 |
+
"def build_request(bbox):\n",
|
| 320 |
+
" return {\n",
|
| 321 |
+
" \"input\": {\n",
|
| 322 |
+
" \"bounds\": {\"bbox\": bbox},\n",
|
| 323 |
+
" \"data\": [{\n",
|
| 324 |
+
" \"type\": \"sentinel-2-l2a\",\n",
|
| 325 |
+
" \"dataFilter\": {\n",
|
| 326 |
+
" \"timeRange\": {\n",
|
| 327 |
+
" \"from\": config[\"TIME_FROM\"],\n",
|
| 328 |
+
" \"to\": config[\"TIME_TO\"],\n",
|
| 329 |
+
" }\n",
|
| 330 |
+
" },\n",
|
| 331 |
+
" \"processing\": {\"mosaickingOrder\": \"leastCC\"},\n",
|
| 332 |
+
" }],\n",
|
| 333 |
+
" },\n",
|
| 334 |
+
" \"output\": {\n",
|
| 335 |
+
" \"width\": config[\"REQUEST_PIXELS\"],\n",
|
| 336 |
+
" \"height\": config[\"REQUEST_PIXELS\"],\n",
|
| 337 |
+
" \"responses\": [{\n",
|
| 338 |
+
" \"identifier\": \"default\",\n",
|
| 339 |
+
" \"format\": {\"type\": \"image/tiff\"},\n",
|
| 340 |
+
" }],\n",
|
| 341 |
+
" },\n",
|
| 342 |
+
" \"evalscript\": EVALSCRIPT,\n",
|
| 343 |
+
" }"
|
| 344 |
+
],
|
| 345 |
+
"execution_count": null,
|
| 346 |
+
"outputs": []
|
| 347 |
+
},
|
| 348 |
+
{
|
| 349 |
+
"cell_type": "code",
|
| 350 |
+
"metadata": {},
|
| 351 |
+
"source": [
|
| 352 |
+
"def compute_cloud_mask(scl):\n",
|
| 353 |
+
" cloud_classes = {3,7,8,9,10}\n",
|
| 354 |
+
" return np.isin(scl, list(cloud_classes))\n",
|
| 355 |
+
"\n",
|
| 356 |
+
"def compute_cloud_ratio(scl):\n",
|
| 357 |
+
" mask = compute_cloud_mask(scl)\n",
|
| 358 |
+
" return mask.sum() / scl.size\n",
|
| 359 |
+
"\n",
|
| 360 |
+
"def parse_multiband_tiff(content):\n",
|
| 361 |
+
" with rasterio.open(BytesIO(content)) as src:\n",
|
| 362 |
+
" rgb = src.read([1,2,3]).astype(np.float32)\n",
|
| 363 |
+
" ndvi = src.read(4).astype(np.float32)\n",
|
| 364 |
+
" scl = src.read(5).astype(np.float32)\n",
|
| 365 |
+
" return rgb, ndvi, scl\n",
|
| 366 |
+
"\n",
|
| 367 |
+
"def bbox_overlap(a, b):\n",
|
| 368 |
+
" # [lon_min, lat_min, lon_max, lat_max]\n",
|
| 369 |
+
" return not (a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1])"
|
| 370 |
+
],
|
| 371 |
+
"execution_count": null,
|
| 372 |
+
"outputs": []
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
"cell_type": "markdown",
|
| 376 |
+
"metadata": {},
|
| 377 |
+
"source": [
|
| 378 |
+
"### Bestehende Kacheln"
|
| 379 |
+
]
|
| 380 |
+
},
|
| 381 |
+
{
|
| 382 |
+
"cell_type": "code",
|
| 383 |
+
"metadata": {},
|
| 384 |
+
"source": [
|
| 385 |
+
"existing_tiles = sorted(glob(os.path.join(DOWNLOAD_DIR, \"aoi_*.npz\")))\n",
|
| 386 |
+
"custom_tiles = sorted(glob(os.path.join(CUSTOM_DOWNLOAD_DIR, \"custom_*.npz\")))\n",
|
| 387 |
+
"\n",
|
| 388 |
+
"occupied = [list(np.load(p)[\"bbox\"]) for p in tqdm(existing_tiles + custom_tiles, desc=\"Read bboxes\", ncols=120)]\n",
|
| 389 |
+
"\n",
|
| 390 |
+
"n_tiles_target = math.ceil(config[\"TARGET_PATCHES\"] / config[\"PATCHES_PER_TILE\"] * 1.03) # 3% Puffer für verworfene Patches\n",
|
| 391 |
+
"n_new = max(0, n_tiles_target - len(existing_tiles) - len(custom_tiles))\n",
|
| 392 |
+
"\n",
|
| 393 |
+
"print(f\"Found {len(existing_tiles)} AOI tiles + {len(custom_tiles)} custom tiles.\")\n",
|
| 394 |
+
"print(f\"Target {n_tiles_target} tiles → {n_new} new tiles to download.\")"
|
| 395 |
+
],
|
| 396 |
+
"execution_count": null,
|
| 397 |
+
"outputs": []
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"cell_type": "markdown",
|
| 401 |
+
"metadata": {},
|
| 402 |
+
"source": [
|
| 403 |
+
"### Neue Kacheln (parallel)\n",
|
| 404 |
+
"\n",
|
| 405 |
+
"Neue Kacheln überlappen keine bestehende Kachel. Bei erneutem Ausführen wird nur der Rest geladen."
|
| 406 |
+
]
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"cell_type": "code",
|
| 410 |
+
"metadata": {},
|
| 411 |
+
"source": [
|
| 412 |
+
"state_lock = threading.Lock()\n",
|
| 413 |
+
"rng_bbox = random.Random(config[\"SEED\"] + len(existing_tiles))\n",
|
| 414 |
+
"next_idx = max([int(os.path.basename(p)[4:8]) for p in existing_tiles], default=-1) + 1\n",
|
| 415 |
+
"collected = []\n",
|
| 416 |
+
"rejected = {\"http\": 0, \"cloud\": 0, \"water\": 0, \"nodata\": 0, \"parse\": 0}\n",
|
| 417 |
+
"\n",
|
| 418 |
+
"def random_bbox():\n",
|
| 419 |
+
" r = config[\"REGION\"]\n",
|
| 420 |
+
" lon = rng_bbox.uniform(r[\"lon_min\"], r[\"lon_max\"])\n",
|
| 421 |
+
" lat = rng_bbox.uniform(r[\"lat_min\"], r[\"lat_max\"])\n",
|
| 422 |
+
" d = config[\"AOI_SIZE_DEG\"] / 2\n",
|
| 423 |
+
" return [lon - d, lat - d, lon + d, lat + d]\n",
|
| 424 |
+
"\n",
|
| 425 |
+
"def reserve_bbox():\n",
|
| 426 |
+
" # Freie Fläche suchen und reservieren\n",
|
| 427 |
+
" with state_lock:\n",
|
| 428 |
+
" while True:\n",
|
| 429 |
+
" bbox = random_bbox()\n",
|
| 430 |
+
" if not any(bbox_overlap(bbox, o) for o in occupied):\n",
|
| 431 |
+
" occupied.append(bbox)\n",
|
| 432 |
+
" return bbox\n",
|
| 433 |
+
"\n",
|
| 434 |
+
"def release_bbox(bbox):\n",
|
| 435 |
+
" with state_lock:\n",
|
| 436 |
+
" occupied.remove(bbox)\n",
|
| 437 |
+
"\n",
|
| 438 |
+
"def post_with_retry(body, max_tries=6):\n",
|
| 439 |
+
" for t in range(max_tries):\n",
|
| 440 |
+
" r = requests.post(PROCESS_URL, headers=dict(HEADERS), json=body, timeout=120)\n",
|
| 441 |
+
" if r.status_code == 401:\n",
|
| 442 |
+
" with token_lock:\n",
|
| 443 |
+
" refresh_token()\n",
|
| 444 |
+
" continue\n",
|
| 445 |
+
" if r.status_code == 429 or r.status_code >= 500:\n",
|
| 446 |
+
" time.sleep(2 ** t)\n",
|
| 447 |
+
" continue\n",
|
| 448 |
+
" return r\n",
|
| 449 |
+
" return r\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"def download_worker(pbar):\n",
|
| 452 |
+
" global next_idx\n",
|
| 453 |
+
" while True:\n",
|
| 454 |
+
" with state_lock:\n",
|
| 455 |
+
" if len(collected) >= n_new:\n",
|
| 456 |
+
" return\n",
|
| 457 |
+
"\n",
|
| 458 |
+
" bbox = reserve_bbox()\n",
|
| 459 |
+
" try:\n",
|
| 460 |
+
" r = post_with_retry(build_request(bbox))\n",
|
| 461 |
+
" except requests.RequestException:\n",
|
| 462 |
+
" r = None\n",
|
| 463 |
+
" if r is None or r.status_code != 200:\n",
|
| 464 |
+
" rejected[\"http\"] += 1\n",
|
| 465 |
+
" release_bbox(bbox)\n",
|
| 466 |
+
" if r is not None and r.status_code in (402, 403):\n",
|
| 467 |
+
" print(\"Quota/permission error:\", r.status_code, r.text[:200])\n",
|
| 468 |
+
" return\n",
|
| 469 |
+
" continue\n",
|
| 470 |
+
"\n",
|
| 471 |
+
" try:\n",
|
| 472 |
+
" rgb, ndvi, scl = parse_multiband_tiff(r.content)\n",
|
| 473 |
+
" except Exception:\n",
|
| 474 |
+
" rejected[\"parse\"] += 1\n",
|
| 475 |
+
" release_bbox(bbox)\n",
|
| 476 |
+
" continue\n",
|
| 477 |
+
"\n",
|
| 478 |
+
" if compute_cloud_ratio(scl) > config[\"MAX_CLOUD_TILE\"]:\n",
|
| 479 |
+
" rejected[\"cloud\"] += 1; release_bbox(bbox); continue\n",
|
| 480 |
+
" if (scl == 6).mean() > config[\"MAX_WATER_TILE\"]:\n",
|
| 481 |
+
" rejected[\"water\"] += 1; release_bbox(bbox); continue\n",
|
| 482 |
+
" if np.isin(scl, [0, 1]).mean() > config[\"MAX_NODATA_TILE\"]:\n",
|
| 483 |
+
" rejected[\"nodata\"] += 1; release_bbox(bbox); continue\n",
|
| 484 |
+
"\n",
|
| 485 |
+
" with state_lock:\n",
|
| 486 |
+
" if len(collected) >= n_new:\n",
|
| 487 |
+
" release_bbox_unlocked = True\n",
|
| 488 |
+
" else:\n",
|
| 489 |
+
" release_bbox_unlocked = False\n",
|
| 490 |
+
" tile_id = f\"aoi_{next_idx:04d}\"\n",
|
| 491 |
+
" next_idx += 1\n",
|
| 492 |
+
" collected.append(tile_id)\n",
|
| 493 |
+
" if release_bbox_unlocked:\n",
|
| 494 |
+
" release_bbox(bbox)\n",
|
| 495 |
+
" return\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" np.savez_compressed(\n",
|
| 498 |
+
" os.path.join(DOWNLOAD_DIR, f\"{tile_id}.npz\"),\n",
|
| 499 |
+
" rgb=rgb,\n",
|
| 500 |
+
" ndvi=ndvi,\n",
|
| 501 |
+
" scl=scl,\n",
|
| 502 |
+
" bbox=bbox,\n",
|
| 503 |
+
" time_from=config[\"TIME_FROM\"],\n",
|
| 504 |
+
" time_to=config[\"TIME_TO\"],\n",
|
| 505 |
+
" )\n",
|
| 506 |
+
" pbar.update(1)\n",
|
| 507 |
+
"\n",
|
| 508 |
+
"if n_new > 0:\n",
|
| 509 |
+
" pbar = tqdm(total=n_new, desc=\"Downloading AOIs\", ncols=120)\n",
|
| 510 |
+
" with ThreadPoolExecutor(max_workers=config[\"N_WORKERS\"]) as ex:\n",
|
| 511 |
+
" for _ in range(config[\"N_WORKERS\"]):\n",
|
| 512 |
+
" ex.submit(download_worker, pbar)\n",
|
| 513 |
+
" pbar.close()\n",
|
| 514 |
+
"\n",
|
| 515 |
+
"print(f\"Added {len(collected)} new AOI tiles. Rejected: {rejected}\")\n",
|
| 516 |
+
"\n",
|
| 517 |
+
"existing_tiles = sorted(glob(os.path.join(DOWNLOAD_DIR, \"aoi_*.npz\")))\n",
|
| 518 |
+
"print(f\"Total AOI tiles on disk: {len(existing_tiles)}\")"
|
| 519 |
+
],
|
| 520 |
+
"execution_count": null,
|
| 521 |
+
"outputs": []
|
| 522 |
+
},
|
| 523 |
+
{
|
| 524 |
+
"cell_type": "markdown",
|
| 525 |
+
"metadata": {},
|
| 526 |
+
"source": [
|
| 527 |
+
"# 3. Splits auf Kachelebene\n",
|
| 528 |
+
"\n",
|
| 529 |
+
"1. Überlappende Kacheln → eine Gruppe (Union-Find).\n",
|
| 530 |
+
"2. Gruppen nach 0,5°-Blöcken zusammenfassen, Blöcke zufällig auf Splits verteilen (räumlich getrennt).\n",
|
| 531 |
+
"3. Blöcke mit Stadt-Kacheln (Herzogenaurach, Wolfsburg, München) → immer `test`.\n",
|
| 532 |
+
"\n",
|
| 533 |
+
"Augmentation passiert erst im Training und nur auf `train`."
|
| 534 |
+
]
|
| 535 |
+
},
|
| 536 |
+
{
|
| 537 |
+
"cell_type": "code",
|
| 538 |
+
"metadata": {},
|
| 539 |
+
"source": [
|
| 540 |
+
"# [lon_min, lat_min, lon_max, lat_max]\n",
|
| 541 |
+
"CUSTOM_AOIS = {\n",
|
| 542 |
+
" \"Herzogenaurach\": [\n",
|
| 543 |
+
" [10.85, 49.55, 10.95, 49.65],\n",
|
| 544 |
+
" [10.80, 49.57, 10.90, 49.67],\n",
|
| 545 |
+
" [10.87, 49.53, 10.97, 49.63],\n",
|
| 546 |
+
" [10.83, 49.56, 10.93, 49.66],\n",
|
| 547 |
+
" ],\n",
|
| 548 |
+
" \"Wolfsburg\": [\n",
|
| 549 |
+
" [10.75, 52.40, 10.85, 52.50],\n",
|
| 550 |
+
" [10.70, 52.42, 10.80, 52.52],\n",
|
| 551 |
+
" ],\n",
|
| 552 |
+
" \"München\": [\n",
|
| 553 |
+
" [11.50, 48.10, 11.60, 48.20],\n",
|
| 554 |
+
" [11.55, 48.12, 11.65, 48.22],\n",
|
| 555 |
+
" ],\n",
|
| 556 |
+
"}\n",
|
| 557 |
+
"\n",
|
| 558 |
+
"def city_name(bbox):\n",
|
| 559 |
+
" for name, boxes in CUSTOM_AOIS.items():\n",
|
| 560 |
+
" if any(np.allclose(bbox, b) for b in boxes):\n",
|
| 561 |
+
" return name\n",
|
| 562 |
+
" return \"unknown\"\n",
|
| 563 |
+
"\n",
|
| 564 |
+
"tiles = []\n",
|
| 565 |
+
"for p in existing_tiles:\n",
|
| 566 |
+
" tiles.append({\"tile_id\": os.path.basename(p)[:-4], \"path\": p, \"source\": \"random\", \"aoi_name\": \"\",\n",
|
| 567 |
+
" \"bbox\": [float(v) for v in np.load(p)[\"bbox\"]]})\n",
|
| 568 |
+
"for p in custom_tiles:\n",
|
| 569 |
+
" bbox = [float(v) for v in np.load(p)[\"bbox\"]]\n",
|
| 570 |
+
" tiles.append({\"tile_id\": os.path.basename(p)[:-4], \"path\": p, \"source\": \"city\", \"aoi_name\": city_name(bbox),\n",
|
| 571 |
+
" \"bbox\": bbox})\n",
|
| 572 |
+
"\n",
|
| 573 |
+
"print(f\"{len(tiles)} tiles total\")"
|
| 574 |
+
],
|
| 575 |
+
"execution_count": null,
|
| 576 |
+
"outputs": []
|
| 577 |
+
},
|
| 578 |
+
{
|
| 579 |
+
"cell_type": "code",
|
| 580 |
+
"metadata": {},
|
| 581 |
+
"source": [
|
| 582 |
+
"# Union-Find über überlappende Kacheln\n",
|
| 583 |
+
"parent = list(range(len(tiles)))\n",
|
| 584 |
+
"\n",
|
| 585 |
+
"def find(i):\n",
|
| 586 |
+
" while parent[i] != i:\n",
|
| 587 |
+
" parent[i] = parent[parent[i]]\n",
|
| 588 |
+
" i = parent[i]\n",
|
| 589 |
+
" return i\n",
|
| 590 |
+
"\n",
|
| 591 |
+
"for i in range(len(tiles)):\n",
|
| 592 |
+
" for j in range(i + 1, len(tiles)):\n",
|
| 593 |
+
" if bbox_overlap(tiles[i][\"bbox\"], tiles[j][\"bbox\"]):\n",
|
| 594 |
+
" parent[find(i)] = find(j)\n",
|
| 595 |
+
"\n",
|
| 596 |
+
"groups = {}\n",
|
| 597 |
+
"for i in range(len(tiles)):\n",
|
| 598 |
+
" groups.setdefault(find(i), []).append(i)\n",
|
| 599 |
+
"\n",
|
| 600 |
+
"sizes = sorted([len(g) for g in groups.values()], reverse=True)\n",
|
| 601 |
+
"print(f\"{len(groups)} overlap groups, largest: {sizes[:5]}\")"
|
| 602 |
+
],
|
| 603 |
+
"execution_count": null,
|
| 604 |
+
"outputs": []
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"cell_type": "code",
|
| 608 |
+
"metadata": {},
|
| 609 |
+
"source": [
|
| 610 |
+
"# Blöcke\n",
|
| 611 |
+
"B = config[\"BLOCK_DEG\"]\n",
|
| 612 |
+
"\n",
|
| 613 |
+
"def block_of(idxs):\n",
|
| 614 |
+
" lon = np.mean([(tiles[i][\"bbox\"][0] + tiles[i][\"bbox\"][2]) / 2 for i in idxs])\n",
|
| 615 |
+
" lat = np.mean([(tiles[i][\"bbox\"][1] + tiles[i][\"bbox\"][3]) / 2 for i in idxs])\n",
|
| 616 |
+
" return (int(math.floor(lon / B)), int(math.floor(lat / B)))\n",
|
| 617 |
+
"\n",
|
| 618 |
+
"block_groups = {}\n",
|
| 619 |
+
"for g, idxs in groups.items():\n",
|
| 620 |
+
" block_groups.setdefault(block_of(idxs), []).append(g)\n",
|
| 621 |
+
"\n",
|
| 622 |
+
"block_tiles = {b: sum(len(groups[g]) for g in gs) for b, gs in block_groups.items()}\n",
|
| 623 |
+
"city_blocks = {b for b, gs in block_groups.items()\n",
|
| 624 |
+
" if any(tiles[i][\"source\"] == \"city\" for g in gs for i in groups[g])}\n",
|
| 625 |
+
"\n",
|
| 626 |
+
"# Greedy: Block in den Split mit dem größten relativen Defizit\n",
|
| 627 |
+
"fractions = config[\"SPLIT_FRACTIONS\"]\n",
|
| 628 |
+
"target = {s: f * len(tiles) for s, f in fractions.items()}\n",
|
| 629 |
+
"count = {s: 0 for s in fractions}\n",
|
| 630 |
+
"block_split = {}\n",
|
| 631 |
+
"\n",
|
| 632 |
+
"for b in sorted(city_blocks):\n",
|
| 633 |
+
" block_split[b] = \"test\"\n",
|
| 634 |
+
" count[\"test\"] += block_tiles[b]\n",
|
| 635 |
+
"\n",
|
| 636 |
+
"rest = sorted(set(block_groups) - city_blocks)\n",
|
| 637 |
+
"np.random.default_rng(config[\"SEED\"]).shuffle(rest)\n",
|
| 638 |
+
"for b in rest:\n",
|
| 639 |
+
" s = max(fractions, key=lambda s: (target[s] - count[s]) / target[s])\n",
|
| 640 |
+
" block_split[b] = s\n",
|
| 641 |
+
" count[s] += block_tiles[b]\n",
|
| 642 |
+
"\n",
|
| 643 |
+
"for g, idxs in groups.items():\n",
|
| 644 |
+
" b = block_of(idxs)\n",
|
| 645 |
+
" for i in idxs:\n",
|
| 646 |
+
" tiles[i][\"split\"] = block_split[b]\n",
|
| 647 |
+
" tiles[i][\"block\"] = f\"{b[0]}_{b[1]}\"\n",
|
| 648 |
+
" tiles[i][\"group\"] = int(g)\n",
|
| 649 |
+
"\n",
|
| 650 |
+
"print(f\"{len(block_groups)} blocks ({len(city_blocks)} city blocks → test)\")\n",
|
| 651 |
+
"for s in fractions:\n",
|
| 652 |
+
" print(f\"{s:10s}: {count[s]:4d} tiles ({count[s] / len(tiles):.1%})\")"
|
| 653 |
+
],
|
| 654 |
+
"execution_count": null,
|
| 655 |
+
"outputs": []
|
| 656 |
+
},
|
| 657 |
+
{
|
| 658 |
+
"cell_type": "code",
|
| 659 |
+
"metadata": {},
|
| 660 |
+
"source": [
|
| 661 |
+
"# Check: keine Überlappung zwischen Splits\n",
|
| 662 |
+
"n_cross = 0\n",
|
| 663 |
+
"for i in range(len(tiles)):\n",
|
| 664 |
+
" for j in range(i + 1, len(tiles)):\n",
|
| 665 |
+
" if tiles[i][\"split\"] != tiles[j][\"split\"] and bbox_overlap(tiles[i][\"bbox\"], tiles[j][\"bbox\"]):\n",
|
| 666 |
+
" n_cross += 1\n",
|
| 667 |
+
"assert n_cross == 0, f\"{n_cross} overlapping tile pairs across splits\"\n",
|
| 668 |
+
"assert all(t[\"split\"] == \"test\" for t in tiles if t[\"source\"] == \"city\")\n",
|
| 669 |
+
"print(\"OK: no pixel is shared between train, validation and test.\")"
|
| 670 |
+
],
|
| 671 |
+
"execution_count": null,
|
| 672 |
+
"outputs": []
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"cell_type": "code",
|
| 676 |
+
"metadata": {},
|
| 677 |
+
"source": [
|
| 678 |
+
"tiles_df = pd.DataFrame([{\n",
|
| 679 |
+
" \"tile_id\": t[\"tile_id\"], \"split\": t[\"split\"], \"source\": t[\"source\"], \"aoi_name\": t[\"aoi_name\"],\n",
|
| 680 |
+
" \"lon_min\": t[\"bbox\"][0], \"lat_min\": t[\"bbox\"][1], \"lon_max\": t[\"bbox\"][2], \"lat_max\": t[\"bbox\"][3],\n",
|
| 681 |
+
" \"block\": t[\"block\"], \"group\": t[\"group\"],\n",
|
| 682 |
+
"} for t in tiles])\n",
|
| 683 |
+
"tiles_df.to_csv(SPLITS_PATH, index=False)\n",
|
| 684 |
+
"print(f\"Saved {SPLITS_PATH}\")\n",
|
| 685 |
+
"tiles_df.groupby([\"split\", \"source\"]).size()"
|
| 686 |
+
],
|
| 687 |
+
"execution_count": null,
|
| 688 |
+
"outputs": []
|
| 689 |
+
},
|
| 690 |
+
{
|
| 691 |
+
"cell_type": "markdown",
|
| 692 |
+
"metadata": {},
|
| 693 |
+
"source": [
|
| 694 |
+
"# 4. Patches"
|
| 695 |
+
]
|
| 696 |
+
},
|
| 697 |
+
{
|
| 698 |
+
"cell_type": "code",
|
| 699 |
+
"metadata": {},
|
| 700 |
+
"source": [
|
| 701 |
+
"PATCH_SIZE_HR = config[\"PATCH_SIZE\"]\n",
|
| 702 |
+
"UPSCALE = config[\"UPSCALE\"]\n",
|
| 703 |
+
"PATCH_SIZE_LR = config[\"LR_PATCH_SIZE\"]"
|
| 704 |
+
],
|
| 705 |
+
"execution_count": null,
|
| 706 |
+
"outputs": []
|
| 707 |
+
},
|
| 708 |
+
{
|
| 709 |
+
"cell_type": "code",
|
| 710 |
+
"metadata": {},
|
| 711 |
+
"source": [
|
| 712 |
+
"def normalize_for_display(patch):\n",
|
| 713 |
+
" p_low = np.percentile(patch, 1)\n",
|
| 714 |
+
" p_high = np.percentile(patch, 99)\n",
|
| 715 |
+
" norm = (patch - p_low) / (p_high - p_low + 1e-8)\n",
|
| 716 |
+
" return np.clip(norm, 0, 1)"
|
| 717 |
+
],
|
| 718 |
+
"execution_count": null,
|
| 719 |
+
"outputs": []
|
| 720 |
+
},
|
| 721 |
+
{
|
| 722 |
+
"cell_type": "code",
|
| 723 |
+
"metadata": {},
|
| 724 |
+
"source": [
|
| 725 |
+
"INTERP_NAMES = {cv2.INTER_CUBIC: \"bicubic\", cv2.INTER_LINEAR: \"bilinear\", cv2.INTER_AREA: \"area\"}\n",
|
| 726 |
+
"\n",
|
| 727 |
+
"def downsample_realistic(hr, scale, return_params=False):\n",
|
| 728 |
+
" # HWC für OpenCV\n",
|
| 729 |
+
" img = hr.transpose(1, 2, 0)\n",
|
| 730 |
+
"\n",
|
| 731 |
+
" h, w = img.shape[:2]\n",
|
| 732 |
+
"\n",
|
| 733 |
+
" # Zufallsfilter\n",
|
| 734 |
+
" filter_type = random.choice([\"gaussian\", \"motion\", \"defocus\"])\n",
|
| 735 |
+
" sigma = 0.0\n",
|
| 736 |
+
"\n",
|
| 737 |
+
" if filter_type == \"gaussian\":\n",
|
| 738 |
+
" # Isotropischer Gauss blur mit zufall kernel + sigma (SIGMA)\n",
|
| 739 |
+
" k = random.choice([3, 5, 7])\n",
|
| 740 |
+
" sigma = random.uniform(0.5, 1.5)\n",
|
| 741 |
+
" img_blur = cv2.GaussianBlur(img, (k, k), sigmaX=sigma)\n",
|
| 742 |
+
"\n",
|
| 743 |
+
" elif filter_type == \"motion\":\n",
|
| 744 |
+
" # Anisotropischer motion blur kernel\n",
|
| 745 |
+
" k = random.choice([5, 7, 9])\n",
|
| 746 |
+
" kernel = np.zeros((k, k), dtype=np.float32)\n",
|
| 747 |
+
"\n",
|
| 748 |
+
" # Zufall horizontal oder vertikal\n",
|
| 749 |
+
" if random.random() < 0.5:\n",
|
| 750 |
+
" kernel[k // 2, :] = 1.0\n",
|
| 751 |
+
" filter_type = \"motion_h\"\n",
|
| 752 |
+
" else:\n",
|
| 753 |
+
" kernel[:, k // 2] = 1.0\n",
|
| 754 |
+
" filter_type = \"motion_v\"\n",
|
| 755 |
+
"\n",
|
| 756 |
+
" kernel /= k\n",
|
| 757 |
+
" img_blur = cv2.filter2D(img, -1, kernel)\n",
|
| 758 |
+
"\n",
|
| 759 |
+
" elif filter_type == \"defocus\":\n",
|
| 760 |
+
" # Approximierter defocus blur mit kreiskernel\n",
|
| 761 |
+
" k = random.choice([5, 7, 9])\n",
|
| 762 |
+
" kernel = np.zeros((k, k), np.float32)\n",
|
| 763 |
+
" center = k // 2\n",
|
| 764 |
+
"\n",
|
| 765 |
+
" for i in range(k):\n",
|
| 766 |
+
" for j in range(k):\n",
|
| 767 |
+
" if (i - center) ** 2 + (j - center) ** 2 <= center ** 2:\n",
|
| 768 |
+
" kernel[i, j] = 1\n",
|
| 769 |
+
"\n",
|
| 770 |
+
" kernel /= np.sum(kernel)\n",
|
| 771 |
+
" img_blur = cv2.filter2D(img, -1, kernel)\n",
|
| 772 |
+
"\n",
|
| 773 |
+
" # Zufallsinterpolation\n",
|
| 774 |
+
" interpolation = random.choice([\n",
|
| 775 |
+
" cv2.INTER_CUBIC, # bikubisch\n",
|
| 776 |
+
" cv2.INTER_LINEAR, # bilinear\n",
|
| 777 |
+
" cv2.INTER_AREA # flächenbasiert\n",
|
| 778 |
+
" ])\n",
|
| 779 |
+
"\n",
|
| 780 |
+
" lr = cv2.resize(\n",
|
| 781 |
+
" img_blur,\n",
|
| 782 |
+
" (w // scale, h // scale),\n",
|
| 783 |
+
" interpolation=interpolation\n",
|
| 784 |
+
" )\n",
|
| 785 |
+
"\n",
|
| 786 |
+
" # Zurück zu CHW\n",
|
| 787 |
+
" lr = lr.transpose(2, 0, 1).astype(np.float32)\n",
|
| 788 |
+
" if not return_params:\n",
|
| 789 |
+
" return lr\n",
|
| 790 |
+
" return lr, {\"blur\": filter_type, \"blur_kernel\": int(k), \"blur_sigma\": float(sigma),\n",
|
| 791 |
+
" \"interpolation\": INTERP_NAMES[interpolation]}"
|
| 792 |
+
],
|
| 793 |
+
"execution_count": null,
|
| 794 |
+
"outputs": []
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"cell_type": "code",
|
| 798 |
+
"metadata": {},
|
| 799 |
+
"source": [
|
| 800 |
+
"def extract_patches_random(aoi_data, n_patches):\n",
|
| 801 |
+
" rgb, ndvi, scl = aoi_data\n",
|
| 802 |
+
" H, W = rgb.shape[1:]\n",
|
| 803 |
+
"\n",
|
| 804 |
+
" extracted = []\n",
|
| 805 |
+
" attempts = 0\n",
|
| 806 |
+
" max_attempts = n_patches * 10\n",
|
| 807 |
+
"\n",
|
| 808 |
+
" while len(extracted) < n_patches and attempts < max_attempts:\n",
|
| 809 |
+
" y = np.random.randint(0, H - PATCH_SIZE_HR)\n",
|
| 810 |
+
" x = np.random.randint(0, W - PATCH_SIZE_HR)\n",
|
| 811 |
+
"\n",
|
| 812 |
+
" patch_rgb = rgb[:, y:y+PATCH_SIZE_HR, x:x+PATCH_SIZE_HR]\n",
|
| 813 |
+
" patch_ndvi = ndvi[y:y+PATCH_SIZE_HR, x:x+PATCH_SIZE_HR]\n",
|
| 814 |
+
" patch_scl = scl[y:y+PATCH_SIZE_HR, x:x+PATCH_SIZE_HR]\n",
|
| 815 |
+
"\n",
|
| 816 |
+
" cloud_ratio = np.isin(\n",
|
| 817 |
+
" patch_scl, [3,7,8,9,10]\n",
|
| 818 |
+
" ).mean()\n",
|
| 819 |
+
"\n",
|
| 820 |
+
" if cloud_ratio > config[\"MAX_CLOUD_PATCH\"]:\n",
|
| 821 |
+
" attempts += 1\n",
|
| 822 |
+
" continue\n",
|
| 823 |
+
"\n",
|
| 824 |
+
" hr = normalize_for_display(patch_rgb / 10000.0)\n",
|
| 825 |
+
" lr, deg = downsample_realistic(hr, UPSCALE, return_params=True)\n",
|
| 826 |
+
"\n",
|
| 827 |
+
" extracted.append({\n",
|
| 828 |
+
" \"lr\": lr, \"hr\": hr, \"ndvi\": patch_ndvi, \"scl\": patch_scl, \"rgb\": patch_rgb,\n",
|
| 829 |
+
" \"y\": int(y), \"x\": int(x), \"cloud_ratio\": float(cloud_ratio),\n",
|
| 830 |
+
" \"p_low\": float(np.percentile(patch_rgb, 1)), \"p_high\": float(np.percentile(patch_rgb, 99)),\n",
|
| 831 |
+
" **deg,\n",
|
| 832 |
+
" })\n",
|
| 833 |
+
" attempts += 1\n",
|
| 834 |
+
"\n",
|
| 835 |
+
" return extracted"
|
| 836 |
+
],
|
| 837 |
+
"execution_count": null,
|
| 838 |
+
"outputs": []
|
| 839 |
+
},
|
| 840 |
+
{
|
| 841 |
+
"cell_type": "markdown",
|
| 842 |
+
"metadata": {},
|
| 843 |
+
"source": [
|
| 844 |
+
"### Beispiel"
|
| 845 |
+
]
|
| 846 |
+
},
|
| 847 |
+
{
|
| 848 |
+
"cell_type": "code",
|
| 849 |
+
"metadata": {},
|
| 850 |
+
"source": [
|
| 851 |
+
"data = np.load(tiles[0][\"path\"])\n",
|
| 852 |
+
"np.random.seed(0); random.seed(0)\n",
|
| 853 |
+
"ex = extract_patches_random((data[\"rgb\"], data[\"ndvi\"], data[\"scl\"]), 4)\n",
|
| 854 |
+
"\n",
|
| 855 |
+
"plt.figure(figsize=(12, 9))\n",
|
| 856 |
+
"for i, p in enumerate(ex):\n",
|
| 857 |
+
" plt.subplot(3, 4, i + 1); plt.imshow(p[\"hr\"].transpose(1, 2, 0)); plt.title(\"HR\"); plt.axis(\"off\")\n",
|
| 858 |
+
" plt.subplot(3, 4, i + 5); plt.imshow(np.clip(p[\"lr\"], 0, 1).transpose(1, 2, 0)); plt.title(f\"LR ({p['blur']}, {p['interpolation']})\"); plt.axis(\"off\")\n",
|
| 859 |
+
" plt.subplot(3, 4, i + 9); plt.imshow(p[\"ndvi\"], cmap=\"RdYlGn\", vmin=-1, vmax=1); plt.title(\"NDVI\"); plt.axis(\"off\")\n",
|
| 860 |
+
"plt.tight_layout(); plt.show()"
|
| 861 |
+
],
|
| 862 |
+
"execution_count": null,
|
| 863 |
+
"outputs": []
|
| 864 |
+
},
|
| 865 |
+
{
|
| 866 |
+
"cell_type": "markdown",
|
| 867 |
+
"metadata": {},
|
| 868 |
+
"source": [
|
| 869 |
+
"### Hugging Face Schema"
|
| 870 |
+
]
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"cell_type": "code",
|
| 874 |
+
"metadata": {},
|
| 875 |
+
"source": [
|
| 876 |
+
"P = PATCH_SIZE_HR\n",
|
| 877 |
+
"\n",
|
| 878 |
+
"features = Features({\n",
|
| 879 |
+
" \"patch_id\": Value(\"string\"),\n",
|
| 880 |
+
" \"tile_id\": Value(\"string\"),\n",
|
| 881 |
+
" \"source\": Value(\"string\"),\n",
|
| 882 |
+
" \"aoi_name\": Value(\"string\"),\n",
|
| 883 |
+
" \"lr\": HFImage(),\n",
|
| 884 |
+
" \"hr\": HFImage(),\n",
|
| 885 |
+
" \"ndvi\": Array2D((P, P), \"float32\"),\n",
|
| 886 |
+
" \"scl\": Array2D((P, P), \"uint8\"),\n",
|
| 887 |
+
" \"reflectance\": Array3D((3, P, P), \"uint16\"),\n",
|
| 888 |
+
" \"lon\": Value(\"float64\"),\n",
|
| 889 |
+
" \"lat\": Value(\"float64\"),\n",
|
| 890 |
+
" \"lon_min\": Value(\"float64\"),\n",
|
| 891 |
+
" \"lat_min\": Value(\"float64\"),\n",
|
| 892 |
+
" \"lon_max\": Value(\"float64\"),\n",
|
| 893 |
+
" \"lat_max\": Value(\"float64\"),\n",
|
| 894 |
+
" \"crop_y\": Value(\"int32\"),\n",
|
| 895 |
+
" \"crop_x\": Value(\"int32\"),\n",
|
| 896 |
+
" \"cloud_fraction\": Value(\"float32\"),\n",
|
| 897 |
+
" \"water_fraction\": Value(\"float32\"),\n",
|
| 898 |
+
" \"stretch_low\": Value(\"float32\"),\n",
|
| 899 |
+
" \"stretch_high\": Value(\"float32\"),\n",
|
| 900 |
+
" \"blur\": Value(\"string\"),\n",
|
| 901 |
+
" \"blur_kernel\": Value(\"int32\"),\n",
|
| 902 |
+
" \"blur_sigma\": Value(\"float32\"),\n",
|
| 903 |
+
" \"interpolation\": Value(\"string\"),\n",
|
| 904 |
+
" \"time_from\": Value(\"string\"),\n",
|
| 905 |
+
" \"time_to\": Value(\"string\"),\n",
|
| 906 |
+
"})\n",
|
| 907 |
+
"\n",
|
| 908 |
+
"def to_png(arr_chw):\n",
|
| 909 |
+
" return Image.fromarray(np.round(np.clip(arr_chw, 0, 1).transpose(1, 2, 0) * 255).astype(np.uint8))\n",
|
| 910 |
+
"\n",
|
| 911 |
+
"def tile_rows(t, tile_index):\n",
|
| 912 |
+
" data = np.load(t[\"path\"])\n",
|
| 913 |
+
" rgb, ndvi, scl = data[\"rgb\"], data[\"ndvi\"], data[\"scl\"]\n",
|
| 914 |
+
"\n",
|
| 915 |
+
" # Reproduzierbar pro Kachel\n",
|
| 916 |
+
" seed = config[\"SEED\"] * 100003 + tile_index\n",
|
| 917 |
+
" np.random.seed(seed); random.seed(seed)\n",
|
| 918 |
+
" patches = extract_patches_random((rgb, ndvi, scl), n_patches=config[\"PATCHES_PER_TILE\"])\n",
|
| 919 |
+
"\n",
|
| 920 |
+
" lon_min, lat_min, lon_max, lat_max = t[\"bbox\"]\n",
|
| 921 |
+
" H, W = rgb.shape[1:]\n",
|
| 922 |
+
" dlon, dlat = (lon_max - lon_min) / W, (lat_max - lat_min) / H\n",
|
| 923 |
+
" t_from = str(data[\"time_from\"]) if \"time_from\" in data.files else (config[\"TIME_FROM\"] if t[\"source\"] == \"random\" else \"\")\n",
|
| 924 |
+
" t_to = str(data[\"time_to\"]) if \"time_to\" in data.files else (config[\"TIME_TO\"] if t[\"source\"] == \"random\" else \"\")\n",
|
| 925 |
+
"\n",
|
| 926 |
+
" for k, p in enumerate(patches):\n",
|
| 927 |
+
" # Zeile 0 = Norden\n",
|
| 928 |
+
" p_lon_min = lon_min + p[\"x\"] * dlon\n",
|
| 929 |
+
" p_lon_max = p_lon_min + P * dlon\n",
|
| 930 |
+
" p_lat_max = lat_max - p[\"y\"] * dlat\n",
|
| 931 |
+
" p_lat_min = p_lat_max - P * dlat\n",
|
| 932 |
+
" yield {\n",
|
| 933 |
+
" \"patch_id\": f\"{t['tile_id']}_p{k:02d}\",\n",
|
| 934 |
+
" \"tile_id\": t[\"tile_id\"],\n",
|
| 935 |
+
" \"source\": t[\"source\"],\n",
|
| 936 |
+
" \"aoi_name\": t[\"aoi_name\"],\n",
|
| 937 |
+
" \"lr\": to_png(p[\"lr\"]),\n",
|
| 938 |
+
" \"hr\": to_png(p[\"hr\"]),\n",
|
| 939 |
+
" \"ndvi\": np.clip(p[\"ndvi\"], -1, 1).astype(np.float32),\n",
|
| 940 |
+
" \"scl\": p[\"scl\"].astype(np.uint8),\n",
|
| 941 |
+
" \"reflectance\": np.round(p[\"rgb\"] * 10000).clip(0, 65535).astype(np.uint16),\n",
|
| 942 |
+
" \"lon\": (p_lon_min + p_lon_max) / 2,\n",
|
| 943 |
+
" \"lat\": (p_lat_min + p_lat_max) / 2,\n",
|
| 944 |
+
" \"lon_min\": p_lon_min, \"lat_min\": p_lat_min, \"lon_max\": p_lon_max, \"lat_max\": p_lat_max,\n",
|
| 945 |
+
" \"crop_y\": p[\"y\"], \"crop_x\": p[\"x\"],\n",
|
| 946 |
+
" \"cloud_fraction\": p[\"cloud_ratio\"],\n",
|
| 947 |
+
" \"water_fraction\": float((p[\"scl\"] == 6).mean()),\n",
|
| 948 |
+
" \"stretch_low\": p[\"p_low\"], \"stretch_high\": p[\"p_high\"],\n",
|
| 949 |
+
" \"blur\": p[\"blur\"], \"blur_kernel\": p[\"blur_kernel\"], \"blur_sigma\": p[\"blur_sigma\"],\n",
|
| 950 |
+
" \"interpolation\": p[\"interpolation\"],\n",
|
| 951 |
+
" \"time_from\": t_from, \"time_to\": t_to,\n",
|
| 952 |
+
" }"
|
| 953 |
+
],
|
| 954 |
+
"execution_count": null,
|
| 955 |
+
"outputs": []
|
| 956 |
+
},
|
| 957 |
+
{
|
| 958 |
+
"cell_type": "markdown",
|
| 959 |
+
"metadata": {},
|
| 960 |
+
"source": [
|
| 961 |
+
"### Patches erzeugen und als Parquet schreiben\n",
|
| 962 |
+
"\n",
|
| 963 |
+
"Shards ≤ ~500 MB wie `data/train-00000-of-00012.parquet`."
|
| 964 |
+
]
|
| 965 |
+
},
|
| 966 |
+
{
|
| 967 |
+
"cell_type": "code",
|
| 968 |
+
"metadata": {},
|
| 969 |
+
"source": [
|
| 970 |
+
"SHARD_ROWS = 800\n",
|
| 971 |
+
"\n",
|
| 972 |
+
"tile_index = {t[\"tile_id\"]: i for i, t in enumerate(tiles)}\n",
|
| 973 |
+
"split_ds = {}\n",
|
| 974 |
+
"\n",
|
| 975 |
+
"for split in [\"train\", \"validation\", \"test\"]:\n",
|
| 976 |
+
" split_tiles = [t for t in tiles if t[\"split\"] == split]\n",
|
| 977 |
+
"\n",
|
| 978 |
+
" def gen(split_tiles=split_tiles):\n",
|
| 979 |
+
" for t in tqdm(split_tiles, desc=f\"Patches {split}\", ncols=120):\n",
|
| 980 |
+
" yield from tile_rows(t, tile_index[t[\"tile_id\"]])\n",
|
| 981 |
+
"\n",
|
| 982 |
+
" ds = Dataset.from_generator(gen, features=features, cache_dir=LOCAL_CACHE)\n",
|
| 983 |
+
" ds = ds.shuffle(seed=config[\"SEED\"]).flatten_indices()\n",
|
| 984 |
+
" split_ds[split] = ds\n",
|
| 985 |
+
" print(f\"{split}: {len(ds)} patches from {len(split_tiles)} tiles\")"
|
| 986 |
+
],
|
| 987 |
+
"execution_count": null,
|
| 988 |
+
"outputs": []
|
| 989 |
+
},
|
| 990 |
+
{
|
| 991 |
+
"cell_type": "code",
|
| 992 |
+
"metadata": {},
|
| 993 |
+
"source": [
|
| 994 |
+
"# Alte Shards entfernen (sonst Mischung aus Läufen)\n",
|
| 995 |
+
"shutil.rmtree(os.path.join(LOCAL_OUT, \"data\"), ignore_errors=True)\n",
|
| 996 |
+
"\n",
|
| 997 |
+
"for split, ds in split_ds.items():\n",
|
| 998 |
+
" n_shards = max(1, math.ceil(len(ds) / SHARD_ROWS))\n",
|
| 999 |
+
" for i in range(n_shards):\n",
|
| 1000 |
+
" out = os.path.join(LOCAL_OUT, \"data\", f\"{split}-{i:05d}-of-{n_shards:05d}.parquet\")\n",
|
| 1001 |
+
" os.makedirs(os.path.dirname(out), exist_ok=True)\n",
|
| 1002 |
+
" ds.shard(n_shards, i, contiguous=True).to_parquet(out)\n",
|
| 1003 |
+
" print(f\"{split}: {n_shards} shards\")\n",
|
| 1004 |
+
"\n",
|
| 1005 |
+
"!du -sh {LOCAL_OUT}/data"
|
| 1006 |
+
],
|
| 1007 |
+
"execution_count": null,
|
| 1008 |
+
"outputs": []
|
| 1009 |
+
},
|
| 1010 |
+
{
|
| 1011 |
+
"cell_type": "markdown",
|
| 1012 |
+
"metadata": {},
|
| 1013 |
+
"source": [
|
| 1014 |
+
"### Auf Drive kopieren (ersetzt alten Stand)"
|
| 1015 |
+
]
|
| 1016 |
+
},
|
| 1017 |
+
{
|
| 1018 |
+
"cell_type": "code",
|
| 1019 |
+
"metadata": {},
|
| 1020 |
+
"source": [
|
| 1021 |
+
"for f in glob(os.path.join(HF_DATA_DIR, \"*.parquet\")):\n",
|
| 1022 |
+
" os.remove(f)\n",
|
| 1023 |
+
"for f in sorted(glob(os.path.join(LOCAL_OUT, \"data\", \"*.parquet\"))):\n",
|
| 1024 |
+
" shutil.copy(f, HF_DATA_DIR)\n",
|
| 1025 |
+
"\n",
|
| 1026 |
+
"tiles_df.to_csv(os.path.join(HF_META_DIR, \"tiles.csv\"), index=False)\n",
|
| 1027 |
+
"print(sorted(os.listdir(HF_DATA_DIR)))"
|
| 1028 |
+
],
|
| 1029 |
+
"execution_count": null,
|
| 1030 |
+
"outputs": []
|
| 1031 |
+
},
|
| 1032 |
+
{
|
| 1033 |
+
"cell_type": "markdown",
|
| 1034 |
+
"metadata": {},
|
| 1035 |
+
"source": [
|
| 1036 |
+
"# 5. Checks"
|
| 1037 |
+
]
|
| 1038 |
+
},
|
| 1039 |
+
{
|
| 1040 |
+
"cell_type": "code",
|
| 1041 |
+
"metadata": {},
|
| 1042 |
+
"source": [
|
| 1043 |
+
"from datasets import load_dataset\n",
|
| 1044 |
+
"\n",
|
| 1045 |
+
"hf = load_dataset(\"parquet\", data_dir=HF_DATA_DIR, cache_dir=LOCAL_CACHE)\n",
|
| 1046 |
+
"print(hf)"
|
| 1047 |
+
],
|
| 1048 |
+
"execution_count": null,
|
| 1049 |
+
"outputs": []
|
| 1050 |
+
},
|
| 1051 |
+
{
|
| 1052 |
+
"cell_type": "code",
|
| 1053 |
+
"metadata": {},
|
| 1054 |
+
"source": [
|
| 1055 |
+
"# Kachel-IDs disjunkt?\n",
|
| 1056 |
+
"ids = {s: set(hf[s][\"tile_id\"]) for s in hf}\n",
|
| 1057 |
+
"assert not (ids[\"train\"] & ids[\"validation\"]), \"train/val leak\"\n",
|
| 1058 |
+
"assert not (ids[\"train\"] & ids[\"test\"]), \"train/test leak\"\n",
|
| 1059 |
+
"assert not (ids[\"validation\"] & ids[\"test\"]), \"val/test leak\"\n",
|
| 1060 |
+
"\n",
|
| 1061 |
+
"# Patch-IDs eindeutig?\n",
|
| 1062 |
+
"all_ids = sum((list(hf[s][\"patch_id\"]) for s in hf), [])\n",
|
| 1063 |
+
"assert len(all_ids) == len(set(all_ids))\n",
|
| 1064 |
+
"\n",
|
| 1065 |
+
"# Geometrisch disjunkt (Patch-Ebene)?\n",
|
| 1066 |
+
"boxes = {s: hf[s].select_columns([\"lon_min\", \"lat_min\", \"lon_max\", \"lat_max\"]).to_pandas().values for s in hf}\n",
|
| 1067 |
+
"for a in hf:\n",
|
| 1068 |
+
" for b in hf:\n",
|
| 1069 |
+
" if a < b:\n",
|
| 1070 |
+
" A, Bx = boxes[a], boxes[b]\n",
|
| 1071 |
+
" ov = ((A[:, None, 0] < Bx[None, :, 2]) & (Bx[None, :, 0] < A[:, None, 2]) &\n",
|
| 1072 |
+
" (A[:, None, 1] < Bx[None, :, 3]) & (Bx[None, :, 1] < A[:, None, 3])).sum()\n",
|
| 1073 |
+
" assert ov == 0, f\"{a}/{b}: {ov} overlapping patches\"\n",
|
| 1074 |
+
"\n",
|
| 1075 |
+
"x = hf[\"train\"][0]\n",
|
| 1076 |
+
"print(x[\"lr\"].size, x[\"hr\"].size, np.array(x[\"ndvi\"]).shape, np.array(x[\"reflectance\"]).shape)\n",
|
| 1077 |
+
"print(\"OK: splits disjoint by tile, patch id and footprint.\")"
|
| 1078 |
+
],
|
| 1079 |
+
"execution_count": null,
|
| 1080 |
+
"outputs": []
|
| 1081 |
+
},
|
| 1082 |
+
{
|
| 1083 |
+
"cell_type": "markdown",
|
| 1084 |
+
"metadata": {},
|
| 1085 |
+
"source": [
|
| 1086 |
+
"# 6. Statistiken und Abbildungen für die Dataset Card"
|
| 1087 |
+
]
|
| 1088 |
+
},
|
| 1089 |
+
{
|
| 1090 |
+
"cell_type": "code",
|
| 1091 |
+
"metadata": {},
|
| 1092 |
+
"source": [
|
| 1093 |
+
"SPLITS = [\"train\", \"validation\", \"test\"]\n",
|
| 1094 |
+
"SPLIT_COLORS = {\"train\": \"#2a78d6\", \"validation\": \"#eb6834\", \"test\": \"#1baf7a\"}\n",
|
| 1095 |
+
"SURFACE, INK, INK2, GRID = \"#fcfcfb\", \"#0b0b0b\", \"#52514e\", \"#e4e3df\"\n",
|
| 1096 |
+
"\n",
|
| 1097 |
+
"plt.rcParams.update({\n",
|
| 1098 |
+
" \"figure.facecolor\": SURFACE, \"axes.facecolor\": SURFACE, \"savefig.facecolor\": SURFACE,\n",
|
| 1099 |
+
" \"axes.edgecolor\": GRID, \"axes.labelcolor\": INK2, \"xtick.color\": INK2, \"ytick.color\": INK2,\n",
|
| 1100 |
+
" \"text.color\": INK, \"axes.grid\": True, \"grid.color\": GRID, \"grid.linewidth\": 0.6,\n",
|
| 1101 |
+
" \"axes.spines.top\": False, \"axes.spines.right\": False, \"font.size\": 11,\n",
|
| 1102 |
+
" \"axes.titleweight\": \"bold\", \"axes.titlesize\": 13, \"legend.frameon\": False,\n",
|
| 1103 |
+
"})\n",
|
| 1104 |
+
"\n",
|
| 1105 |
+
"def save_fig(fig, name):\n",
|
| 1106 |
+
" path = os.path.join(HF_IMAGES_DIR, name)\n",
|
| 1107 |
+
" fig.savefig(path, dpi=160, bbox_inches=\"tight\")\n",
|
| 1108 |
+
" print(\"saved\", path)\n",
|
| 1109 |
+
"\n",
|
| 1110 |
+
"meta = {s: hf[s].select_columns([c for c in hf[s].column_names if c not in\n",
|
| 1111 |
+
" (\"lr\", \"hr\", \"ndvi\", \"scl\", \"reflectance\")]).to_pandas() for s in SPLITS}"
|
| 1112 |
+
],
|
| 1113 |
+
"execution_count": null,
|
| 1114 |
+
"outputs": []
|
| 1115 |
+
},
|
| 1116 |
+
{
|
| 1117 |
+
"cell_type": "markdown",
|
| 1118 |
+
"metadata": {},
|
| 1119 |
+
"source": [
|
| 1120 |
+
"### LR/HR-Paare (Test-Split)"
|
| 1121 |
+
]
|
| 1122 |
+
},
|
| 1123 |
+
{
|
| 1124 |
+
"cell_type": "code",
|
| 1125 |
+
"metadata": {},
|
| 1126 |
+
"source": [
|
| 1127 |
+
"plt.rcdefaults()\n",
|
| 1128 |
+
"plt.rcParams.update({\"figure.facecolor\": \"white\", \"savefig.facecolor\": \"white\", \"font.size\": 12})\n",
|
| 1129 |
+
"\n",
|
| 1130 |
+
"def plain(axs):\n",
|
| 1131 |
+
" for a in np.ravel(axs):\n",
|
| 1132 |
+
" a.set_xticks([]); a.set_yticks([])\n",
|
| 1133 |
+
" for sp in a.spines.values():\n",
|
| 1134 |
+
" sp.set_visible(False)\n",
|
| 1135 |
+
"\n",
|
| 1136 |
+
"# Ausgewählte Test-Patches für die Dataset Card\n",
|
| 1137 |
+
"PAIR_IDS = ['aoi_0424_p03', 'custom_0004_p02', 'aoi_0176_p10', 'aoi_0374_p02', 'custom_0002_p00', 'aoi_0261_p02']\n",
|
| 1138 |
+
"DEG_ID = 'custom_0006_p07'\n",
|
| 1139 |
+
"pid = list(meta[\"test\"].patch_id)\n",
|
| 1140 |
+
"pair_idx = [pid.index(p) for p in PAIR_IDS]\n",
|
| 1141 |
+
"\n",
|
| 1142 |
+
"fig, axs = plt.subplots(2, len(pair_idx), figsize=(3.1 * len(pair_idx), 6.6))\n",
|
| 1143 |
+
"for c, i in enumerate(pair_idx):\n",
|
| 1144 |
+
" r = hf[\"test\"][i]\n",
|
| 1145 |
+
" axs[0, c].imshow(r[\"lr\"], interpolation=\"nearest\")\n",
|
| 1146 |
+
" axs[1, c].imshow(r[\"hr\"])\n",
|
| 1147 |
+
" axs[0, c].set_title(r[\"aoi_name\"] or f\"{r['lat']:.2f}°N {r['lon']:.2f}°E\", fontsize=12)\n",
|
| 1148 |
+
"axs[0, 0].set_ylabel(\"LR input (64×64)\", fontsize=13)\n",
|
| 1149 |
+
"axs[1, 0].set_ylabel(\"HR target (256×256)\", fontsize=13)\n",
|
| 1150 |
+
"plain(axs)\n",
|
| 1151 |
+
"plt.tight_layout(pad=0.4)\n",
|
| 1152 |
+
"save_fig(fig, \"psrgan_s2_pairs.png\"); plt.show()"
|
| 1153 |
+
],
|
| 1154 |
+
"execution_count": null,
|
| 1155 |
+
"outputs": []
|
| 1156 |
+
},
|
| 1157 |
+
{
|
| 1158 |
+
"cell_type": "markdown",
|
| 1159 |
+
"metadata": {},
|
| 1160 |
+
"source": [
|
| 1161 |
+
"### Land Cover (SCL) je Split (für README)"
|
| 1162 |
+
]
|
| 1163 |
+
},
|
| 1164 |
+
{
|
| 1165 |
+
"cell_type": "code",
|
| 1166 |
+
"metadata": {},
|
| 1167 |
+
"source": [
|
| 1168 |
+
"SCL_NAMES = {2: \"Dark area\", 3: \"Cloud shadow\", 4: \"Vegetation\", 5: \"Not vegetated\", 6: \"Water\",\n",
|
| 1169 |
+
" 7: \"Unclassified\", 8: \"Cloud (medium)\", 9: \"Cloud (high)\", 10: \"Thin cirrus\", 11: \"Snow/ice\"}\n",
|
| 1170 |
+
"\n",
|
| 1171 |
+
"scl_frac = {}\n",
|
| 1172 |
+
"for s in SPLITS:\n",
|
| 1173 |
+
" counts = np.zeros(12)\n",
|
| 1174 |
+
" for batch in hf[s].select_columns([\"scl\"]).with_format(\"numpy\").iter(batch_size=256):\n",
|
| 1175 |
+
" counts += np.bincount(batch[\"scl\"].ravel(), minlength=12)[:12]\n",
|
| 1176 |
+
" scl_frac[s] = counts / counts.sum()\n",
|
| 1177 |
+
"\n",
|
| 1178 |
+
"for c in SCL_NAMES:\n",
|
| 1179 |
+
" print(f\"{SCL_NAMES[c]:15s}\", \" \".join(f\"{s}: {scl_frac[s][c]:6.1%}\" for s in SPLITS))"
|
| 1180 |
+
],
|
| 1181 |
+
"execution_count": null,
|
| 1182 |
+
"outputs": []
|
| 1183 |
+
},
|
| 1184 |
+
{
|
| 1185 |
+
"cell_type": "markdown",
|
| 1186 |
+
"metadata": {},
|
| 1187 |
+
"source": [
|
| 1188 |
+
"### NDVI je Split (für README)"
|
| 1189 |
+
]
|
| 1190 |
+
},
|
| 1191 |
+
{
|
| 1192 |
+
"cell_type": "code",
|
| 1193 |
+
"metadata": {},
|
| 1194 |
+
"source": [
|
| 1195 |
+
"bins = np.linspace(-1, 1, 81)\n",
|
| 1196 |
+
"ndvi_stats = {}\n",
|
| 1197 |
+
"for s in SPLITS:\n",
|
| 1198 |
+
" h = np.zeros(len(bins) - 1)\n",
|
| 1199 |
+
" for batch in hf[s].select_columns([\"ndvi\"]).with_format(\"numpy\").iter(batch_size=256):\n",
|
| 1200 |
+
" h += np.histogram(batch[\"ndvi\"].ravel(), bins=bins)[0]\n",
|
| 1201 |
+
" h = h / h.sum() / np.diff(bins)\n",
|
| 1202 |
+
" ndvi_stats[s] = float(((bins[:-1] + bins[1:]) / 2 * h * np.diff(bins)).sum())\n",
|
| 1203 |
+
"print(\"NDVI-Mittel:\", {s: round(v, 3) for s, v in ndvi_stats.items()})"
|
| 1204 |
+
],
|
| 1205 |
+
"execution_count": null,
|
| 1206 |
+
"outputs": []
|
| 1207 |
+
},
|
| 1208 |
+
{
|
| 1209 |
+
"cell_type": "markdown",
|
| 1210 |
+
"metadata": {},
|
| 1211 |
+
"source": [
|
| 1212 |
+
"### Degradations-Pipeline an einem Patch"
|
| 1213 |
+
]
|
| 1214 |
+
},
|
| 1215 |
+
{
|
| 1216 |
+
"cell_type": "code",
|
| 1217 |
+
"metadata": {},
|
| 1218 |
+
"source": [
|
| 1219 |
+
"# Echter downsample_realistic-Aufruf, je Filter ein Beispiel (unten: Ausschnitt vergrößert)\n",
|
| 1220 |
+
"src = pid.index(DEG_ID)\n",
|
| 1221 |
+
"hr_img = np.asarray(hf[\"test\"][src][\"hr\"])[..., :3].astype(np.float32) / 255\n",
|
| 1222 |
+
"targets = [(\"gaussian\", \"bicubic\"), (\"motion_h\", \"bilinear\"), (\"motion_v\", \"area\"), (\"defocus\", \"bicubic\")]\n",
|
| 1223 |
+
"found, seed = {}, 0\n",
|
| 1224 |
+
"while len(found) < len(targets) and seed < 5000:\n",
|
| 1225 |
+
" random.seed(seed)\n",
|
| 1226 |
+
" lr, p = downsample_realistic(hr_img.transpose(2, 0, 1), UPSCALE, return_params=True)\n",
|
| 1227 |
+
" if (p[\"blur\"], p[\"interpolation\"]) in targets:\n",
|
| 1228 |
+
" found.setdefault((p[\"blur\"], p[\"interpolation\"]), (lr, p))\n",
|
| 1229 |
+
" seed += 1\n",
|
| 1230 |
+
"\n",
|
| 1231 |
+
"BLUR_NAMES = {\"gaussian\": \"Gaussian\", \"motion_h\": \"Motion (horizontal)\", \"motion_v\": \"Motion (vertical)\", \"defocus\": \"Defocus\"}\n",
|
| 1232 |
+
"keys = [t for t in targets if t in found]\n",
|
| 1233 |
+
"# Ausschnitt mit den meisten Kanten (64×64 HR bzw. 16×16 LR)\n",
|
| 1234 |
+
"g = hr_img.mean(-1)\n",
|
| 1235 |
+
"gm = np.abs(np.diff(g, axis=0))[:, :-1] + np.abs(np.diff(g, axis=1))[:-1, :]\n",
|
| 1236 |
+
"cy, cx = max(((y, x) for y in range(0, 193, 16) for x in range(0, 193, 16)),\n",
|
| 1237 |
+
" key=lambda q: gm[q[0]:q[0] + 64, q[1]:q[1] + 64].sum())\n",
|
| 1238 |
+
"fig, axs = plt.subplots(2, 1 + len(keys), figsize=(3.3 * (1 + len(keys)), 7.2))\n",
|
| 1239 |
+
"axs[0, 0].imshow(hr_img); axs[0, 0].set_title(\"HR (256×256)\", fontsize=12)\n",
|
| 1240 |
+
"axs[0, 0].add_patch(plt.Rectangle((cx, cy), 64, 64, fill=False, edgecolor=\"white\", linewidth=1.5))\n",
|
| 1241 |
+
"axs[1, 0].imshow(hr_img[cy:cy + 64, cx:cx + 64], interpolation=\"nearest\")\n",
|
| 1242 |
+
"for c, key in enumerate(keys, start=1):\n",
|
| 1243 |
+
" lr, p = found[key]\n",
|
| 1244 |
+
" lr_img = np.clip(lr.transpose(1, 2, 0), 0, 1)\n",
|
| 1245 |
+
" axs[0, c].imshow(lr_img, interpolation=\"nearest\")\n",
|
| 1246 |
+
" axs[1, c].imshow(lr_img[cy // 4:(cy + 64) // 4, cx // 4:(cx + 64) // 4], interpolation=\"nearest\")\n",
|
| 1247 |
+
" blur = BLUR_NAMES[p[\"blur\"]] + f\" {p['blur_kernel']}×{p['blur_kernel']}\"\n",
|
| 1248 |
+
" if p[\"blur\"] == \"gaussian\":\n",
|
| 1249 |
+
" blur += f\", σ {p['blur_sigma']:.1f}\"\n",
|
| 1250 |
+
" axs[0, c].set_title(f\"{blur}\\n+ {p['interpolation']} ↓4\", fontsize=12)\n",
|
| 1251 |
+
"axs[0, 0].set_ylabel(\"full patch\", fontsize=12)\n",
|
| 1252 |
+
"axs[1, 0].set_ylabel(\"zoom (white box)\", fontsize=12)\n",
|
| 1253 |
+
"plain(axs)\n",
|
| 1254 |
+
"plt.tight_layout(pad=0.4)\n",
|
| 1255 |
+
"save_fig(fig, \"psrgan_s2_degradation.png\"); plt.show()"
|
| 1256 |
+
],
|
| 1257 |
+
"execution_count": null,
|
| 1258 |
+
"outputs": []
|
| 1259 |
+
},
|
| 1260 |
+
{
|
| 1261 |
+
"cell_type": "markdown",
|
| 1262 |
+
"metadata": {},
|
| 1263 |
+
"source": [
|
| 1264 |
+
"### Kennzahlen (für README)"
|
| 1265 |
+
]
|
| 1266 |
+
},
|
| 1267 |
+
{
|
| 1268 |
+
"cell_type": "code",
|
| 1269 |
+
"metadata": {},
|
| 1270 |
+
"source": [
|
| 1271 |
+
"def dir_bytes(pattern):\n",
|
| 1272 |
+
" return int(sum(os.path.getsize(f) for f in glob(pattern)))\n",
|
| 1273 |
+
"\n",
|
| 1274 |
+
"stats = {\n",
|
| 1275 |
+
" \"created\": time.strftime(\"%Y-%m-%d\"),\n",
|
| 1276 |
+
" \"region\": config[\"REGION\"],\n",
|
| 1277 |
+
" \"region_v1\": config[\"REGION_V1\"],\n",
|
| 1278 |
+
" \"time_window\": [config[\"TIME_FROM\"], config[\"TIME_TO\"]],\n",
|
| 1279 |
+
" \"patches_per_tile\": config[\"PATCHES_PER_TILE\"],\n",
|
| 1280 |
+
" \"n_tiles_total\": len(tiles),\n",
|
| 1281 |
+
" \"n_blocks\": len(block_groups),\n",
|
| 1282 |
+
" \"splits\": {},\n",
|
| 1283 |
+
"}\n",
|
| 1284 |
+
"for s in SPLITS:\n",
|
| 1285 |
+
" m = meta[s]\n",
|
| 1286 |
+
" stats[\"splits\"][s] = {\n",
|
| 1287 |
+
" \"num_examples\": len(hf[s]),\n",
|
| 1288 |
+
" \"num_tiles\": int(m.tile_id.nunique()),\n",
|
| 1289 |
+
" \"num_city_patches\": int((m.source == \"city\").sum()),\n",
|
| 1290 |
+
" \"num_bytes_parquet\": dir_bytes(os.path.join(HF_DATA_DIR, f\"{s}-*.parquet\")),\n",
|
| 1291 |
+
" \"num_bytes_arrow\": int(split_ds[s].data.nbytes),\n",
|
| 1292 |
+
" \"num_shards\": len(glob(os.path.join(HF_DATA_DIR, f\"{s}-*.parquet\"))),\n",
|
| 1293 |
+
" \"cloud_fraction_mean\": float(m.cloud_fraction.mean()),\n",
|
| 1294 |
+
" \"water_fraction_mean\": float(m.water_fraction.mean()),\n",
|
| 1295 |
+
" \"ndvi_mean\": ndvi_stats[s],\n",
|
| 1296 |
+
" \"scl_fraction\": {SCL_NAMES.get(c, str(c)): float(scl_frac[s][c]) for c in range(12) if scl_frac[s][c] > 0},\n",
|
| 1297 |
+
" \"lat_range\": [float(m.lat.min()), float(m.lat.max())],\n",
|
| 1298 |
+
" \"lon_range\": [float(m.lon.min()), float(m.lon.max())],\n",
|
| 1299 |
+
" }\n",
|
| 1300 |
+
"stats[\"num_examples_total\"] = sum(v[\"num_examples\"] for v in stats[\"splits\"].values())\n",
|
| 1301 |
+
"stats[\"download_size\"] = sum(v[\"num_bytes_parquet\"] for v in stats[\"splits\"].values())\n",
|
| 1302 |
+
"\n",
|
| 1303 |
+
"json.dump(stats, open(os.path.join(HF_META_DIR, \"stats.json\"), \"w\"), indent=2, ensure_ascii=False)\n",
|
| 1304 |
+
"print(json.dumps(stats, indent=2, ensure_ascii=False))"
|
| 1305 |
+
],
|
| 1306 |
+
"execution_count": null,
|
| 1307 |
+
"outputs": []
|
| 1308 |
+
},
|
| 1309 |
+
{
|
| 1310 |
+
"cell_type": "code",
|
| 1311 |
+
"metadata": {},
|
| 1312 |
+
"source": [
|
| 1313 |
+
"!ls -la {HF_DATASET_DIR} {HF_DATA_DIR} {HF_IMAGES_DIR} {HF_META_DIR}"
|
| 1314 |
+
],
|
| 1315 |
+
"execution_count": null,
|
| 1316 |
+
"outputs": []
|
| 1317 |
+
}
|
| 1318 |
+
],
|
| 1319 |
+
"metadata": {
|
| 1320 |
+
"colab": {
|
| 1321 |
+
"machine_shape": "hm",
|
| 1322 |
+
"provenance": []
|
| 1323 |
+
},
|
| 1324 |
+
"kernelspec": {
|
| 1325 |
+
"display_name": "Python 3",
|
| 1326 |
+
"name": "python3"
|
| 1327 |
+
},
|
| 1328 |
+
"language_info": {
|
| 1329 |
+
"name": "python"
|
| 1330 |
+
}
|
| 1331 |
+
},
|
| 1332 |
+
"nbformat": 4,
|
| 1333 |
+
"nbformat_minor": 0
|
| 1334 |
+
}
|
notebooks/PSRGAN_training.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
psrgan.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PSRGAN: probabilistic 4x super-resolution for Sentinel-2 RGB.
|
| 2 |
+
|
| 3 |
+
Standalone model code (PyTorch only). The backbone is a copy of the RRDBNet
|
| 4 |
+
architecture from BasicSR / Real-ESRGAN (BSD-3-Clause), with module names kept
|
| 5 |
+
identical so the published weights load strictly.
|
| 6 |
+
|
| 7 |
+
from psrgan import PSRGAN
|
| 8 |
+
model = PSRGAN.from_pretrained("marklpd/PSRGAN").eval()
|
| 9 |
+
out = model.super_resolve(lr) # lr: (B, 3, H, W) in [0, 1]
|
| 10 |
+
out["sr"], out["sigma_aleatoric"]
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
from huggingface_hub import PyTorchModelHubMixin
|
| 18 |
+
|
| 19 |
+
__all__ = ["PSRGAN", "RRDBNet", "BayesianConv2d"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------- backbone --
|
| 23 |
+
class ResidualDenseBlock(nn.Module):
|
| 24 |
+
def __init__(self, num_feat: int = 64, num_grow_ch: int = 32):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.conv1 = nn.Conv2d(num_feat, num_grow_ch, 3, 1, 1)
|
| 27 |
+
self.conv2 = nn.Conv2d(num_feat + num_grow_ch, num_grow_ch, 3, 1, 1)
|
| 28 |
+
self.conv3 = nn.Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, 3, 1, 1)
|
| 29 |
+
self.conv4 = nn.Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, 3, 1, 1)
|
| 30 |
+
self.conv5 = nn.Conv2d(num_feat + 4 * num_grow_ch, num_feat, 3, 1, 1)
|
| 31 |
+
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
|
| 32 |
+
|
| 33 |
+
def forward(self, x):
|
| 34 |
+
x1 = self.lrelu(self.conv1(x))
|
| 35 |
+
x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1)))
|
| 36 |
+
x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1)))
|
| 37 |
+
x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1)))
|
| 38 |
+
x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1))
|
| 39 |
+
return x5 * 0.2 + x
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class RRDB(nn.Module):
|
| 43 |
+
def __init__(self, num_feat: int, num_grow_ch: int = 32):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.rdb1 = ResidualDenseBlock(num_feat, num_grow_ch)
|
| 46 |
+
self.rdb2 = ResidualDenseBlock(num_feat, num_grow_ch)
|
| 47 |
+
self.rdb3 = ResidualDenseBlock(num_feat, num_grow_ch)
|
| 48 |
+
|
| 49 |
+
def forward(self, x):
|
| 50 |
+
out = self.rdb1(x)
|
| 51 |
+
out = self.rdb2(out)
|
| 52 |
+
out = self.rdb3(out)
|
| 53 |
+
return out * 0.2 + x
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class RRDBNet(nn.Module):
|
| 57 |
+
"""RRDBNet (x4) with the same parameter names as basicsr.archs.rrdbnet_arch.RRDBNet."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.conv_first = nn.Conv2d(num_in_ch, num_feat, 3, 1, 1)
|
| 62 |
+
self.body = nn.Sequential(*[RRDB(num_feat, num_grow_ch) for _ in range(num_block)])
|
| 63 |
+
self.conv_body = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
| 64 |
+
self.conv_up1 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
| 65 |
+
self.conv_up2 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
| 66 |
+
self.conv_hr = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
|
| 67 |
+
# Unused by PSRGAN (the mu/s heads replace it); kept so Real-ESRGAN keys line up.
|
| 68 |
+
self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1)
|
| 69 |
+
self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ------------------------------------------------------------------- heads --
|
| 73 |
+
class BayesianConv2d(nn.Module):
|
| 74 |
+
"""3x3 conv with a Gaussian over its weights (mean + log-variance)."""
|
| 75 |
+
|
| 76 |
+
def __init__(self, in_ch: int, out_ch: int, init_logvar: float = -10.0):
|
| 77 |
+
super().__init__()
|
| 78 |
+
conv = nn.Conv2d(in_ch, out_ch, 3, 1, 1)
|
| 79 |
+
self.weight_mu = nn.Parameter(conv.weight.data.clone())
|
| 80 |
+
self.weight_logvar = nn.Parameter(torch.full_like(conv.weight, init_logvar))
|
| 81 |
+
self.bias = conv.bias
|
| 82 |
+
|
| 83 |
+
def forward(self, x, sample: bool = True):
|
| 84 |
+
# Weights are only sampled in train mode, exactly as during training.
|
| 85 |
+
if self.training and sample:
|
| 86 |
+
eps = torch.randn_like(self.weight_mu)
|
| 87 |
+
weight = self.weight_mu + eps * torch.exp(0.5 * self.weight_logvar)
|
| 88 |
+
else:
|
| 89 |
+
weight = self.weight_mu
|
| 90 |
+
return F.conv2d(x, weight, self.bias, stride=1, padding=1)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ------------------------------------------------------------------- model --
|
| 94 |
+
class PSRGAN(
|
| 95 |
+
nn.Module,
|
| 96 |
+
PyTorchModelHubMixin,
|
| 97 |
+
library_name="pytorch",
|
| 98 |
+
license="bsd-3-clause",
|
| 99 |
+
pipeline_tag="image-to-image",
|
| 100 |
+
tags=["super-resolution", "sentinel-2", "remote-sensing", "uncertainty", "esrgan"],
|
| 101 |
+
):
|
| 102 |
+
"""4x super-resolution with a per-pixel mean (mu) and log-variance (s) output.
|
| 103 |
+
|
| 104 |
+
forward(lr) returns (mu, s): mu in [0, 1] is the super-resolved image,
|
| 105 |
+
s = log(sigma^2) is the aleatoric uncertainty per pixel and channel.
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
def __init__(self, num_feat: int = 64, num_block: int = 23, num_grow_ch: int = 32,
|
| 109 |
+
scale: int = 4, bayesian_heads: bool = True, logvar_min: float = -10.0,
|
| 110 |
+
logvar_max: float = 10.0):
|
| 111 |
+
super().__init__()
|
| 112 |
+
assert scale == 4, "PSRGAN is trained for x4"
|
| 113 |
+
self.scale = scale
|
| 114 |
+
self.logvar_min, self.logvar_max = logvar_min, logvar_max
|
| 115 |
+
self.backbone = RRDBNet(3, 3, num_feat, num_block, num_grow_ch)
|
| 116 |
+
if bayesian_heads:
|
| 117 |
+
self.mu_head = BayesianConv2d(num_feat, 3)
|
| 118 |
+
self.s_head = BayesianConv2d(num_feat, 3)
|
| 119 |
+
else:
|
| 120 |
+
self.mu_head = nn.Conv2d(num_feat, 3, 3, 1, 1)
|
| 121 |
+
self.s_head = nn.Conv2d(num_feat, 3, 3, 1, 1)
|
| 122 |
+
self._swag = None
|
| 123 |
+
|
| 124 |
+
def features(self, lr):
|
| 125 |
+
b = self.backbone
|
| 126 |
+
feat = b.conv_first(lr)
|
| 127 |
+
feat = feat + b.conv_body(b.body(feat))
|
| 128 |
+
feat = F.leaky_relu(b.conv_up1(F.interpolate(feat, scale_factor=2, mode="nearest")), 0.2, inplace=True)
|
| 129 |
+
feat = F.leaky_relu(b.conv_up2(F.interpolate(feat, scale_factor=2, mode="nearest")), 0.2, inplace=True)
|
| 130 |
+
return F.leaky_relu(b.conv_hr(feat), 0.2, inplace=True)
|
| 131 |
+
|
| 132 |
+
def forward(self, lr, sample_bayes: bool = False):
|
| 133 |
+
feat = self.features(lr)
|
| 134 |
+
if isinstance(self.mu_head, BayesianConv2d):
|
| 135 |
+
mu = torch.sigmoid(self.mu_head(feat, sample=sample_bayes))
|
| 136 |
+
s = self.s_head(feat, sample=sample_bayes)
|
| 137 |
+
else:
|
| 138 |
+
mu = torch.sigmoid(self.mu_head(feat))
|
| 139 |
+
s = self.s_head(feat)
|
| 140 |
+
return mu, torch.clamp(s, self.logvar_min, self.logvar_max)
|
| 141 |
+
|
| 142 |
+
# --------------------------------------------------------------- SWAG --
|
| 143 |
+
def load_swag(self, path: str):
|
| 144 |
+
"""Load SWAG moments (psrgan_x4_swag.safetensors) for epistemic uncertainty."""
|
| 145 |
+
from safetensors.torch import load_file
|
| 146 |
+
|
| 147 |
+
t = load_file(path)
|
| 148 |
+
self._swag = {
|
| 149 |
+
"mean": {k[5:]: v for k, v in t.items() if k.startswith("mean.")},
|
| 150 |
+
"var": {k[4:]: v for k, v in t.items() if k.startswith("var.")},
|
| 151 |
+
}
|
| 152 |
+
return self
|
| 153 |
+
|
| 154 |
+
@torch.no_grad()
|
| 155 |
+
def _set_swag_sample(self, generator=None):
|
| 156 |
+
for n, p in self.named_parameters():
|
| 157 |
+
if n in self._swag["mean"]:
|
| 158 |
+
m = self._swag["mean"][n].to(p.device)
|
| 159 |
+
v = self._swag["var"][n].to(p.device)
|
| 160 |
+
eps = torch.randn(m.shape, generator=generator, device="cpu").to(p.device)
|
| 161 |
+
p.copy_(m + v.sqrt() * eps)
|
| 162 |
+
|
| 163 |
+
# ---------------------------------------------------------- inference --
|
| 164 |
+
@torch.no_grad()
|
| 165 |
+
def super_resolve(self, lr, mc_samples: int = 0, swag_samples: int = 0, epistemic_weight: float = 10.0):
|
| 166 |
+
"""Super-resolve a batch.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
lr: (B, 3, H, W) float tensor in [0, 1], RGB = (B04, B03, B02) after a 1-99 % stretch.
|
| 170 |
+
mc_samples: Monte-Carlo samples of the Bayesian heads (epistemic, cheap).
|
| 171 |
+
swag_samples: SWAG weight samples (epistemic, needs load_swag()).
|
| 172 |
+
epistemic_weight: scale of the epistemic variance in sigma_total (10 as in the paper code).
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
dict with sr (B,3,4H,4W), sigma_aleatoric, and if requested sigma_epistemic and sigma_total.
|
| 176 |
+
"""
|
| 177 |
+
was_training = self.training
|
| 178 |
+
self.eval()
|
| 179 |
+
mu, s = self(lr, sample_bayes=False)
|
| 180 |
+
out = {"sr": mu.clamp(0, 1), "sigma_aleatoric": torch.exp(0.5 * s)}
|
| 181 |
+
|
| 182 |
+
mus = []
|
| 183 |
+
if mc_samples > 0:
|
| 184 |
+
self.mu_head.train()
|
| 185 |
+
self.s_head.train()
|
| 186 |
+
mus += [self(lr, sample_bayes=True)[0] for _ in range(mc_samples)]
|
| 187 |
+
self.mu_head.eval()
|
| 188 |
+
self.s_head.eval()
|
| 189 |
+
if swag_samples > 0:
|
| 190 |
+
if self._swag is None:
|
| 191 |
+
raise RuntimeError("call load_swag() first")
|
| 192 |
+
backup = {k: v.clone() for k, v in self.state_dict().items()}
|
| 193 |
+
for _ in range(swag_samples):
|
| 194 |
+
self._set_swag_sample()
|
| 195 |
+
mus.append(self(lr, sample_bayes=False)[0])
|
| 196 |
+
self.load_state_dict(backup)
|
| 197 |
+
if mus:
|
| 198 |
+
var_epi = torch.stack(mus).var(0, unbiased=False)
|
| 199 |
+
out["sigma_epistemic"] = var_epi.sqrt()
|
| 200 |
+
out["sigma_total"] = torch.sqrt(out["sigma_aleatoric"] ** 2 + epistemic_weight * var_epi + 1e-8)
|
| 201 |
+
|
| 202 |
+
self.train(was_training)
|
| 203 |
+
return out
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def stretch(rgb_reflectance, low: float = 1.0, high: float = 99.0):
|
| 207 |
+
"""The 1-99 % percentile stretch used to build the training data.
|
| 208 |
+
|
| 209 |
+
rgb_reflectance: (..., 3, H, W) Sentinel-2 L2A reflectance (B04, B03, B02), any scale.
|
| 210 |
+
Percentiles are computed over all channels and pixels of each image, like the dataset.
|
| 211 |
+
"""
|
| 212 |
+
x = torch.as_tensor(rgb_reflectance, dtype=torch.float32)
|
| 213 |
+
flat = x.flatten(-3)
|
| 214 |
+
p_low = torch.quantile(flat, low / 100, dim=-1, keepdim=True)[..., None, None]
|
| 215 |
+
p_high = torch.quantile(flat, high / 100, dim=-1, keepdim=True)[..., None, None]
|
| 216 |
+
return ((x - p_low) / (p_high - p_low + 1e-8)).clamp(0, 1)
|
psrgan_grid.jpg
ADDED
|
Git LFS Details
|
psrgan_x4_psnr.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3a68bf9b007ddb4c3c172e5cca60b26e3f6acd68931adb6d472822e2ad36535c
|
| 3 |
+
size 66892468
|
psrgan_x4_swag.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d51a3cbc734f820875320f20e434abb26c2e15becaf6365fc945999463fa0b6
|
| 3 |
+
size 133792144
|