JeonghyeokDo commited on
Commit
5d63a76
·
verified ·
1 Parent(s): f49a31e

Add GeoSET release files (189/211)

Browse files
Files changed (50) hide show
  1. baselines/spacenet6/cbbdm/README.md +42 -0
  2. baselines/spacenet6/cbbdm/last_model.pth +3 -0
  3. baselines/spacenet6/conddiff/README.md +40 -0
  4. baselines/spacenet6/conddiff/ema_final.pt +3 -0
  5. baselines/spacenet6/controlnet/README.md +41 -0
  6. baselines/spacenet6/controlnet/config.json +56 -0
  7. baselines/spacenet6/controlnet/diffusion_pytorch_model.safetensors +3 -0
  8. baselines/spacenet6/cyclegan/README.md +43 -0
  9. baselines/spacenet6/cyclegan/net_G_A.pth +3 -0
  10. baselines/spacenet6/cyclegan/net_G_B.pth +3 -0
  11. baselines/spacenet6/ddpm-sr3/README.md +40 -0
  12. baselines/spacenet6/ddpm-sr3/gen.pth +3 -0
  13. baselines/spacenet6/e3diff/README.md +41 -0
  14. baselines/spacenet6/e3diff/gen.pth +3 -0
  15. baselines/spacenet6/hi-diff/README.md +44 -0
  16. baselines/spacenet6/hi-diff/S1_net_g_latest.pth +3 -0
  17. baselines/spacenet6/hi-diff/S1_net_le_latest.pth +3 -0
  18. baselines/spacenet6/hi-diff/S2_net_d_latest.pth +3 -0
  19. baselines/spacenet6/hi-diff/S2_net_g_latest.pth +3 -0
  20. baselines/spacenet6/hi-diff/S2_net_le_dm_latest.pth +3 -0
  21. baselines/spacenet6/pix2pix/README.md +41 -0
  22. baselines/spacenet6/pix2pix/net_G.pth +3 -0
  23. baselines/spacenet6/pix2pixhd/README.md +42 -0
  24. baselines/spacenet6/pix2pixhd/net_G.pth +3 -0
  25. baselines/spacenet6/resshift/README.md +40 -0
  26. baselines/spacenet6/resshift/ema_model.pth +3 -0
  27. baselines/spacenet6/sd21-ft/README.md +42 -0
  28. baselines/spacenet6/sd21-ft/config.json +45 -0
  29. baselines/spacenet6/sd21-ft/diffusion_pytorch_model.safetensors +3 -0
  30. baselines/spacenet6/seg-cyclegan/README.md +44 -0
  31. baselines/spacenet6/seg-cyclegan/net_G_A.pth +3 -0
  32. baselines/spacenet6/seg-cyclegan/net_G_B.pth +3 -0
  33. baselines/spacenet6/seg-cyclegan/segnet_guide.pth +3 -0
  34. baselines/spacenet6/spade/README.md +42 -0
  35. baselines/spacenet6/spade/net_G.pth +3 -0
  36. baselines/spacenet6/stegogan/README.md +43 -0
  37. baselines/spacenet6/stegogan/net_G_A.pth +3 -0
  38. baselines/spacenet6/stegogan/net_G_B.pth +3 -0
  39. generalist/autoencoder_flux2.py +224 -0
  40. generalist/lora.py +130 -0
  41. generalist/model_index.json +20 -0
  42. generalist/pipeline.py +121 -0
  43. generalist/pipeline_geoset.py +121 -0
  44. generalist/sar_encoder/autoencoder_flux2.py +224 -0
  45. generalist/sar_encoder/config.json +13 -0
  46. generalist/sar_encoder/diffusion_pytorch_model.safetensors +3 -0
  47. generalist/sar_encoder/sar_encoder_geoset.py +102 -0
  48. generalist/sar_encoder_geoset.py +102 -0
  49. generalist/scheduler/scheduler_config.json +4 -0
  50. generalist/scheduler/scheduler_flow_bridge.py +59 -0
baselines/spacenet6/cbbdm/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cBBDM · SpaceNet6
2
+
3
+ cBBDM (GRSL'25) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the cBBDM row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Kim and Chung, "Conditional Brownian bridge diffusion model for VHR SAR to optical image translation", IEEE Geoscience and Remote Sensing Letters, 2025.
8
+ - Implementation: [egshkim/ConditionalBBDM-for-VHR-SAR-to-Optical](https://github.com/egshkim/ConditionalBBDM-for-VHR-SAR-to-Optical) at commit `8ce15934f4d4e3f01efe70d11e2d9b9e0859210c`.
9
+ - Training and inference code: [`baselines/cbbdm`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/cbbdm) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `last_model.pth` | 2,118,345,622 | `4c6148b7e86c1764ddbdfaa655d6a84a2f663a1174d61a9107cc98af25836f0b` | cBBDM checkpoint (`model`, `ema`, `epoch`, `step`), including the vq-f4 autoencoder; test.sh samples with the EMA weights |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 280.4 | 0.347 | 0.2620 | 0.450 | 0.419 | 0.447 | 19.72 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/cbbdm/fetch.sh
31
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
32
+ python baselines/data/ldm_prepare.py spacenet6
33
+ wget https://ommer-lab.com/files/latent-diffusion/vq-f4.zip && unzip vq-f4.zip -d $CKPT_ROOT/vq-f4
34
+ hf download JeonghyeokDo/GeoSET "baselines/spacenet6/cbbdm/last_model.pth" --local-dir hf
35
+ mkdir -p $CKPT_ROOT/cbbdm/spacenet6 && cp hf/baselines/spacenet6/cbbdm/* $CKPT_ROOT/cbbdm/spacenet6/
36
+ bash baselines/cbbdm/test.sh spacenet6 # writes $WORK_DIR/results/cbbdm/spacenet6/<stem>.png
37
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/cbbdm/spacenet6
38
+ ```
39
+
40
+ ## Licence
41
+
42
+ These weights are released under the MIT licence of the cBBDM code (Copyright (c) 2025 egshkim, [LICENSE](https://github.com/egshkim/ConditionalBBDM-for-VHR-SAR-to-Optical/blob/8ce15934f4d4e3f01efe70d11e2d9b9e0859210c/LICENSE)). The checkpoint contains the weights of the CompVis latent-diffusion vq-f4 autoencoder, MIT licence, Copyright (c) 2022 Machine Vision and Learning Group, LMU Munich ([LICENSE](https://github.com/CompVis/latent-diffusion/blob/main/LICENSE)).
baselines/spacenet6/cbbdm/last_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c6148b7e86c1764ddbdfaa655d6a84a2f663a1174d61a9107cc98af25836f0b
3
+ size 2118345622
baselines/spacenet6/conddiff/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CondDiff · SpaceNet6
2
+
3
+ CondDiff (GRSL'23) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the CondDiff row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Bai, Pu and Xu, "Conditional diffusion for SAR to optical image translation", GRSL 2023.
8
+ - Implementation: [Coordi777/Conditional-Diffusion-for-SAR-to-Optical-Image-Translation](https://github.com/Coordi777/Conditional-Diffusion-for-SAR-to-Optical-Image-Translation) at commit `8326f54adf3a8d717792cb7abc7d3d2b7dfac565`.
9
+ - Training and inference code: [`baselines/conddiff`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/conddiff) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `ema_final.pt` | 657,495,287 | `7ba72b69057de1aaef89ce7aabd3adc358dd7f1123874952fbc29cf9e7bf5b66` | UNet EMA (0.9999) weights at step 50,000 (256 × 256 model) |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 160.1 | 0.274 | 0.1012 | 0.514 | 0.517 | 0.198 | 14.68 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/conddiff/fetch.sh
31
+ python baselines/data/diff_data.py conddiff spacenet6
32
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/conddiff/*" --local-dir hf
33
+ mkdir -p $CKPT_ROOT/conddiff/spacenet6 && cp hf/baselines/spacenet6/conddiff/ema_final.pt $CKPT_ROOT/conddiff/spacenet6/
34
+ bash baselines/conddiff/test.sh spacenet6 # writes $WORK_DIR/results/conddiff/spacenet6/<stem>.png
35
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/conddiff/spacenet6
36
+ ```
37
+
38
+ ## Licence
39
+
40
+ The CondDiff repository contains no licence file; it is derived from [openai/guided-diffusion](https://github.com/openai/guided-diffusion) (MIT). These weights were trained by the GeoSET authors with that code; the GeoSET repository distributes only its changes to the code.
baselines/spacenet6/conddiff/ema_final.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ba72b69057de1aaef89ce7aabd3adc358dd7f1123874952fbc29cf9e7bf5b66
3
+ size 657495287
baselines/spacenet6/controlnet/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ControlNet · SpaceNet6
2
+
3
+ ControlNet (ICCV'23) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the ControlNet row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Zhang, Rao and Agrawala, "Adding conditional control to text-to-image diffusion models", ICCV 2023.
8
+ - Implementation: the diffusers ControlNet training example ([`examples/controlnet/train_controlnet.py`](https://github.com/huggingface/diffusers/blob/v0.37.0/examples/controlnet/train_controlnet.py), v0.37.0) on Stable Diffusion 2.1-base (`Manojb/stable-diffusion-2-1-base`, revision `0094d483a120f3f33dafbd187ea4aa60d10de75c`).
9
+ - Training and inference code: [`baselines/controlnet`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/controlnet) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `diffusion_pytorch_model.safetensors` | 1,456,953,560 | `39c14b3d5059b9380673b9c9341f0aa7774a36ad8d39419d0360e5ff76afeea4` | ControlNet weights (diffusers `ControlNetModel`, fp32) |
16
+ | `config.json` | 1,293 | `911d4ffe1344ecfbb778a561186875c08ae2fc51a9f3a4c54a087363ef09b881` | ControlNet configuration |
17
+
18
+ ## Results (GeoSET paper, Table 3)
19
+
20
+ SpaceNet6 test split, 495 pairs, 256 × 256.
21
+
22
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
23
+ |---:|---:|---:|---:|---:|---:|---:|
24
+ | 131.3 | 0.279 | 0.0635 | 0.615 | 0.413 | 0.448 | 14.45 |
25
+
26
+ ## Inference
27
+
28
+ ```bash
29
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
30
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
31
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
32
+ python baselines/data/ldm_prepare.py spacenet6
33
+ hf download JeonghyeokDo/GeoSET "baselines/spacenet6/controlnet/diffusion_pytorch_model.safetensors" "baselines/spacenet6/controlnet/config.json" --local-dir hf
34
+ mkdir -p $CKPT_ROOT/controlnet/spacenet6 && cp hf/baselines/spacenet6/controlnet/* $CKPT_ROOT/controlnet/spacenet6/
35
+ bash baselines/controlnet/test.sh spacenet6 # writes $WORK_DIR/results/controlnet/spacenet6/<stem>.png
36
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/controlnet/spacenet6
37
+ ```
38
+
39
+ ## Licence
40
+
41
+ These weights are a derivative of Stable Diffusion 2.1-base and are released under the CreativeML Open RAIL++-M licence ([text](https://huggingface.co/stabilityai/stable-diffusion-2/blob/main/LICENSE-MODEL)); the use-based restrictions of its Attachment A apply to any use and redistribution. The training script is Apache-2.0 (diffusers).
baselines/spacenet6/controlnet/config.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "ControlNetModel",
3
+ "_diffusers_version": "0.37.1",
4
+ "act_fn": "silu",
5
+ "addition_embed_type": null,
6
+ "addition_embed_type_num_heads": 64,
7
+ "addition_time_embed_dim": null,
8
+ "attention_head_dim": [
9
+ 5,
10
+ 10,
11
+ 20,
12
+ 20
13
+ ],
14
+ "block_out_channels": [
15
+ 320,
16
+ 640,
17
+ 1280,
18
+ 1280
19
+ ],
20
+ "class_embed_type": null,
21
+ "conditioning_channels": 3,
22
+ "conditioning_embedding_out_channels": [
23
+ 16,
24
+ 32,
25
+ 96,
26
+ 256
27
+ ],
28
+ "controlnet_conditioning_channel_order": "rgb",
29
+ "cross_attention_dim": 1024,
30
+ "down_block_types": [
31
+ "CrossAttnDownBlock2D",
32
+ "CrossAttnDownBlock2D",
33
+ "CrossAttnDownBlock2D",
34
+ "DownBlock2D"
35
+ ],
36
+ "downsample_padding": 1,
37
+ "encoder_hid_dim": null,
38
+ "encoder_hid_dim_type": null,
39
+ "flip_sin_to_cos": true,
40
+ "freq_shift": 0,
41
+ "global_pool_conditions": false,
42
+ "in_channels": 4,
43
+ "layers_per_block": 2,
44
+ "mid_block_scale_factor": 1,
45
+ "mid_block_type": "UNetMidBlock2DCrossAttn",
46
+ "norm_eps": 1e-05,
47
+ "norm_num_groups": 32,
48
+ "num_attention_heads": null,
49
+ "num_class_embeds": null,
50
+ "only_cross_attention": false,
51
+ "projection_class_embeddings_input_dim": null,
52
+ "resnet_time_scale_shift": "default",
53
+ "transformer_layers_per_block": 1,
54
+ "upcast_attention": false,
55
+ "use_linear_projection": true
56
+ }
baselines/spacenet6/controlnet/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39c14b3d5059b9380673b9c9341f0aa7774a36ad8d39419d0360e5ff76afeea4
3
+ size 1456953560
baselines/spacenet6/cyclegan/README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CycleGAN · SpaceNet6
2
+
3
+ CycleGAN (ICCV'17) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the CycleGAN row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Zhu, Park, Isola and Efros, "Unpaired image-to-image translation using cycle-consistent adversarial networks", ICCV 2017.
8
+ - Implementation: [junyanz/pytorch-CycleGAN-and-pix2pix](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix) at commit `2a7afba2895d52556dd5dfe07e8555ef657ced6f`.
9
+ - Training and inference code: [`baselines/cyclegan`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/cyclegan) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G_A.pth` | 45,533,133 | `580135302c8bfa73b258f37c3194c5ab4a3bbe2b26c25bdd17cb77506f6e99a2` | Generator G_A (`resnet_9blocks`), SAR → EO; produces the output image |
16
+ | `net_G_B.pth` | 45,533,133 | `bdd465d9502581b9181d0a8f6f8dd7ddda35e549815b6c8dba147f525a917aa3` | Generator G_B (`resnet_9blocks`), EO → SAR; loaded by the upstream test script |
17
+
18
+ ## Results (GeoSET paper, Table 3)
19
+
20
+ SpaceNet6 test split, 495 pairs, 256 × 256.
21
+
22
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
23
+ |---:|---:|---:|---:|---:|---:|---:|
24
+ | 119.3 | 0.219 | 0.0436 | 0.660 | 0.373 | 0.478 | 16.88 |
25
+
26
+ ## Inference
27
+
28
+ ```bash
29
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
30
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
31
+ bash baselines/cyclegan/fetch.sh
32
+ bash baselines/pix2pix/fetch.sh # combine_A_and_B.py, used by gan_prepare.py
33
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
34
+ python baselines/data/gan_prepare.py spacenet6
35
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/cyclegan/*.pth" --local-dir hf
36
+ mkdir -p $CKPT_ROOT/cyclegan/spacenet6 && cp hf/baselines/spacenet6/cyclegan/*.pth $CKPT_ROOT/cyclegan/spacenet6/
37
+ bash baselines/cyclegan/test.sh spacenet6 # writes $WORK_DIR/results/cyclegan/spacenet6/<stem>.png
38
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/cyclegan/spacenet6
39
+ ```
40
+
41
+ ## Licence
42
+
43
+ These weights were trained with the pytorch-CycleGAN-and-pix2pix code and are released under its BSD licence (Copyright (c) 2017 Jun-Yan Zhu and Taesung Park; Copyright (c) 2016 Phillip Isola and Jun-Yan Zhu; [LICENSE](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/2a7afba2895d52556dd5dfe07e8555ef657ced6f/LICENSE)). The GeoSET repository states its change to the code (`baselines/cyclegan`).
baselines/spacenet6/cyclegan/net_G_A.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:580135302c8bfa73b258f37c3194c5ab4a3bbe2b26c25bdd17cb77506f6e99a2
3
+ size 45533133
baselines/spacenet6/cyclegan/net_G_B.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bdd465d9502581b9181d0a8f6f8dd7ddda35e549815b6c8dba147f525a917aa3
3
+ size 45533133
baselines/spacenet6/ddpm-sr3/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DDPM (SR3) · SpaceNet6
2
+
3
+ DDPM (SR3) (TPAMI'22) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the DDPM (SR3) row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Saharia, Ho, Chan, Salimans, Fleet and Norouzi, "Image super-resolution via iterative refinement", TPAMI 2022.
8
+ - Implementation: stage 1 (conditional DDPM) of [DeepSARRS/E3Diff](https://github.com/DeepSARRS/E3Diff) at commit `38601093ab8f8e4b478144621f20890b100a3b74`.
9
+ - Training and inference code: [`baselines/ddpm-sr3`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/ddpm-sr3) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `gen.pth` | 768,427,299 | `204f19caaa3efb59cd4d351a2792a6671bdb40f3400eae39aa6ee006fd203db6` | Conditional DDPM UNet (E3Diff stage 1), state dict at iteration 250,000; 3-channel output, [SAR-PPB, SAR-canny, SAR] condition |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 219.1 | 0.333 | 0.1616 | 0.419 | 0.622 | 0.111 | 12.45 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/ddpm-sr3/fetch.sh
31
+ python baselines/data/diff_data.py e3diff spacenet6
32
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/ddpm-sr3/*" --local-dir hf
33
+ mkdir -p $CKPT_ROOT/ddpm-sr3/spacenet6 && cp hf/baselines/spacenet6/ddpm-sr3/gen.pth $CKPT_ROOT/ddpm-sr3/spacenet6/
34
+ bash baselines/ddpm-sr3/test.sh spacenet6 # writes $WORK_DIR/results/ddpm-sr3/spacenet6/<stem>.png
35
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/ddpm-sr3/spacenet6
36
+ ```
37
+
38
+ ## Licence
39
+
40
+ The E3Diff repository contains no licence file (its bundled `SoftPool/` directory is MIT). These weights were trained by the GeoSET authors with that code; the GeoSET repository distributes only its changes to the code.
baselines/spacenet6/ddpm-sr3/gen.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:204f19caaa3efb59cd4d351a2792a6671bdb40f3400eae39aa6ee006fd203db6
3
+ size 768427299
baselines/spacenet6/e3diff/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # E3Diff · SpaceNet6
2
+
3
+ E3Diff (GRSL'24) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the E3Diff row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Qin, Zou, Li and Zhang, "Efficient end-to-end diffusion model for one-step SAR-to-optical translation", GRSL 2024.
8
+ - Implementation: [DeepSARRS/E3Diff](https://github.com/DeepSARRS/E3Diff) at commit `38601093ab8f8e4b478144621f20890b100a3b74` (stage 2, initialised from the DDPM (SR3) stage-1 weights).
9
+ - Training and inference code: [`baselines/e3diff`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/e3diff) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `gen.pth` | 768,427,299 | `53786dd3255871070effc0353ffefffa3339ab3ebacccbde3f9d5ed04fef542c` | One-step generator UNet (E3Diff stage 2), state dict at iteration 310,000; 3-channel output, [SAR-PPB, SAR-canny, SAR] condition |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 110.6 | 0.197 | 0.0440 | 0.696 | 0.361 | 0.462 | 18.32 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/e3diff/fetch.sh
31
+ # vision-aided-loss (CLIP discriminator, built by the model): see baselines/e3diff/README.md
32
+ python baselines/data/diff_data.py e3diff spacenet6
33
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/e3diff/*" --local-dir hf
34
+ mkdir -p $CKPT_ROOT/e3diff/spacenet6 && cp hf/baselines/spacenet6/e3diff/gen.pth $CKPT_ROOT/e3diff/spacenet6/
35
+ bash baselines/e3diff/test.sh spacenet6 # writes $WORK_DIR/results/e3diff/spacenet6/<stem>.png
36
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/e3diff/spacenet6
37
+ ```
38
+
39
+ ## Licence
40
+
41
+ The E3Diff repository contains no licence file (its bundled `SoftPool/` directory is MIT). These weights were trained by the GeoSET authors with that code; the GeoSET repository distributes only its changes to the code.
baselines/spacenet6/e3diff/gen.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53786dd3255871070effc0353ffefffa3339ab3ebacccbde3f9d5ed04fef542c
3
+ size 768427299
baselines/spacenet6/hi-diff/README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HI-Diff · SpaceNet6
2
+
3
+ HI-Diff (NeurIPS'23) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the HI-Diff row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Chen, Zhang, Liu, Gu, Kong, Yuan et al., "Hierarchical integration diffusion model for realistic image deblurring", NeurIPS 2023.
8
+ - Implementation: [zhengchen1999/HI-Diff](https://github.com/zhengchen1999/HI-Diff) at commit `b3bfd167997e27f8edd57681cf70e5031a0e35f2`.
9
+ - Training and inference code: [`baselines/hi-diff`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/hi-diff) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `S1_net_g_latest.pth` | 101,401,457 | `7e5b2546e43ed2ab3b6543733ffc7bec2ec74fbdc43f150a134527ac4c8aef71` | Stage-1 Transformer restorer (iteration 25,000) |
16
+ | `S1_net_le_latest.pth` | 2,357,509 | `b2ea2d2c8df523aac1220377dc05bf62655963842c4098fa13adc6eac0205993` | Stage-1 latent encoder, SAR + EO input (iteration 25,000) |
17
+ | `S2_net_g_latest.pth` | 101,401,457 | `8de9e57dc6e2a8c6c7299e846d0021eebcb1090ce162e023c8e804aa6f42b0c0` | Stage-2 Transformer restorer (iteration 25,000); used by test.sh |
18
+ | `S2_net_le_dm_latest.pth` | 2,247,061 | `b8ed232b742fcbb4eb836b1185b9947d65781a87afa9a627b412553ea028773a` | Stage-2 latent encoder, SAR input (iteration 25,000); used by test.sh |
19
+ | `S2_net_d_latest.pth` | 10,530,491 | `6fb28955d58f72a71f9d4e7da7049d12430749340dd7d6fb7847cdcdea4e6878` | Stage-2 latent denoising network (iteration 25,000); used by test.sh |
20
+
21
+ ## Results (GeoSET paper, Table 3)
22
+
23
+ SpaceNet6 test split, 495 pairs, 256 × 256.
24
+
25
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
26
+ |---:|---:|---:|---:|---:|---:|---:|
27
+ | 261.2 | 0.300 | 0.2036 | 0.499 | 0.403 | 0.589 | 20.61 |
28
+
29
+ ## Inference
30
+
31
+ ```bash
32
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
33
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
34
+ bash baselines/hi-diff/fetch.sh
35
+ python baselines/data/diff_data.py paired spacenet6
36
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/hi-diff/*" --local-dir hf
37
+ mkdir -p $CKPT_ROOT/hi-diff/spacenet6 && cp hf/baselines/spacenet6/hi-diff/*.pth $CKPT_ROOT/hi-diff/spacenet6/
38
+ bash baselines/hi-diff/test.sh spacenet6 # writes $WORK_DIR/results/hi-diff/spacenet6/<stem>.png
39
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/hi-diff/spacenet6
40
+ ```
41
+
42
+ ## Licence
43
+
44
+ These weights were trained with the HI-Diff code and are released under its [Apache-2.0](https://github.com/zhengchen1999/HI-Diff/blob/b3bfd167997e27f8edd57681cf70e5031a0e35f2/LICENSE) licence; the GeoSET repository states its modifications to the code (`baselines/hi-diff`).
baselines/spacenet6/hi-diff/S1_net_g_latest.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7e5b2546e43ed2ab3b6543733ffc7bec2ec74fbdc43f150a134527ac4c8aef71
3
+ size 101401457
baselines/spacenet6/hi-diff/S1_net_le_latest.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b2ea2d2c8df523aac1220377dc05bf62655963842c4098fa13adc6eac0205993
3
+ size 2357509
baselines/spacenet6/hi-diff/S2_net_d_latest.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6fb28955d58f72a71f9d4e7da7049d12430749340dd7d6fb7847cdcdea4e6878
3
+ size 10530491
baselines/spacenet6/hi-diff/S2_net_g_latest.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8de9e57dc6e2a8c6c7299e846d0021eebcb1090ce162e023c8e804aa6f42b0c0
3
+ size 101401457
baselines/spacenet6/hi-diff/S2_net_le_dm_latest.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8ed232b742fcbb4eb836b1185b9947d65781a87afa9a627b412553ea028773a
3
+ size 2247061
baselines/spacenet6/pix2pix/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pix2pix · SpaceNet6
2
+
3
+ pix2pix (CVPR'17) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the pix2pix row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Isola, Zhu, Zhou and Efros, "Image-to-image translation with conditional adversarial networks", CVPR 2017.
8
+ - Implementation: [junyanz/pytorch-CycleGAN-and-pix2pix](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix) at commit `2a7afba2895d52556dd5dfe07e8555ef657ced6f`.
9
+ - Training and inference code: [`baselines/pix2pix`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/pix2pix) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G.pth` | 217,728,293 | `a9a8c502b453643b26f5718ece86d27a7af503adce5d0e85cb694adbd4aeaf26` | U-Net generator (`unet_256`), SAR → EO; used by test.sh |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 166.7 | 0.242 | 0.1091 | 0.589 | 0.419 | 0.459 | 17.07 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/pix2pix/fetch.sh
31
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
32
+ python baselines/data/gan_prepare.py spacenet6
33
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/pix2pix/*.pth" --local-dir hf
34
+ mkdir -p $CKPT_ROOT/pix2pix/spacenet6 && cp hf/baselines/spacenet6/pix2pix/*.pth $CKPT_ROOT/pix2pix/spacenet6/
35
+ bash baselines/pix2pix/test.sh spacenet6 # writes $WORK_DIR/results/pix2pix/spacenet6/<stem>.png
36
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/pix2pix/spacenet6
37
+ ```
38
+
39
+ ## Licence
40
+
41
+ These weights were trained with the pytorch-CycleGAN-and-pix2pix code and are released under its BSD licence (Copyright (c) 2017 Jun-Yan Zhu and Taesung Park; Copyright (c) 2016 Phillip Isola and Jun-Yan Zhu; [LICENSE](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix/blob/2a7afba2895d52556dd5dfe07e8555ef657ced6f/LICENSE)). The GeoSET repository states its change to the code (`baselines/pix2pix`).
baselines/spacenet6/pix2pix/net_G.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9a8c502b453643b26f5718ece86d27a7af503adce5d0e85cb694adbd4aeaf26
3
+ size 217728293
baselines/spacenet6/pix2pixhd/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pix2pixHD · SpaceNet6
2
+
3
+ pix2pixHD (CVPR'18) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the pix2pixHD row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Wang, Liu, Zhu, Tao, Kautz and Catanzaro, "High-resolution image synthesis and semantic manipulation with conditional GANs", CVPR 2018.
8
+ - Implementation: [NVIDIA/pix2pixHD](https://github.com/NVIDIA/pix2pixHD) at commit `14b3b3c7fff413086e3b58df52096f16b6891172`.
9
+ - Training and inference code: [`baselines/pix2pixhd`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/pix2pixhd) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G.pth` | 729,795,505 | `1f32e16f7468b5fe285645926b75e6730bbb5068cd2d7f3e5b840a0931df5eca` | Global generator (4 downsampling layers, 9 residual blocks), SAR → EO; used by test.sh |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 175.1 | 0.246 | 0.1136 | 0.682 | 0.361 | 0.525 | 19.21 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/pix2pixhd/fetch.sh
31
+ bash baselines/pix2pix/fetch.sh # combine_A_and_B.py, used by gan_prepare.py
32
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
33
+ python baselines/data/gan_prepare.py spacenet6
34
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/pix2pixhd/*.pth" --local-dir hf
35
+ mkdir -p $CKPT_ROOT/pix2pixhd/spacenet6 && cp hf/baselines/spacenet6/pix2pixhd/*.pth $CKPT_ROOT/pix2pixhd/spacenet6/
36
+ bash baselines/pix2pixhd/test.sh spacenet6 # writes $WORK_DIR/results/pix2pixhd/spacenet6/<stem>.png
37
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/pix2pixhd/spacenet6
38
+ ```
39
+
40
+ ## Licence
41
+
42
+ These weights were trained with the pix2pixHD code and are released under its BSD licence (Copyright (C) 2019 NVIDIA Corporation, Ting-Chun Wang, Ming-Yu Liu, Jun-Yan Zhu; [LICENSE.txt](https://github.com/NVIDIA/pix2pixHD/blob/14b3b3c7fff413086e3b58df52096f16b6891172/LICENSE.txt)). The GeoSET repository states its changes to the code (`baselines/pix2pixhd`).
baselines/spacenet6/pix2pixhd/net_G.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f32e16f7468b5fe285645926b75e6730bbb5068cd2d7f3e5b840a0931df5eca
3
+ size 729795505
baselines/spacenet6/resshift/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ResShift · SpaceNet6
2
+
3
+ ResShift (NeurIPS'23) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the ResShift row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Yue, Wang and Loy, "ResShift: Efficient diffusion model for image super-resolution by residual shifting", NeurIPS 2023.
8
+ - Implementation: [zsyOAOA/ResShift](https://github.com/zsyOAOA/ResShift) at commit `bb03b7d21614cace01787e097c8a6ab6b945227d`.
9
+ - Training and inference code: [`baselines/resshift`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/resshift) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `ema_model.pth` | 478,392,663 | `a5c3d1b910970efd770bee33e76d6ab0e308df9d721a2c1f2174cd89e56e904d` | UNetModelSwin EMA (0.999) weights at iteration 50,000; used with the ResShift VQ-f4 autoencoder, which `baselines/resshift/fetch.sh` downloads |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 155.4 | 0.230 | 0.0858 | 0.540 | 0.367 | 0.421 | 18.58 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/resshift/fetch.sh
31
+ python baselines/data/diff_data.py paired spacenet6
32
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/resshift/*" --local-dir hf
33
+ mkdir -p $CKPT_ROOT/resshift/spacenet6 && cp hf/baselines/spacenet6/resshift/ema_model.pth $CKPT_ROOT/resshift/spacenet6/
34
+ bash baselines/resshift/test.sh spacenet6 # writes $WORK_DIR/results/resshift/spacenet6/<stem>.png
35
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/resshift/spacenet6
36
+ ```
37
+
38
+ ## Licence
39
+
40
+ These weights were trained with the ResShift code and are released under its [S-Lab License 1.0](https://github.com/zsyOAOA/ResShift/blob/bb03b7d21614cace01787e097c8a6ab6b945227d/LICENSE) (Copyright 2022 S-Lab): non-commercial use only. The VQ-f4 autoencoder used at inference is part of the ResShift v2.0 release and is not included here.
baselines/spacenet6/resshift/ema_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5c3d1b910970efd770bee33e76d6ab0e308df9d721a2c1f2174cd89e56e904d
3
+ size 478392663
baselines/spacenet6/sd21-ft/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SD2.1 FT · SpaceNet6
2
+
3
+ SD2.1 FT (CVPR'22) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the SD2.1 FT row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Rombach, Blattmann, Lorenz, Esser and Ommer, "High-resolution image synthesis with latent diffusion models", CVPR 2022.
8
+ - Implementation: Stable Diffusion 2.1-base (`Manojb/stable-diffusion-2-1-base`, revision `0094d483a120f3f33dafbd187ea4aa60d10de75c`), fine-tuned with the stage-1 scripts that `baselines/sd21-ft/sd21-ft.patch` adds to [KAIST-VICLab/C-DiffSET](https://github.com/KAIST-VICLab/C-DiffSET) at commit `6fc380215ae97c9d134e8f1434061b652339b45c`.
9
+ - Training and inference code: [`baselines/sd21-ft`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/sd21-ft) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `diffusion_pytorch_model.safetensors` | 3,463,772,592 | `93c99b5bf5cefb5df57a403f9691b3b50a138d6a41b1d4726f64035ddf8c5fd4` | UNet weights (diffusers `UNet2DConditionModel`, fp32); input = SAR latent and noisy EO latent (8 channels), output = noise (4 channels) |
16
+ | `config.json` | 911 | `4ea373ed4a09313343a02c2ebb22a839cca967e7e97a9dd281f68b7d0fbee7c3` | UNet configuration |
17
+
18
+ ## Results (GeoSET paper, Table 3)
19
+
20
+ SpaceNet6 test split, 495 pairs, 256 × 256.
21
+
22
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
23
+ |---:|---:|---:|---:|---:|---:|---:|
24
+ | 124.7 | 0.196 | 0.0537 | 0.612 | 0.348 | 0.513 | 19.29 |
25
+
26
+ ## Inference
27
+
28
+ ```bash
29
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
30
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
31
+ bash baselines/sd21-ft/fetch.sh
32
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
33
+ python baselines/data/ldm_prepare.py spacenet6
34
+ hf download JeonghyeokDo/GeoSET "baselines/spacenet6/sd21-ft/diffusion_pytorch_model.safetensors" "baselines/spacenet6/sd21-ft/config.json" --local-dir hf
35
+ mkdir -p $CKPT_ROOT/sd21-ft/spacenet6 && cp hf/baselines/spacenet6/sd21-ft/* $CKPT_ROOT/sd21-ft/spacenet6/
36
+ bash baselines/sd21-ft/test.sh spacenet6 # writes $WORK_DIR/results/sd21-ft/spacenet6/<stem>.png
37
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/sd21-ft/spacenet6
38
+ ```
39
+
40
+ ## Licence
41
+
42
+ These weights are a derivative of Stable Diffusion 2.1-base and are released under the CreativeML Open RAIL++-M licence ([text](https://huggingface.co/stabilityai/stable-diffusion-2/blob/main/LICENSE-MODEL)); the use-based restrictions of its Attachment A apply to any use and redistribution. The training code is MIT (C-DiffSET, Copyright (c) 2026 KAIST VICLab).
baselines/spacenet6/sd21-ft/config.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "UNet2DConditionModel",
3
+ "_diffusers_version": "0.10.0.dev0",
4
+ "act_fn": "silu",
5
+ "attention_head_dim": [
6
+ 5,
7
+ 10,
8
+ 20,
9
+ 20
10
+ ],
11
+ "block_out_channels": [
12
+ 320,
13
+ 640,
14
+ 1280,
15
+ 1280
16
+ ],
17
+ "center_input_sample": false,
18
+ "cross_attention_dim": 1024,
19
+ "down_block_types": [
20
+ "CrossAttnDownBlock2D",
21
+ "CrossAttnDownBlock2D",
22
+ "CrossAttnDownBlock2D",
23
+ "DownBlock2D"
24
+ ],
25
+ "downsample_padding": 1,
26
+ "dual_cross_attention": false,
27
+ "flip_sin_to_cos": true,
28
+ "freq_shift": 0,
29
+ "in_channels": 8,
30
+ "layers_per_block": 2,
31
+ "mid_block_scale_factor": 1,
32
+ "norm_eps": 1e-05,
33
+ "norm_num_groups": 32,
34
+ "num_class_embeds": null,
35
+ "only_cross_attention": false,
36
+ "out_channels": 4,
37
+ "sample_size": 64,
38
+ "up_block_types": [
39
+ "UpBlock2D",
40
+ "CrossAttnUpBlock2D",
41
+ "CrossAttnUpBlock2D",
42
+ "CrossAttnUpBlock2D"
43
+ ],
44
+ "use_linear_projection": true
45
+ }
baselines/spacenet6/sd21-ft/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:93c99b5bf5cefb5df57a403f9691b3b50a138d6a41b1d4726f64035ddf8c5fd4
3
+ size 3463772592
baselines/spacenet6/seg-cyclegan/README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Seg-CycleGAN · SpaceNet6
2
+
3
+ Seg-CycleGAN (GRSL'25) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the Seg-CycleGAN row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Zhang, Li, Lin, Zhang, Fan, Liu and Liu, "Seg-CycleGAN: SAR-to-optical image translation guided by a downstream task", IEEE GRSL 2025.
8
+ - Implementation: [NWPU-LHH/Seg-CycleGAN](https://github.com/NWPU-LHH/Seg-CycleGAN) at commit `f12ff88ac089b7099443ffa2c34fc47e035def6a`.
9
+ - Training and inference code: [`baselines/seg-cyclegan`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/seg-cyclegan) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G_A.pth` | 45,532,493 | `a3cfe869a0d67bc298d52527b95eec952fbe301959d43c7d971a74cbca3c5b9f` | Generator G_A (`resnet_9blocks`), SAR → EO; produces the output image |
16
+ | `net_G_B.pth` | 45,532,493 | `592a9e9770f78bd9df29a14e01344a5a4bf29f1121e2a6c3ad7252d79ac5824f` | Generator G_B (`resnet_9blocks`), EO → SAR; loaded by the upstream test script |
17
+ | `segnet_guide.pth` | 117,899,184 | `f48ed0a584d9dde5a6a4b8cde3eace2104dbb6006d378ea26c2b460ceded1934` | Guide SegNet (VGG16-BN encoder; background / building) trained by the GeoSET authors with `baselines/seg-cyclegan/train_segnet.py` on the SpaceNet6 training tiles; used by train.sh only |
18
+
19
+ ## Results (GeoSET paper, Table 3)
20
+
21
+ SpaceNet6 test split, 495 pairs, 256 × 256.
22
+
23
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
24
+ |---:|---:|---:|---:|---:|---:|---:|
25
+ | 131.4 | 0.229 | 0.0611 | 0.649 | 0.386 | 0.471 | 16.62 |
26
+
27
+ ## Inference
28
+
29
+ ```bash
30
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
31
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
32
+ bash baselines/seg-cyclegan/fetch.sh
33
+ bash baselines/pix2pix/fetch.sh # combine_A_and_B.py, used by gan_prepare.py
34
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
35
+ python baselines/data/gan_prepare.py spacenet6
36
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/seg-cyclegan/*.pth" --local-dir hf
37
+ mkdir -p $CKPT_ROOT/seg-cyclegan/spacenet6 && cp hf/baselines/spacenet6/seg-cyclegan/*.pth $CKPT_ROOT/seg-cyclegan/spacenet6/
38
+ bash baselines/seg-cyclegan/test.sh spacenet6 # writes $WORK_DIR/results/seg-cyclegan/spacenet6/<stem>.png
39
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/seg-cyclegan/spacenet6
40
+ ```
41
+
42
+ ## Licence
43
+
44
+ The Seg-CycleGAN repository contains no licence file. These weights were trained by the GeoSET authors with that code; the GeoSET repository distributes only its changes to the code. The CycleGAN components of that code derive from junyanz/pytorch-CycleGAN-and-pix2pix (BSD).
baselines/spacenet6/seg-cyclegan/net_G_A.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3cfe869a0d67bc298d52527b95eec952fbe301959d43c7d971a74cbca3c5b9f
3
+ size 45532493
baselines/spacenet6/seg-cyclegan/net_G_B.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:592a9e9770f78bd9df29a14e01344a5a4bf29f1121e2a6c3ad7252d79ac5824f
3
+ size 45532493
baselines/spacenet6/seg-cyclegan/segnet_guide.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f48ed0a584d9dde5a6a4b8cde3eace2104dbb6006d378ea26c2b460ceded1934
3
+ size 117899184
baselines/spacenet6/spade/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPADE · SpaceNet6
2
+
3
+ SPADE (CVPR'19) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the SPADE row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Park, Liu, Wang and Zhu, "Semantic image synthesis with spatially-adaptive normalization", CVPR 2019.
8
+ - Implementation: [NVlabs/SPADE](https://github.com/NVlabs/SPADE) at commit `fecacc920c1367a038995c45a39c15f6521ca64f`.
9
+ - Training and inference code: [`baselines/spade`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/spade) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G.pth` | 368,775,707 | `b6714fb30bd34f62c8495af92bc225b96d9363cb8edc2839df3918784f2a4e6f` | SPADE generator with the SAR image as its 3-channel input map, SAR → EO; loads only into the code patched by `baselines/spade/spade.patch`; used by test.sh |
16
+
17
+ ## Results (GeoSET paper, Table 3)
18
+
19
+ SpaceNet6 test split, 495 pairs, 256 × 256.
20
+
21
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
22
+ |---:|---:|---:|---:|---:|---:|---:|
23
+ | 179.7 | 0.242 | 0.1228 | 0.662 | 0.385 | 0.489 | 17.77 |
24
+
25
+ ## Inference
26
+
27
+ ```bash
28
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
29
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
30
+ bash baselines/spade/fetch.sh
31
+ bash baselines/pix2pix/fetch.sh # combine_A_and_B.py, used by gan_prepare.py
32
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
33
+ python baselines/data/gan_prepare.py spacenet6
34
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/spade/*.pth" --local-dir hf
35
+ mkdir -p $CKPT_ROOT/spade/spacenet6 && cp hf/baselines/spacenet6/spade/*.pth $CKPT_ROOT/spade/spacenet6/
36
+ bash baselines/spade/test.sh spacenet6 # writes $WORK_DIR/results/spade/spacenet6/<stem>.png
37
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/spade/spacenet6
38
+ ```
39
+
40
+ ## Licence
41
+
42
+ These weights were trained with the SPADE code (Copyright (C) 2019 NVIDIA Corporation) and are released under its licence, [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode) ([LICENSE.md](https://github.com/NVlabs/SPADE/blob/fecacc920c1367a038995c45a39c15f6521ca64f/LICENSE.md)): non-commercial use only, and adaptations must be shared under the same licence. The GeoSET repository states its changes to the code (`baselines/spade`, same licence).
baselines/spacenet6/spade/net_G.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b6714fb30bd34f62c8495af92bc225b96d9363cb8edc2839df3918784f2a4e6f
3
+ size 368775707
baselines/spacenet6/stegogan/README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # StegoGAN · SpaceNet6
2
+
3
+ StegoGAN (CVPR'24) retrained by the GeoSET authors on the SpaceNet6 split. These are the weights behind
4
+ the StegoGAN row for SpaceNet6 in Table 3 of the GeoSET paper
5
+ ([arXiv:2609.37496](https://arxiv.org/abs/2609.37496)).
6
+
7
+ - Method: Wu, Chen, Mermet, Hurni, Schindler, Gonthier and Landrieu, "StegoGAN: Leveraging steganography for non-bijective image-to-image translation", CVPR 2024.
8
+ - Implementation: [sian-wusidi/StegoGAN](https://github.com/sian-wusidi/StegoGAN) at commit `cad61997c0f82793444f60f81298142b80cdf3c1`.
9
+ - Training and inference code: [`baselines/stegogan`](https://github.com/KAIST-VICLab/GeoSET/tree/main/baselines/stegogan) in the GeoSET repository.
10
+
11
+ ## Files
12
+
13
+ | File | Bytes | SHA-256 | Role |
14
+ |---|---:|---|---|
15
+ | `net_G_A.pth` | 50,254,749 | `27aad6ddc483d83eba63c500080cf7f6b4a02205defbf468c7dd222ab4c445fa` | Generator G_A (`resnet_9blocks_maskv1`), SAR → EO; the output image is G_A applied to the SAR image alone (`fake_B_clean`) |
16
+ | `net_G_B.pth` | 52,615,525 | `425e3a0a0a08a0cd30d257f8085b8c33e68efb26a174f3443740e3f8d74bc6dc` | Generator G_B (`resnet_9blocks_maskv3`), EO → SAR; loaded by the upstream test script |
17
+
18
+ ## Results (GeoSET paper, Table 3)
19
+
20
+ SpaceNet6 test split, 495 pairs, 256 × 256.
21
+
22
+ | FID↓ | DISTS↓ | KID↓ | DINO↑ | LPIPS↓ | SSIM↑ | PSNR↑ |
23
+ |---:|---:|---:|---:|---:|---:|---:|
24
+ | 113.1 | 0.225 | 0.0361 | 0.649 | 0.388 | 0.457 | 16.04 |
25
+
26
+ ## Inference
27
+
28
+ ```bash
29
+ git clone https://github.com/KAIST-VICLab/GeoSET && cd GeoSET
30
+ export BASELINES_ROOT=/path/to/upstream_code DATA_ROOT=/path/to/data WORK_DIR=/path/to/work CKPT_ROOT=/path/to/checkpoints
31
+ bash baselines/stegogan/fetch.sh
32
+ bash baselines/pix2pix/fetch.sh # combine_A_and_B.py, used by gan_prepare.py
33
+ python scripts/datasets/build_spacenet6.py --data-root $DATA_ROOT
34
+ python baselines/data/gan_prepare.py spacenet6
35
+ hf download JeonghyeokDo/GeoSET --include "baselines/spacenet6/stegogan/*.pth" --local-dir hf
36
+ mkdir -p $CKPT_ROOT/stegogan/spacenet6 && cp hf/baselines/spacenet6/stegogan/*.pth $CKPT_ROOT/stegogan/spacenet6/
37
+ bash baselines/stegogan/test.sh spacenet6 # writes $WORK_DIR/results/stegogan/spacenet6/<stem>.png
38
+ python scripts/evaluate.py --dataset spacenet6 --pred-dir $WORK_DIR/results/stegogan/spacenet6
39
+ ```
40
+
41
+ ## Licence
42
+
43
+ The StegoGAN repository contains no licence file. These weights were trained by the GeoSET authors with that code; the GeoSET repository distributes only scripts that run the unmodified code.
baselines/spacenet6/stegogan/net_G_A.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27aad6ddc483d83eba63c500080cf7f6b4a02205defbf468c7dd222ab4c445fa
3
+ size 50254749
baselines/spacenet6/stegogan/net_G_B.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:425e3a0a0a08a0cd30d257f8085b8c33e68efb26a174f3443740e3f8d74bc6dc
3
+ size 52615525
generalist/autoencoder_flux2.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Black Forest Labs.
2
+ # Copyright (c) 2026 The GeoSET Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Adapted from black-forest-labs/flux2 (Apache-2.0). Modified by the GeoSET authors.
17
+ """Frozen FLUX.2 autoencoder with the packed (2x2 space-to-depth), BatchNorm-normalised 128-channel latent."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import torch
22
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
23
+ from diffusers.models.modeling_utils import ModelMixin
24
+ from einops import rearrange
25
+ from torch import Tensor, nn
26
+
27
+
28
+ def swish(x: Tensor) -> Tensor:
29
+ return x * torch.sigmoid(x)
30
+
31
+
32
+ class AttnBlock(nn.Module):
33
+ def __init__(self, in_channels: int):
34
+ super().__init__()
35
+ self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
36
+ self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
37
+ self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
38
+ self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
39
+ self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
40
+
41
+ def forward(self, x: Tensor) -> Tensor:
42
+ h_ = self.norm(x)
43
+ q, k, v = self.q(h_), self.k(h_), self.v(h_)
44
+ b, c, h, w = q.shape
45
+ q, k, v = (rearrange(t, "b c h w -> b 1 (h w) c").contiguous() for t in (q, k, v))
46
+ h_ = nn.functional.scaled_dot_product_attention(q, k, v)
47
+ return x + self.proj_out(rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b))
48
+
49
+
50
+ class ResnetBlock(nn.Module):
51
+ def __init__(self, in_channels: int, out_channels: int):
52
+ super().__init__()
53
+ self.in_channels = in_channels
54
+ self.out_channels = out_channels
55
+ self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
56
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
57
+ self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True)
58
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
59
+ if in_channels != out_channels:
60
+ self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
61
+
62
+ def forward(self, x: Tensor) -> Tensor:
63
+ h = self.conv1(swish(self.norm1(x)))
64
+ h = self.conv2(swish(self.norm2(h)))
65
+ if self.in_channels != self.out_channels:
66
+ x = self.nin_shortcut(x)
67
+ return x + h
68
+
69
+
70
+ class Downsample(nn.Module):
71
+ def __init__(self, in_channels: int):
72
+ super().__init__()
73
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
74
+
75
+ def forward(self, x: Tensor) -> Tensor:
76
+ return self.conv(nn.functional.pad(x, (0, 1, 0, 1), mode="constant", value=0))
77
+
78
+
79
+ class Upsample(nn.Module):
80
+ def __init__(self, in_channels: int):
81
+ super().__init__()
82
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
83
+
84
+ def forward(self, x: Tensor) -> Tensor:
85
+ return self.conv(nn.functional.interpolate(x, scale_factor=2.0, mode="nearest"))
86
+
87
+
88
+ class Encoder(nn.Module):
89
+ """Image -> latent moments (mean and log-variance, 2 * z_channels at 1/8 resolution)."""
90
+
91
+ def __init__(self, in_channels: int, ch: int, ch_mult: list[int], num_res_blocks: int, z_channels: int):
92
+ super().__init__()
93
+ self.quant_conv = nn.Conv2d(2 * z_channels, 2 * z_channels, 1)
94
+ self.num_resolutions = len(ch_mult)
95
+ self.num_res_blocks = num_res_blocks
96
+ self.conv_in = nn.Conv2d(in_channels, ch, kernel_size=3, stride=1, padding=1)
97
+ in_ch_mult = (1,) + tuple(ch_mult)
98
+ self.down = nn.ModuleList()
99
+ for i_level in range(self.num_resolutions):
100
+ block = nn.ModuleList()
101
+ block_in = ch * in_ch_mult[i_level]
102
+ block_out = ch * ch_mult[i_level]
103
+ for _ in range(num_res_blocks):
104
+ block.append(ResnetBlock(block_in, block_out))
105
+ block_in = block_out
106
+ down = nn.Module()
107
+ down.block = block
108
+ if i_level != self.num_resolutions - 1:
109
+ down.downsample = Downsample(block_in)
110
+ self.down.append(down)
111
+ self.mid = nn.Module()
112
+ self.mid.block_1 = ResnetBlock(block_in, block_in)
113
+ self.mid.attn_1 = AttnBlock(block_in)
114
+ self.mid.block_2 = ResnetBlock(block_in, block_in)
115
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
116
+ self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1)
117
+
118
+ def forward(self, x: Tensor) -> Tensor:
119
+ h = self.conv_in(x)
120
+ for i_level in range(self.num_resolutions):
121
+ for i_block in range(self.num_res_blocks):
122
+ h = self.down[i_level].block[i_block](h)
123
+ if i_level != self.num_resolutions - 1:
124
+ h = self.down[i_level].downsample(h)
125
+ h = self.mid.block_2(self.mid.attn_1(self.mid.block_1(h)))
126
+ return self.quant_conv(self.conv_out(swish(self.norm_out(h))))
127
+
128
+
129
+ class Decoder(nn.Module):
130
+ """Latent (z_channels at 1/8 resolution) -> image."""
131
+
132
+ def __init__(self, ch: int, out_ch: int, ch_mult: list[int], num_res_blocks: int, z_channels: int):
133
+ super().__init__()
134
+ self.post_quant_conv = nn.Conv2d(z_channels, z_channels, 1)
135
+ self.num_resolutions = len(ch_mult)
136
+ self.num_res_blocks = num_res_blocks
137
+ block_in = ch * ch_mult[-1]
138
+ self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
139
+ self.mid = nn.Module()
140
+ self.mid.block_1 = ResnetBlock(block_in, block_in)
141
+ self.mid.attn_1 = AttnBlock(block_in)
142
+ self.mid.block_2 = ResnetBlock(block_in, block_in)
143
+ self.up = nn.ModuleList()
144
+ for i_level in reversed(range(self.num_resolutions)):
145
+ block = nn.ModuleList()
146
+ block_out = ch * ch_mult[i_level]
147
+ for _ in range(num_res_blocks + 1):
148
+ block.append(ResnetBlock(block_in, block_out))
149
+ block_in = block_out
150
+ up = nn.Module()
151
+ up.block = block
152
+ if i_level != 0:
153
+ up.upsample = Upsample(block_in)
154
+ self.up.insert(0, up)
155
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
156
+ self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
157
+
158
+ def trunk(self, z: Tensor) -> Tensor:
159
+ """Every layer except ``conv_out``: returns the ``ch``-channel features at full resolution."""
160
+ h = self.conv_in(self.post_quant_conv(z))
161
+ h = self.mid.block_2(self.mid.attn_1(self.mid.block_1(h)))
162
+ h = h.to(next(self.up.parameters()).dtype)
163
+ for i_level in reversed(range(self.num_resolutions)):
164
+ for i_block in range(self.num_res_blocks + 1):
165
+ h = self.up[i_level].block[i_block](h)
166
+ if i_level != 0:
167
+ h = self.up[i_level].upsample(h)
168
+ return swish(self.norm_out(h))
169
+
170
+ def forward(self, z: Tensor) -> Tensor:
171
+ return self.conv_out(self.trunk(z))
172
+
173
+
174
+ class AutoencoderFlux2(ModelMixin, ConfigMixin):
175
+ """FLUX.2 autoencoder: ``encode`` maps ``[B, 3, H, W]`` in [-1, 1] to ``[B, 128, H/16, W/16]``, ``decode`` inverts it.
176
+
177
+ The latent is the posterior mean, packed 2x2 space-to-depth and normalised by a BatchNorm with fixed running
178
+ statistics. The module is frozen and always in eval mode.
179
+ """
180
+
181
+ @register_to_config
182
+ def __init__(
183
+ self,
184
+ in_channels: int = 3,
185
+ out_ch: int = 3,
186
+ ch: int = 128,
187
+ ch_mult: tuple[int, ...] = (1, 2, 4, 4),
188
+ num_res_blocks: int = 2,
189
+ z_channels: int = 32,
190
+ ):
191
+ super().__init__()
192
+ self.encoder = Encoder(in_channels, ch, list(ch_mult), num_res_blocks, z_channels)
193
+ self.decoder = Decoder(ch, out_ch, list(ch_mult), num_res_blocks, z_channels)
194
+ self.bn = nn.BatchNorm2d(4 * z_channels, eps=1e-4, momentum=0.1, affine=False, track_running_stats=True)
195
+ self.requires_grad_(False)
196
+ self.eval()
197
+
198
+ @staticmethod
199
+ def pack(z: Tensor) -> Tensor:
200
+ return rearrange(z, "... c (i pi) (j pj) -> ... (c pi pj) i j", pi=2, pj=2)
201
+
202
+ @staticmethod
203
+ def unpack(z: Tensor) -> Tensor:
204
+ return rearrange(z, "... (c pi pj) i j -> ... c (i pi) (j pj)", pi=2, pj=2)
205
+
206
+ def normalize(self, z: Tensor) -> Tensor:
207
+ self.bn.eval()
208
+ return self.bn(z)
209
+
210
+ def inv_normalize(self, z: Tensor) -> Tensor:
211
+ self.bn.eval()
212
+ s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + self.bn.eps)
213
+ m = self.bn.running_mean.view(1, -1, 1, 1)
214
+ return z * s + m
215
+
216
+ def encode(self, x: Tensor) -> Tensor:
217
+ moments = self.encoder(x)
218
+ return self.normalize(self.pack(torch.chunk(moments, 2, dim=1)[0]))
219
+
220
+ def decode(self, z: Tensor) -> Tensor:
221
+ return self.decoder(self.unpack(self.inv_normalize(z)))
222
+
223
+ def train(self, mode: bool = True) -> "AutoencoderFlux2":
224
+ return super().train(False)
generalist/lora.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LoRA for the GeoSET transformer: rank-r adapters on the attention and MLP projections of every block.
2
+
3
+ An adapter directory holds ``adapter_model.safetensors`` (``<module path>.lora_A`` ``[r, in]`` and
4
+ ``<module path>.lora_B`` ``[out, r]``, float32) and ``adapter_config.json``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import math
12
+ import os
13
+ from pathlib import Path
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from safetensors.torch import load_file, save_file
18
+ from torch import Tensor, nn
19
+
20
+ LORA_TARGETS = (
21
+ "img_attn.qkv", "img_attn.proj", "img_mlp.0", "img_mlp.2",
22
+ "txt_attn.qkv", "txt_attn.proj", "txt_mlp.0", "txt_mlp.2",
23
+ "linear1", "linear2",
24
+ )
25
+ _FINGERPRINT_TENSORS = ("img_in.weight", "final_layer.linear.weight", "time_in.in_layer.weight")
26
+
27
+
28
+ class LoRALinear(nn.Linear):
29
+ """``W x + (alpha / r) B A x`` sharing the base weight; ``A`` is Kaiming-uniform, ``B`` starts at zero."""
30
+
31
+ def __init__(self, base: nn.Linear, rank: int, alpha: float):
32
+ nn.Module.__init__(self)
33
+ self.in_features = base.in_features
34
+ self.out_features = base.out_features
35
+ self.weight = base.weight
36
+ self.register_parameter("bias", base.bias)
37
+ self.rank = int(rank)
38
+ self.alpha = float(alpha)
39
+ self.scale = self.alpha / self.rank
40
+ kw = dict(dtype=self.weight.dtype, device=self.weight.device)
41
+ self.lora_A = nn.Parameter(torch.empty(self.rank, self.in_features, **kw))
42
+ self.lora_B = nn.Parameter(torch.zeros(self.out_features, self.rank, **kw))
43
+ nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
44
+
45
+ def forward(self, x: Tensor) -> Tensor:
46
+ return F.linear(x, self.weight, self.bias) + F.linear(F.linear(x, self.lora_A), self.lora_B) * self.scale
47
+
48
+
49
+ def add_lora(model: nn.Module, rank: int = 16, alpha: float = 16.0) -> list[str]:
50
+ """Wrap the target projections in place and freeze every other parameter; returns the wrapped module paths."""
51
+ targets = [name for name, mod in model.named_modules()
52
+ if isinstance(mod, nn.Linear) and name.startswith(("double_blocks.", "single_blocks."))
53
+ and name.split(".", 2)[2] in LORA_TARGETS]
54
+ model.requires_grad_(False)
55
+ for path in targets:
56
+ parent, _, leaf = path.rpartition(".")
57
+ setattr(model.get_submodule(parent), leaf, LoRALinear(model.get_submodule(path), rank, alpha))
58
+ for p in lora_parameters(model).values():
59
+ p.requires_grad_(True)
60
+ return targets
61
+
62
+
63
+ def lora_parameters(model: nn.Module) -> dict[str, nn.Parameter]:
64
+ """The adapter parameters by name, in module order."""
65
+ return {n: p for n, p in model.named_parameters() if n.endswith((".lora_A", ".lora_B"))}
66
+
67
+
68
+ def check_lora_grads(model: nn.Module) -> int:
69
+ """Raise unless exactly the adapter parameters carry gradients; returns their count."""
70
+ params = dict(model.named_parameters())
71
+ adapter = lora_parameters(model)
72
+ wrong = [n for n, p in params.items() if p.grad is not None and n not in adapter]
73
+ dead = [n for n, p in adapter.items() if p.grad is None]
74
+ if wrong or dead or not adapter:
75
+ raise RuntimeError(f"LoRA gradient check failed: base tensors with grad {wrong[:3]}, adapter tensors without {dead[:3]}")
76
+ return len(adapter)
77
+
78
+
79
+ def base_fingerprint(model: nn.Module) -> str:
80
+ """sha256 over three base tensors that no adapter modifies."""
81
+ sd = model.state_dict()
82
+ h = hashlib.sha256()
83
+ for name in _FINGERPRINT_TENSORS:
84
+ h.update(name.encode())
85
+ h.update(sd[name].detach().float().cpu().contiguous().numpy().tobytes())
86
+ return h.hexdigest()
87
+
88
+
89
+ def save_lora(save_directory: str | os.PathLike, adapter: dict[str, Tensor], rank: int, alpha: float,
90
+ base_fingerprint: str) -> None:
91
+ """Write ``adapter_model.safetensors`` and ``adapter_config.json``."""
92
+ out = Path(save_directory)
93
+ out.mkdir(parents=True, exist_ok=True)
94
+ tensors = {k: v.detach().float().cpu().contiguous() for k, v in adapter.items()}
95
+ save_file(tensors, str(out / "adapter_model.safetensors"), metadata={"format": "pt"})
96
+ config = {
97
+ "rank": int(rank),
98
+ "alpha": float(alpha),
99
+ "target_modules": sorted({k.rsplit(".", 1)[0] for k in tensors}),
100
+ "base_fingerprint": base_fingerprint,
101
+ }
102
+ (out / "adapter_config.json").write_text(json.dumps(config, indent=2) + "\n")
103
+
104
+
105
+ def load_lora(path: str | os.PathLike) -> tuple[dict[str, Tensor], dict]:
106
+ """Read an adapter directory: ``(tensors, config)``."""
107
+ path = Path(path)
108
+ return load_file(str(path / "adapter_model.safetensors")), json.loads((path / "adapter_config.json").read_text())
109
+
110
+
111
+ @torch.no_grad()
112
+ def merge_lora(model: nn.Module, path: str | os.PathLike) -> dict:
113
+ """Fold an adapter into an unadapted model in place: ``W += (alpha / r) B A`` (float32); returns a report."""
114
+ if getattr(model, "_lora_merged", None):
115
+ raise RuntimeError(f"{model._lora_merged} is already merged into this model")
116
+ tensors, cfg = load_lora(path)
117
+ if base_fingerprint(model) != cfg["base_fingerprint"]:
118
+ raise RuntimeError("this adapter was trained on different base weights")
119
+ scale = float(cfg["alpha"]) / float(cfg["rank"])
120
+ params = dict(model.named_parameters())
121
+ num = den = 0.0
122
+ for name in cfg["target_modules"]:
123
+ w = params[f"{name}.weight"]
124
+ d = (tensors[f"{name}.lora_B"].float() @ tensors[f"{name}.lora_A"].float()) * scale
125
+ num += float(d.pow(2).sum())
126
+ den += float(w.detach().float().pow(2).sum())
127
+ w.add_(d.to(w.dtype, copy=False).to(w.device))
128
+ model._lora_merged = str(path)
129
+ return {"rank": cfg["rank"], "alpha": cfg["alpha"], "merged_modules": len(cfg["target_modules"]),
130
+ "delta_rel_fro": math.sqrt(num / den)}
generalist/model_index.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "GeoSETPipeline",
3
+ "_diffusers_version": "0.37.1",
4
+ "sar_encoder": [
5
+ "sar_encoder_geoset",
6
+ "GeoSETSAREncoder"
7
+ ],
8
+ "scheduler": [
9
+ "scheduler_flow_bridge",
10
+ "FlowBridgeScheduler"
11
+ ],
12
+ "transformer": [
13
+ "transformer_geoset",
14
+ "GeoSETTransformer2DModel"
15
+ ],
16
+ "vae": [
17
+ "autoencoder_flux2",
18
+ "AutoencoderFlux2"
19
+ ]
20
+ }
generalist/pipeline.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GeoSET SAR-to-EO translation pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional, Union
8
+
9
+ import numpy as np
10
+ import torch
11
+ from diffusers.models.modeling_utils import ModelMixin
12
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline, ImagePipelineOutput
13
+ from diffusers.schedulers.scheduling_utils import SchedulerMixin
14
+ from diffusers.utils.torch_utils import randn_tensor
15
+ from huggingface_hub import snapshot_download
16
+ from PIL import Image
17
+
18
+ from .lora import merge_lora
19
+
20
+ HUB_REPO = "JeonghyeokDo/GeoSET"
21
+ LORA_ADAPTERS = ("qxs-saropt", "sar2opt", "sar2eo", "spacenet6")
22
+
23
+
24
+ def sar_to_tensor(image: Image.Image) -> torch.Tensor:
25
+ """8-bit SAR image -> ``[1, H, W]`` float32 in [-1, 1] (colour inputs are reduced to luminance)."""
26
+ return torch.from_numpy(np.asarray(image.convert("L"), np.float32)[None] / 127.5 - 1.0)
27
+
28
+
29
+ class GeoSETPipeline(DiffusionPipeline):
30
+ """SAR image -> EO image.
31
+
32
+ Components: ``transformer`` (GeoSETTransformer2DModel), ``sar_encoder`` (GeoSETSAREncoder), ``vae``
33
+ (AutoencoderFlux2) and ``scheduler`` (FlowBridgeScheduler). The SAR encoder maps the input to the condition
34
+ latent ``z_s``; the flow bridge is integrated from Gaussian noise with classifier-free guidance
35
+ ``v_u + w (v_c - v_u)`` (all-zero null condition) and the result is decoded by the FLUX.2 autoencoder. The
36
+ transformer and the SAR encoder run in float32 (the transformer under bfloat16 autocast on CUDA); the
37
+ autoencoder decodes in bfloat16.
38
+ """
39
+
40
+ model_cpu_offload_seq = "sar_encoder->transformer->vae"
41
+
42
+ def __init__(
43
+ self,
44
+ transformer: ModelMixin,
45
+ sar_encoder: ModelMixin,
46
+ vae: ModelMixin,
47
+ scheduler: SchedulerMixin,
48
+ ):
49
+ super().__init__()
50
+ vae.to(dtype=torch.bfloat16, memory_format=torch.channels_last)
51
+ self.register_modules(transformer=transformer, sar_encoder=sar_encoder, vae=vae, scheduler=scheduler)
52
+
53
+ def load_lora(self, adapter: Union[str, os.PathLike], repo_id: str = HUB_REPO) -> dict:
54
+ """Merge a dataset adapter into the transformer: a released key (``LORA_ADAPTERS``) or an adapter directory."""
55
+ path = Path(adapter)
56
+ if not path.is_dir():
57
+ if adapter not in LORA_ADAPTERS:
58
+ raise ValueError(f"{adapter!r} is neither an adapter directory nor one of {LORA_ADAPTERS}")
59
+ root = Path(self.name_or_path).parent if self.name_or_path else None
60
+ if root is None or not (root / "lora" / adapter).is_dir():
61
+ root = Path(snapshot_download(repo_id, allow_patterns=[f"lora/{adapter}/*"]))
62
+ path = root / "lora" / adapter
63
+ return merge_lora(self.transformer, path)
64
+
65
+ @staticmethod
66
+ def preprocess(image: Union[Image.Image, list, torch.Tensor]) -> torch.Tensor:
67
+ """PIL image(s) -> ``[B, 1, H, W]`` in [-1, 1]; tensors ``[C, H, W]`` / ``[B, C, H, W]`` must be in [-1, 1]."""
68
+ if isinstance(image, torch.Tensor):
69
+ return image if image.ndim == 4 else image[None]
70
+ images = image if isinstance(image, (list, tuple)) else [image]
71
+ return torch.stack([sar_to_tensor(im) for im in images])
72
+
73
+ @torch.no_grad()
74
+ def __call__(
75
+ self,
76
+ image: Union[Image.Image, list, torch.Tensor],
77
+ num_inference_steps: int = 50,
78
+ guidance_scale: float = 2.0,
79
+ generator: Optional[Union[torch.Generator, list]] = None,
80
+ latents: Optional[torch.Tensor] = None,
81
+ output_type: str = "pil",
82
+ return_dict: bool = True,
83
+ ) -> Union[ImagePipelineOutput, tuple]:
84
+ """Translate SAR image(s); height and width must be multiples of 16.
85
+
86
+ Returns EO images as PIL (8-bit RGB), or as float tensors / arrays in [0, 1] (``"pt"`` ``[B, 3, H, W]``,
87
+ ``"np"`` ``[B, H, W, 3]``).
88
+ """
89
+ if output_type not in ("pil", "np", "pt"):
90
+ raise ValueError(f"output_type must be 'pil', 'np' or 'pt', got {output_type!r}")
91
+ device = self._execution_device
92
+ sar = self.preprocess(image).to(device=device, dtype=torch.float32)
93
+ if sar.shape[-2] % 16 or sar.shape[-1] % 16:
94
+ raise ValueError(f"SAR size {tuple(sar.shape[-2:])} must be a multiple of 16")
95
+ z_s = self.sar_encoder(sar)
96
+ if latents is None:
97
+ latents = randn_tensor(z_s.shape, generator=generator, device=device, dtype=torch.float32)
98
+ latents = latents.to(device=device, dtype=torch.float32)
99
+ null = torch.zeros_like(z_s)
100
+
101
+ self.scheduler.set_timesteps(num_inference_steps, device=device)
102
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=device.type == "cuda"):
103
+ for t in self.progress_bar(self.scheduler.timesteps):
104
+ t_batch = t.expand(latents.shape[0])
105
+ v = self.transformer(latents, t_batch, z_s)
106
+ if guidance_scale != 1.0:
107
+ v_u = self.transformer(latents, t_batch, null)
108
+ v = v_u + guidance_scale * (v - v_u)
109
+ latents = self.scheduler.step(v, t, latents, return_dict=False)[0]
110
+ image = self.vae.decode(latents.to(self.vae.dtype)).float()
111
+ image = ((image + 1) / 2).clamp(0, 1)
112
+ self.maybe_free_model_hooks()
113
+
114
+ if output_type == "pil":
115
+ arr = (image * 255).round().to(torch.uint8).permute(0, 2, 3, 1).cpu().numpy()
116
+ image = [Image.fromarray(a) for a in arr]
117
+ elif output_type == "np":
118
+ image = image.permute(0, 2, 3, 1).cpu().numpy()
119
+ if not return_dict:
120
+ return (image,)
121
+ return ImagePipelineOutput(images=image)
generalist/pipeline_geoset.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GeoSET SAR-to-EO translation pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional, Union
8
+
9
+ import numpy as np
10
+ import torch
11
+ from diffusers.models.modeling_utils import ModelMixin
12
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline, ImagePipelineOutput
13
+ from diffusers.schedulers.scheduling_utils import SchedulerMixin
14
+ from diffusers.utils.torch_utils import randn_tensor
15
+ from huggingface_hub import snapshot_download
16
+ from PIL import Image
17
+
18
+ from .lora import merge_lora
19
+
20
+ HUB_REPO = "JeonghyeokDo/GeoSET"
21
+ LORA_ADAPTERS = ("qxs-saropt", "sar2opt", "sar2eo", "spacenet6")
22
+
23
+
24
+ def sar_to_tensor(image: Image.Image) -> torch.Tensor:
25
+ """8-bit SAR image -> ``[1, H, W]`` float32 in [-1, 1] (colour inputs are reduced to luminance)."""
26
+ return torch.from_numpy(np.asarray(image.convert("L"), np.float32)[None] / 127.5 - 1.0)
27
+
28
+
29
+ class GeoSETPipeline(DiffusionPipeline):
30
+ """SAR image -> EO image.
31
+
32
+ Components: ``transformer`` (GeoSETTransformer2DModel), ``sar_encoder`` (GeoSETSAREncoder), ``vae``
33
+ (AutoencoderFlux2) and ``scheduler`` (FlowBridgeScheduler). The SAR encoder maps the input to the condition
34
+ latent ``z_s``; the flow bridge is integrated from Gaussian noise with classifier-free guidance
35
+ ``v_u + w (v_c - v_u)`` (all-zero null condition) and the result is decoded by the FLUX.2 autoencoder. The
36
+ transformer and the SAR encoder run in float32 (the transformer under bfloat16 autocast on CUDA); the
37
+ autoencoder decodes in bfloat16.
38
+ """
39
+
40
+ model_cpu_offload_seq = "sar_encoder->transformer->vae"
41
+
42
+ def __init__(
43
+ self,
44
+ transformer: ModelMixin,
45
+ sar_encoder: ModelMixin,
46
+ vae: ModelMixin,
47
+ scheduler: SchedulerMixin,
48
+ ):
49
+ super().__init__()
50
+ vae.to(dtype=torch.bfloat16, memory_format=torch.channels_last)
51
+ self.register_modules(transformer=transformer, sar_encoder=sar_encoder, vae=vae, scheduler=scheduler)
52
+
53
+ def load_lora(self, adapter: Union[str, os.PathLike], repo_id: str = HUB_REPO) -> dict:
54
+ """Merge a dataset adapter into the transformer: a released key (``LORA_ADAPTERS``) or an adapter directory."""
55
+ path = Path(adapter)
56
+ if not path.is_dir():
57
+ if adapter not in LORA_ADAPTERS:
58
+ raise ValueError(f"{adapter!r} is neither an adapter directory nor one of {LORA_ADAPTERS}")
59
+ root = Path(self.name_or_path).parent if self.name_or_path else None
60
+ if root is None or not (root / "lora" / adapter).is_dir():
61
+ root = Path(snapshot_download(repo_id, allow_patterns=[f"lora/{adapter}/*"]))
62
+ path = root / "lora" / adapter
63
+ return merge_lora(self.transformer, path)
64
+
65
+ @staticmethod
66
+ def preprocess(image: Union[Image.Image, list, torch.Tensor]) -> torch.Tensor:
67
+ """PIL image(s) -> ``[B, 1, H, W]`` in [-1, 1]; tensors ``[C, H, W]`` / ``[B, C, H, W]`` must be in [-1, 1]."""
68
+ if isinstance(image, torch.Tensor):
69
+ return image if image.ndim == 4 else image[None]
70
+ images = image if isinstance(image, (list, tuple)) else [image]
71
+ return torch.stack([sar_to_tensor(im) for im in images])
72
+
73
+ @torch.no_grad()
74
+ def __call__(
75
+ self,
76
+ image: Union[Image.Image, list, torch.Tensor],
77
+ num_inference_steps: int = 50,
78
+ guidance_scale: float = 2.0,
79
+ generator: Optional[Union[torch.Generator, list]] = None,
80
+ latents: Optional[torch.Tensor] = None,
81
+ output_type: str = "pil",
82
+ return_dict: bool = True,
83
+ ) -> Union[ImagePipelineOutput, tuple]:
84
+ """Translate SAR image(s); height and width must be multiples of 16.
85
+
86
+ Returns EO images as PIL (8-bit RGB), or as float tensors / arrays in [0, 1] (``"pt"`` ``[B, 3, H, W]``,
87
+ ``"np"`` ``[B, H, W, 3]``).
88
+ """
89
+ if output_type not in ("pil", "np", "pt"):
90
+ raise ValueError(f"output_type must be 'pil', 'np' or 'pt', got {output_type!r}")
91
+ device = self._execution_device
92
+ sar = self.preprocess(image).to(device=device, dtype=torch.float32)
93
+ if sar.shape[-2] % 16 or sar.shape[-1] % 16:
94
+ raise ValueError(f"SAR size {tuple(sar.shape[-2:])} must be a multiple of 16")
95
+ z_s = self.sar_encoder(sar)
96
+ if latents is None:
97
+ latents = randn_tensor(z_s.shape, generator=generator, device=device, dtype=torch.float32)
98
+ latents = latents.to(device=device, dtype=torch.float32)
99
+ null = torch.zeros_like(z_s)
100
+
101
+ self.scheduler.set_timesteps(num_inference_steps, device=device)
102
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=device.type == "cuda"):
103
+ for t in self.progress_bar(self.scheduler.timesteps):
104
+ t_batch = t.expand(latents.shape[0])
105
+ v = self.transformer(latents, t_batch, z_s)
106
+ if guidance_scale != 1.0:
107
+ v_u = self.transformer(latents, t_batch, null)
108
+ v = v_u + guidance_scale * (v - v_u)
109
+ latents = self.scheduler.step(v, t, latents, return_dict=False)[0]
110
+ image = self.vae.decode(latents.to(self.vae.dtype)).float()
111
+ image = ((image + 1) / 2).clamp(0, 1)
112
+ self.maybe_free_model_hooks()
113
+
114
+ if output_type == "pil":
115
+ arr = (image * 255).round().to(torch.uint8).permute(0, 2, 3, 1).cpu().numpy()
116
+ image = [Image.fromarray(a) for a in arr]
117
+ elif output_type == "np":
118
+ image = image.permute(0, 2, 3, 1).cpu().numpy()
119
+ if not return_dict:
120
+ return (image,)
121
+ return ImagePipelineOutput(images=image)
generalist/sar_encoder/autoencoder_flux2.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Black Forest Labs.
2
+ # Copyright (c) 2026 The GeoSET Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Adapted from black-forest-labs/flux2 (Apache-2.0). Modified by the GeoSET authors.
17
+ """Frozen FLUX.2 autoencoder with the packed (2x2 space-to-depth), BatchNorm-normalised 128-channel latent."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import torch
22
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
23
+ from diffusers.models.modeling_utils import ModelMixin
24
+ from einops import rearrange
25
+ from torch import Tensor, nn
26
+
27
+
28
+ def swish(x: Tensor) -> Tensor:
29
+ return x * torch.sigmoid(x)
30
+
31
+
32
+ class AttnBlock(nn.Module):
33
+ def __init__(self, in_channels: int):
34
+ super().__init__()
35
+ self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
36
+ self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
37
+ self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
38
+ self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
39
+ self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
40
+
41
+ def forward(self, x: Tensor) -> Tensor:
42
+ h_ = self.norm(x)
43
+ q, k, v = self.q(h_), self.k(h_), self.v(h_)
44
+ b, c, h, w = q.shape
45
+ q, k, v = (rearrange(t, "b c h w -> b 1 (h w) c").contiguous() for t in (q, k, v))
46
+ h_ = nn.functional.scaled_dot_product_attention(q, k, v)
47
+ return x + self.proj_out(rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b))
48
+
49
+
50
+ class ResnetBlock(nn.Module):
51
+ def __init__(self, in_channels: int, out_channels: int):
52
+ super().__init__()
53
+ self.in_channels = in_channels
54
+ self.out_channels = out_channels
55
+ self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
56
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
57
+ self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True)
58
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
59
+ if in_channels != out_channels:
60
+ self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
61
+
62
+ def forward(self, x: Tensor) -> Tensor:
63
+ h = self.conv1(swish(self.norm1(x)))
64
+ h = self.conv2(swish(self.norm2(h)))
65
+ if self.in_channels != self.out_channels:
66
+ x = self.nin_shortcut(x)
67
+ return x + h
68
+
69
+
70
+ class Downsample(nn.Module):
71
+ def __init__(self, in_channels: int):
72
+ super().__init__()
73
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
74
+
75
+ def forward(self, x: Tensor) -> Tensor:
76
+ return self.conv(nn.functional.pad(x, (0, 1, 0, 1), mode="constant", value=0))
77
+
78
+
79
+ class Upsample(nn.Module):
80
+ def __init__(self, in_channels: int):
81
+ super().__init__()
82
+ self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
83
+
84
+ def forward(self, x: Tensor) -> Tensor:
85
+ return self.conv(nn.functional.interpolate(x, scale_factor=2.0, mode="nearest"))
86
+
87
+
88
+ class Encoder(nn.Module):
89
+ """Image -> latent moments (mean and log-variance, 2 * z_channels at 1/8 resolution)."""
90
+
91
+ def __init__(self, in_channels: int, ch: int, ch_mult: list[int], num_res_blocks: int, z_channels: int):
92
+ super().__init__()
93
+ self.quant_conv = nn.Conv2d(2 * z_channels, 2 * z_channels, 1)
94
+ self.num_resolutions = len(ch_mult)
95
+ self.num_res_blocks = num_res_blocks
96
+ self.conv_in = nn.Conv2d(in_channels, ch, kernel_size=3, stride=1, padding=1)
97
+ in_ch_mult = (1,) + tuple(ch_mult)
98
+ self.down = nn.ModuleList()
99
+ for i_level in range(self.num_resolutions):
100
+ block = nn.ModuleList()
101
+ block_in = ch * in_ch_mult[i_level]
102
+ block_out = ch * ch_mult[i_level]
103
+ for _ in range(num_res_blocks):
104
+ block.append(ResnetBlock(block_in, block_out))
105
+ block_in = block_out
106
+ down = nn.Module()
107
+ down.block = block
108
+ if i_level != self.num_resolutions - 1:
109
+ down.downsample = Downsample(block_in)
110
+ self.down.append(down)
111
+ self.mid = nn.Module()
112
+ self.mid.block_1 = ResnetBlock(block_in, block_in)
113
+ self.mid.attn_1 = AttnBlock(block_in)
114
+ self.mid.block_2 = ResnetBlock(block_in, block_in)
115
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
116
+ self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1)
117
+
118
+ def forward(self, x: Tensor) -> Tensor:
119
+ h = self.conv_in(x)
120
+ for i_level in range(self.num_resolutions):
121
+ for i_block in range(self.num_res_blocks):
122
+ h = self.down[i_level].block[i_block](h)
123
+ if i_level != self.num_resolutions - 1:
124
+ h = self.down[i_level].downsample(h)
125
+ h = self.mid.block_2(self.mid.attn_1(self.mid.block_1(h)))
126
+ return self.quant_conv(self.conv_out(swish(self.norm_out(h))))
127
+
128
+
129
+ class Decoder(nn.Module):
130
+ """Latent (z_channels at 1/8 resolution) -> image."""
131
+
132
+ def __init__(self, ch: int, out_ch: int, ch_mult: list[int], num_res_blocks: int, z_channels: int):
133
+ super().__init__()
134
+ self.post_quant_conv = nn.Conv2d(z_channels, z_channels, 1)
135
+ self.num_resolutions = len(ch_mult)
136
+ self.num_res_blocks = num_res_blocks
137
+ block_in = ch * ch_mult[-1]
138
+ self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
139
+ self.mid = nn.Module()
140
+ self.mid.block_1 = ResnetBlock(block_in, block_in)
141
+ self.mid.attn_1 = AttnBlock(block_in)
142
+ self.mid.block_2 = ResnetBlock(block_in, block_in)
143
+ self.up = nn.ModuleList()
144
+ for i_level in reversed(range(self.num_resolutions)):
145
+ block = nn.ModuleList()
146
+ block_out = ch * ch_mult[i_level]
147
+ for _ in range(num_res_blocks + 1):
148
+ block.append(ResnetBlock(block_in, block_out))
149
+ block_in = block_out
150
+ up = nn.Module()
151
+ up.block = block
152
+ if i_level != 0:
153
+ up.upsample = Upsample(block_in)
154
+ self.up.insert(0, up)
155
+ self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True)
156
+ self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
157
+
158
+ def trunk(self, z: Tensor) -> Tensor:
159
+ """Every layer except ``conv_out``: returns the ``ch``-channel features at full resolution."""
160
+ h = self.conv_in(self.post_quant_conv(z))
161
+ h = self.mid.block_2(self.mid.attn_1(self.mid.block_1(h)))
162
+ h = h.to(next(self.up.parameters()).dtype)
163
+ for i_level in reversed(range(self.num_resolutions)):
164
+ for i_block in range(self.num_res_blocks + 1):
165
+ h = self.up[i_level].block[i_block](h)
166
+ if i_level != 0:
167
+ h = self.up[i_level].upsample(h)
168
+ return swish(self.norm_out(h))
169
+
170
+ def forward(self, z: Tensor) -> Tensor:
171
+ return self.conv_out(self.trunk(z))
172
+
173
+
174
+ class AutoencoderFlux2(ModelMixin, ConfigMixin):
175
+ """FLUX.2 autoencoder: ``encode`` maps ``[B, 3, H, W]`` in [-1, 1] to ``[B, 128, H/16, W/16]``, ``decode`` inverts it.
176
+
177
+ The latent is the posterior mean, packed 2x2 space-to-depth and normalised by a BatchNorm with fixed running
178
+ statistics. The module is frozen and always in eval mode.
179
+ """
180
+
181
+ @register_to_config
182
+ def __init__(
183
+ self,
184
+ in_channels: int = 3,
185
+ out_ch: int = 3,
186
+ ch: int = 128,
187
+ ch_mult: tuple[int, ...] = (1, 2, 4, 4),
188
+ num_res_blocks: int = 2,
189
+ z_channels: int = 32,
190
+ ):
191
+ super().__init__()
192
+ self.encoder = Encoder(in_channels, ch, list(ch_mult), num_res_blocks, z_channels)
193
+ self.decoder = Decoder(ch, out_ch, list(ch_mult), num_res_blocks, z_channels)
194
+ self.bn = nn.BatchNorm2d(4 * z_channels, eps=1e-4, momentum=0.1, affine=False, track_running_stats=True)
195
+ self.requires_grad_(False)
196
+ self.eval()
197
+
198
+ @staticmethod
199
+ def pack(z: Tensor) -> Tensor:
200
+ return rearrange(z, "... c (i pi) (j pj) -> ... (c pi pj) i j", pi=2, pj=2)
201
+
202
+ @staticmethod
203
+ def unpack(z: Tensor) -> Tensor:
204
+ return rearrange(z, "... (c pi pj) i j -> ... c (i pi) (j pj)", pi=2, pj=2)
205
+
206
+ def normalize(self, z: Tensor) -> Tensor:
207
+ self.bn.eval()
208
+ return self.bn(z)
209
+
210
+ def inv_normalize(self, z: Tensor) -> Tensor:
211
+ self.bn.eval()
212
+ s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + self.bn.eps)
213
+ m = self.bn.running_mean.view(1, -1, 1, 1)
214
+ return z * s + m
215
+
216
+ def encode(self, x: Tensor) -> Tensor:
217
+ moments = self.encoder(x)
218
+ return self.normalize(self.pack(torch.chunk(moments, 2, dim=1)[0]))
219
+
220
+ def decode(self, z: Tensor) -> Tensor:
221
+ return self.decoder(self.unpack(self.inv_normalize(z)))
222
+
223
+ def train(self, mode: bool = True) -> "AutoencoderFlux2":
224
+ return super().train(False)
generalist/sar_encoder/config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "GeoSETSAREncoder",
3
+ "_diffusers_version": "0.37.1",
4
+ "ch": 128,
5
+ "ch_mult": [
6
+ 1,
7
+ 2,
8
+ 4,
9
+ 4
10
+ ],
11
+ "num_res_blocks": 2,
12
+ "z_channels": 32
13
+ }
generalist/sar_encoder/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:49d44c88ed075c262115ced4ffb3f1e010cd6ce84423b85bea3e0243d61694f4
3
+ size 137749092
generalist/sar_encoder/sar_encoder_geoset.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Black Forest Labs.
2
+ # Copyright (c) 2026 The GeoSET Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Adapted from black-forest-labs/flux2 (Apache-2.0). Modified by the GeoSET authors.
17
+ """Speckle-robust SAR encoder (stage 1): the FLUX.2 encoder trunk with channel-specific input and output stems."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import torch
22
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
23
+ from diffusers.models.modeling_utils import ModelMixin
24
+ from torch import Tensor, nn
25
+
26
+ from .autoencoder_flux2 import AutoencoderFlux2, Encoder
27
+
28
+ _CHUNK = 32
29
+
30
+
31
+ class GeoSETSAREncoder(ModelMixin, ConfigMixin):
32
+ """SAR image ``[B, 1|2, H, W]`` in [-1, 1] -> SAR latent ``z_s`` ``[B, 128, H/16, W/16]``.
33
+
34
+ Stems are keyed by input channels: ``"3"`` takes single-channel SAR folded to three channels, ``"2"`` takes
35
+ VV/VH. The latent is normalised with the autoencoder's fixed BatchNorm statistics. ``stem_out`` (stage-1
36
+ reconstruction heads) is not used by ``forward``.
37
+ """
38
+
39
+ @register_to_config
40
+ def __init__(
41
+ self,
42
+ ch: int = 128,
43
+ ch_mult: tuple[int, ...] = (1, 2, 4, 4),
44
+ num_res_blocks: int = 2,
45
+ z_channels: int = 32,
46
+ ):
47
+ super().__init__()
48
+ self.encoder = Encoder(3, ch, list(ch_mult), num_res_blocks, z_channels)
49
+ self.encoder.conv_in = nn.Identity()
50
+ self.bn = nn.BatchNorm2d(4 * z_channels, eps=1e-4, momentum=0.1, affine=False, track_running_stats=True)
51
+ self.bn.eval()
52
+ self.stem_in = nn.ModuleDict({"3": nn.Conv2d(3, ch, 3, 1, 1), "2": nn.Conv2d(2, ch, 3, 1, 1)})
53
+ self.stem_out = nn.ModuleDict({"3": nn.Conv2d(ch, 3, 3, 1, 1), "2": nn.Conv2d(ch, 2, 3, 1, 1)})
54
+
55
+ def train(self, mode: bool = True) -> "GeoSETSAREncoder":
56
+ super().train(mode)
57
+ self.bn.eval()
58
+ return self
59
+
60
+ def encode(self, x: Tensor) -> tuple[Tensor, Tensor]:
61
+ """``[B, 3|2, H, W]`` -> ``(z, z_raw)``: the BatchNorm-normalised and the raw packed posterior mean."""
62
+ moments = self.encoder(self.stem_in[str(x.shape[1])](x))
63
+ z_raw = AutoencoderFlux2.pack(torch.chunk(moments, 2, dim=1)[0])
64
+ return self.bn(z_raw), z_raw
65
+
66
+ def forward(self, sar: Tensor) -> Tensor:
67
+ if sar.shape[1] == 1:
68
+ sar = sar.repeat(1, 3, 1, 1)
69
+ return torch.cat([self.encode(c)[0] for c in sar.split(_CHUNK)], dim=0).float()
70
+
71
+ def reconstruct(self, x: Tensor, vae: AutoencoderFlux2) -> Tensor:
72
+ """Stage-1 reconstruction ``[B, 3|2, H, W]`` -> ``[B, 3|2, H, W]`` through the frozen decoder trunk."""
73
+ _, z_raw = self.encode(x)
74
+ return self.stem_out[str(x.shape[1])](vae.decoder.trunk(AutoencoderFlux2.unpack(z_raw)))
75
+
76
+ @classmethod
77
+ @torch.no_grad()
78
+ def from_autoencoder(cls, vae: AutoencoderFlux2) -> "GeoSETSAREncoder":
79
+ """Stage-1 initialisation from the FLUX.2 autoencoder.
80
+
81
+ The trunk and latent BatchNorm are copies of the encoder's; ``conv_in`` and ``conv_out`` become the
82
+ three-channel stems; the two-channel stems start from their input-channel sum and output-channel mean.
83
+ Trainable: the trunk, both input stems and ``stem_out["2"]``; ``stem_out["3"]`` and the BatchNorm stay fixed.
84
+ """
85
+ c = vae.config
86
+ model = cls(ch=c.ch, ch_mult=c.ch_mult, num_res_blocks=c.num_res_blocks, z_channels=c.z_channels)
87
+ model.encoder.load_state_dict({k: v for k, v in vae.encoder.state_dict().items()
88
+ if not k.startswith("conv_in.")})
89
+ model.bn.load_state_dict(vae.bn.state_dict())
90
+ w_in, b_in = vae.encoder.conv_in.weight.float(), vae.encoder.conv_in.bias.float()
91
+ w_out, b_out = vae.decoder.conv_out.weight.float(), vae.decoder.conv_out.bias.float()
92
+ stems = {
93
+ ("stem_in", "3"): (w_in, b_in),
94
+ ("stem_in", "2"): (w_in.sum(dim=1, keepdim=True).repeat(1, 2, 1, 1) / 2, b_in.clone()),
95
+ ("stem_out", "3"): (w_out, b_out),
96
+ ("stem_out", "2"): (w_out.mean(dim=0, keepdim=True).repeat(2, 1, 1, 1), b_out.mean().repeat(2)),
97
+ }
98
+ for (group, key), (w, b) in stems.items():
99
+ getattr(model, group)[key].weight.copy_(w)
100
+ getattr(model, group)[key].bias.copy_(b)
101
+ model.stem_out["3"].requires_grad_(False)
102
+ return model
generalist/sar_encoder_geoset.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Black Forest Labs.
2
+ # Copyright (c) 2026 The GeoSET Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Adapted from black-forest-labs/flux2 (Apache-2.0). Modified by the GeoSET authors.
17
+ """Speckle-robust SAR encoder (stage 1): the FLUX.2 encoder trunk with channel-specific input and output stems."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import torch
22
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
23
+ from diffusers.models.modeling_utils import ModelMixin
24
+ from torch import Tensor, nn
25
+
26
+ from .autoencoder_flux2 import AutoencoderFlux2, Encoder
27
+
28
+ _CHUNK = 32
29
+
30
+
31
+ class GeoSETSAREncoder(ModelMixin, ConfigMixin):
32
+ """SAR image ``[B, 1|2, H, W]`` in [-1, 1] -> SAR latent ``z_s`` ``[B, 128, H/16, W/16]``.
33
+
34
+ Stems are keyed by input channels: ``"3"`` takes single-channel SAR folded to three channels, ``"2"`` takes
35
+ VV/VH. The latent is normalised with the autoencoder's fixed BatchNorm statistics. ``stem_out`` (stage-1
36
+ reconstruction heads) is not used by ``forward``.
37
+ """
38
+
39
+ @register_to_config
40
+ def __init__(
41
+ self,
42
+ ch: int = 128,
43
+ ch_mult: tuple[int, ...] = (1, 2, 4, 4),
44
+ num_res_blocks: int = 2,
45
+ z_channels: int = 32,
46
+ ):
47
+ super().__init__()
48
+ self.encoder = Encoder(3, ch, list(ch_mult), num_res_blocks, z_channels)
49
+ self.encoder.conv_in = nn.Identity()
50
+ self.bn = nn.BatchNorm2d(4 * z_channels, eps=1e-4, momentum=0.1, affine=False, track_running_stats=True)
51
+ self.bn.eval()
52
+ self.stem_in = nn.ModuleDict({"3": nn.Conv2d(3, ch, 3, 1, 1), "2": nn.Conv2d(2, ch, 3, 1, 1)})
53
+ self.stem_out = nn.ModuleDict({"3": nn.Conv2d(ch, 3, 3, 1, 1), "2": nn.Conv2d(ch, 2, 3, 1, 1)})
54
+
55
+ def train(self, mode: bool = True) -> "GeoSETSAREncoder":
56
+ super().train(mode)
57
+ self.bn.eval()
58
+ return self
59
+
60
+ def encode(self, x: Tensor) -> tuple[Tensor, Tensor]:
61
+ """``[B, 3|2, H, W]`` -> ``(z, z_raw)``: the BatchNorm-normalised and the raw packed posterior mean."""
62
+ moments = self.encoder(self.stem_in[str(x.shape[1])](x))
63
+ z_raw = AutoencoderFlux2.pack(torch.chunk(moments, 2, dim=1)[0])
64
+ return self.bn(z_raw), z_raw
65
+
66
+ def forward(self, sar: Tensor) -> Tensor:
67
+ if sar.shape[1] == 1:
68
+ sar = sar.repeat(1, 3, 1, 1)
69
+ return torch.cat([self.encode(c)[0] for c in sar.split(_CHUNK)], dim=0).float()
70
+
71
+ def reconstruct(self, x: Tensor, vae: AutoencoderFlux2) -> Tensor:
72
+ """Stage-1 reconstruction ``[B, 3|2, H, W]`` -> ``[B, 3|2, H, W]`` through the frozen decoder trunk."""
73
+ _, z_raw = self.encode(x)
74
+ return self.stem_out[str(x.shape[1])](vae.decoder.trunk(AutoencoderFlux2.unpack(z_raw)))
75
+
76
+ @classmethod
77
+ @torch.no_grad()
78
+ def from_autoencoder(cls, vae: AutoencoderFlux2) -> "GeoSETSAREncoder":
79
+ """Stage-1 initialisation from the FLUX.2 autoencoder.
80
+
81
+ The trunk and latent BatchNorm are copies of the encoder's; ``conv_in`` and ``conv_out`` become the
82
+ three-channel stems; the two-channel stems start from their input-channel sum and output-channel mean.
83
+ Trainable: the trunk, both input stems and ``stem_out["2"]``; ``stem_out["3"]`` and the BatchNorm stay fixed.
84
+ """
85
+ c = vae.config
86
+ model = cls(ch=c.ch, ch_mult=c.ch_mult, num_res_blocks=c.num_res_blocks, z_channels=c.z_channels)
87
+ model.encoder.load_state_dict({k: v for k, v in vae.encoder.state_dict().items()
88
+ if not k.startswith("conv_in.")})
89
+ model.bn.load_state_dict(vae.bn.state_dict())
90
+ w_in, b_in = vae.encoder.conv_in.weight.float(), vae.encoder.conv_in.bias.float()
91
+ w_out, b_out = vae.decoder.conv_out.weight.float(), vae.decoder.conv_out.bias.float()
92
+ stems = {
93
+ ("stem_in", "3"): (w_in, b_in),
94
+ ("stem_in", "2"): (w_in.sum(dim=1, keepdim=True).repeat(1, 2, 1, 1) / 2, b_in.clone()),
95
+ ("stem_out", "3"): (w_out, b_out),
96
+ ("stem_out", "2"): (w_out.mean(dim=0, keepdim=True).repeat(2, 1, 1, 1), b_out.mean().repeat(2)),
97
+ }
98
+ for (group, key), (w, b) in stems.items():
99
+ getattr(model, group)[key].weight.copy_(w)
100
+ getattr(model, group)[key].bias.copy_(b)
101
+ model.stem_out["3"].requires_grad_(False)
102
+ return model
generalist/scheduler/scheduler_config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "_class_name": "FlowBridgeScheduler",
3
+ "_diffusers_version": "0.37.1"
4
+ }
generalist/scheduler/scheduler_flow_bridge.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit-Euler solver for the GeoSET flow bridge (t = 0: Gaussian noise, t = 1: EO latent)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Union
7
+
8
+ import torch
9
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
10
+ from diffusers.schedulers.scheduling_utils import SchedulerMixin
11
+ from diffusers.utils import BaseOutput
12
+
13
+
14
+ @dataclass
15
+ class FlowBridgeSchedulerOutput(BaseOutput):
16
+ prev_sample: torch.Tensor
17
+
18
+
19
+ class FlowBridgeScheduler(SchedulerMixin, ConfigMixin):
20
+ """Uniform grid ``linspace(0, 1, N + 1)``; the model is queried at the first ``N`` points, ``t`` ascending.
21
+
22
+ ``step`` integrates the velocity ``dz/dt`` over one grid interval in float32.
23
+ """
24
+
25
+ order = 1
26
+
27
+ @register_to_config
28
+ def __init__(self):
29
+ self.grid: Optional[torch.Tensor] = None
30
+ self.num_inference_steps: Optional[int] = None
31
+ self.step_index = 0
32
+
33
+ @property
34
+ def timesteps(self) -> torch.Tensor:
35
+ return self.grid[:-1]
36
+
37
+ def set_timesteps(self, num_inference_steps: int, device: Optional[Union[str, torch.device]] = None) -> None:
38
+ if num_inference_steps < 1:
39
+ raise ValueError(f"num_inference_steps must be >= 1, got {num_inference_steps}")
40
+ self.num_inference_steps = num_inference_steps
41
+ self.grid = torch.linspace(0.0, 1.0, num_inference_steps + 1, device=device, dtype=torch.float32)
42
+ self.step_index = 0
43
+
44
+ def step(
45
+ self,
46
+ model_output: torch.Tensor,
47
+ timestep: Union[float, torch.Tensor],
48
+ sample: torch.Tensor,
49
+ return_dict: bool = True,
50
+ ) -> Union[FlowBridgeSchedulerOutput, tuple]:
51
+ """``z + (t_next - t) * v``; steps must follow ``timesteps`` in order."""
52
+ t, t_next = self.grid[self.step_index], self.grid[self.step_index + 1]
53
+ if float(timestep) != float(t):
54
+ raise ValueError(f"expected timestep {float(t)} at step {self.step_index}, got {float(timestep)}")
55
+ prev_sample = sample.float() + (t_next - t) * model_output.float()
56
+ self.step_index += 1
57
+ if not return_dict:
58
+ return (prev_sample,)
59
+ return FlowBridgeSchedulerOutput(prev_sample=prev_sample)