rsdiff 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rsdiff/__init__.py +3 -0
- rsdiff/cascade.py +184 -0
- rsdiff/cli.py +137 -0
- rsdiff/config.py +107 -0
- rsdiff/configs/rsdiff1.5.yaml +54 -0
- rsdiff/configs/rsdiff1.yaml +61 -0
- rsdiff/datasets/__init__.py +16 -0
- rsdiff/datasets/base.py +44 -0
- rsdiff/datasets/rsicd.py +92 -0
- rsdiff/eval/__init__.py +11 -0
- rsdiff/eval/clip.py +90 -0
- rsdiff/eval/fid.py +125 -0
- rsdiff-0.1.0.dist-info/METADATA +245 -0
- rsdiff-0.1.0.dist-info/RECORD +18 -0
- rsdiff-0.1.0.dist-info/WHEEL +4 -0
- rsdiff-0.1.0.dist-info/entry_points.txt +2 -0
- rsdiff-0.1.0.dist-info/licenses/LICENSE +202 -0
- rsdiff-0.1.0.dist-info/licenses/NOTICE +23 -0
rsdiff/__init__.py
ADDED
rsdiff/cascade.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Build, load and sample the Imagen-style text-to-image cascade.
|
|
2
|
+
|
|
3
|
+
The architecture comes from a :class:`~rsdiff.config.CascadeConfig`; weights come from an
|
|
4
|
+
``ImagenTrainer`` checkpoint such as the released ``rsdiff1.5`` file. Sampling goes through
|
|
5
|
+
``ImagenTrainer.sample`` so the EMA weights are used, exactly as in the published evaluation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import os
|
|
12
|
+
import random
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
from dataclasses import asdict
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import torch
|
|
20
|
+
from PIL import Image
|
|
21
|
+
from PIL.PngImagePlugin import PngInfo
|
|
22
|
+
|
|
23
|
+
from rsdiff import __version__
|
|
24
|
+
from rsdiff.config import CascadeConfig, RunConfig, UnetArch
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from imagen_pytorch import Imagen, ImagenTrainer
|
|
28
|
+
|
|
29
|
+
RELEASE_REPO = "asebaq/rsdiff-sr-cascade-ep650"
|
|
30
|
+
RELEASE_REVISION = "0ac4d46c3aac32f4fcfb40831fcaf83d5e520fb0"
|
|
31
|
+
RELEASE_FILENAME = "ckpt_sr_ep650_step89050.pt"
|
|
32
|
+
RELEASE_SHA256 = "1c7721d5c32bd2fdebd938302435269b9a4f5ddb3691ec8f5226c11d110b09a1"
|
|
33
|
+
|
|
34
|
+
SYNTHETIC_NOTICE = (
|
|
35
|
+
"Synthetic image generated by RSDiff from a text prompt. "
|
|
36
|
+
"It is not an observation or prediction of any real place."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Callable with the ImagenTrainer.sample keywords we use: texts, cond_scale,
|
|
40
|
+
# stop_at_unet_number, return_pil_images. Tests substitute a fake.
|
|
41
|
+
Sampler = Callable[..., list[Image.Image]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def seed_everything(seed: int) -> None:
|
|
45
|
+
"""Seed Python, NumPy and torch, matching the seeding used for the release."""
|
|
46
|
+
random.seed(seed)
|
|
47
|
+
os.environ["PYTHONHASHSEED"] = str(seed)
|
|
48
|
+
np.random.seed(seed)
|
|
49
|
+
torch.manual_seed(seed)
|
|
50
|
+
torch.cuda.manual_seed_all(seed)
|
|
51
|
+
torch.backends.cudnn.deterministic = True
|
|
52
|
+
torch.backends.cudnn.benchmark = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sha256_file(path: str | Path, chunk_size: int = 1 << 24) -> str:
|
|
56
|
+
digest = hashlib.sha256()
|
|
57
|
+
with open(path, "rb") as f:
|
|
58
|
+
while chunk := f.read(chunk_size):
|
|
59
|
+
digest.update(chunk)
|
|
60
|
+
return digest.hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def verify_checkpoint(path: str | Path, sha256: str = RELEASE_SHA256) -> None:
|
|
64
|
+
"""Raise ``ValueError`` unless the file at ``path`` has the expected SHA-256."""
|
|
65
|
+
actual = sha256_file(path)
|
|
66
|
+
if actual != sha256:
|
|
67
|
+
raise ValueError(f"checksum mismatch for {path}: expected {sha256}, got {actual}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def download_release(cache_dir: str | Path | None = None) -> Path:
|
|
71
|
+
"""Download the released ``rsdiff1.5`` checkpoint (1.9 GB) and verify its digest."""
|
|
72
|
+
from huggingface_hub import hf_hub_download
|
|
73
|
+
|
|
74
|
+
path = Path(
|
|
75
|
+
hf_hub_download(
|
|
76
|
+
repo_id=RELEASE_REPO,
|
|
77
|
+
filename=RELEASE_FILENAME,
|
|
78
|
+
revision=RELEASE_REVISION,
|
|
79
|
+
cache_dir=cache_dir,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
verify_checkpoint(path)
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_unet(arch: UnetArch):
|
|
87
|
+
"""Construct one standalone ``imagen_pytorch.Unet`` stage from ``arch``."""
|
|
88
|
+
from imagen_pytorch import Unet
|
|
89
|
+
|
|
90
|
+
kwargs = {k: tuple(v) if isinstance(v, list) else v for k, v in asdict(arch).items()}
|
|
91
|
+
del kwargs["optimizer"], kwargs["params_m"]
|
|
92
|
+
return Unet(**kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_imagen(cascade: CascadeConfig, text_embed_dim: int | None = None) -> Imagen:
|
|
96
|
+
"""Construct the (untrained) cascade described by ``cascade``.
|
|
97
|
+
|
|
98
|
+
``text_embed_dim`` skips looking up the text encoder's width on the Hugging Face Hub;
|
|
99
|
+
leave it ``None`` when sampling from text.
|
|
100
|
+
"""
|
|
101
|
+
from imagen_pytorch import Imagen
|
|
102
|
+
|
|
103
|
+
unets = [build_unet(cascade.base)]
|
|
104
|
+
if cascade.sr is not None:
|
|
105
|
+
unets.append(build_unet(cascade.sr))
|
|
106
|
+
image_sizes = tuple(cascade.image_sizes[: len(unets)])
|
|
107
|
+
return Imagen(
|
|
108
|
+
text_encoder_name=cascade.text_encoder,
|
|
109
|
+
text_embed_dim=text_embed_dim,
|
|
110
|
+
unets=tuple(unets) if len(unets) > 1 else unets[0],
|
|
111
|
+
image_sizes=image_sizes if len(unets) > 1 else image_sizes[0],
|
|
112
|
+
timesteps=cascade.timesteps,
|
|
113
|
+
cond_drop_prob=cascade.cond_drop_prob,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_cascade(cfg: RunConfig, checkpoint: str | Path, cpu: bool = False) -> ImagenTrainer:
|
|
118
|
+
"""Build the cascade for ``cfg`` and load ``checkpoint`` into an ``ImagenTrainer``.
|
|
119
|
+
|
|
120
|
+
Runs on CUDA when available, otherwise on CPU. Apple MPS is not used: imagen-pytorch
|
|
121
|
+
2.1.0 casts to float64 while sampling, which MPS does not support.
|
|
122
|
+
"""
|
|
123
|
+
from imagen_pytorch import ImagenTrainer
|
|
124
|
+
|
|
125
|
+
cpu = cpu or not torch.cuda.is_available()
|
|
126
|
+
trainer = ImagenTrainer(imagen=build_imagen(cfg.cascade), verbose=False, accelerate_cpu=cpu)
|
|
127
|
+
trainer.load(str(checkpoint))
|
|
128
|
+
return trainer
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _slug(text: str, n: int = 40) -> str:
|
|
132
|
+
s = "".join(ch if ch.isalnum() else "_" for ch in text.strip().lower())
|
|
133
|
+
return "_".join(filter(None, s.split("_")))[:n]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def output_paths(captions: Sequence[str], out_dir: Path) -> list[Path]:
|
|
137
|
+
"""PNG path per caption; zero-padded so sorted file order equals caption order."""
|
|
138
|
+
pad = max(2, len(str(len(captions) - 1)))
|
|
139
|
+
return [out_dir / f"{i:0{pad}d}_{_slug(c)}.png" for i, c in enumerate(captions)]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def generate(
|
|
143
|
+
sample: Sampler,
|
|
144
|
+
captions: Sequence[str],
|
|
145
|
+
out_dir: str | Path,
|
|
146
|
+
*,
|
|
147
|
+
cond_scale: float = 5.0,
|
|
148
|
+
batch_size: int = 2,
|
|
149
|
+
stop_at_unet: int | None = None,
|
|
150
|
+
on_batch: Callable[[int, int], None] | None = None,
|
|
151
|
+
) -> list[Path]:
|
|
152
|
+
"""Sample one image per caption into ``out_dir`` and return the PNG paths.
|
|
153
|
+
|
|
154
|
+
Writes ``captions.txt`` (one caption per line, in file order) before sampling. A batch
|
|
155
|
+
whose PNGs all exist is skipped, so an interrupted run resumes where it stopped. Every
|
|
156
|
+
PNG carries the prompt and a synthetic-content notice in its text chunks.
|
|
157
|
+
"""
|
|
158
|
+
if not captions:
|
|
159
|
+
raise ValueError("no captions to sample")
|
|
160
|
+
if any(not c.strip() for c in captions):
|
|
161
|
+
raise ValueError("captions must be non-empty")
|
|
162
|
+
out_dir = Path(out_dir)
|
|
163
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
(out_dir / "captions.txt").write_text("\n".join(captions) + "\n", encoding="utf-8")
|
|
165
|
+
paths = output_paths(captions, out_dir)
|
|
166
|
+
|
|
167
|
+
for start in range(0, len(captions), batch_size):
|
|
168
|
+
batch_caps = list(captions[start : start + batch_size])
|
|
169
|
+
batch_paths = paths[start : start + batch_size]
|
|
170
|
+
if all(p.exists() for p in batch_paths):
|
|
171
|
+
continue
|
|
172
|
+
kwargs: dict = {"texts": batch_caps, "cond_scale": cond_scale, "return_pil_images": True}
|
|
173
|
+
if stop_at_unet is not None:
|
|
174
|
+
kwargs["stop_at_unet_number"] = stop_at_unet
|
|
175
|
+
images = sample(**kwargs)
|
|
176
|
+
for path, caption, image in zip(batch_paths, batch_caps, images, strict=True):
|
|
177
|
+
info = PngInfo()
|
|
178
|
+
info.add_text("Software", f"rsdiff {__version__}")
|
|
179
|
+
info.add_text("Description", SYNTHETIC_NOTICE)
|
|
180
|
+
info.add_text("Prompt", caption)
|
|
181
|
+
image.save(path, pnginfo=info)
|
|
182
|
+
if on_batch is not None:
|
|
183
|
+
on_batch(min(start + batch_size, len(captions)), len(captions))
|
|
184
|
+
return paths
|
rsdiff/cli.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""``rsdiff`` command line: sample the released cascade and score generation bundles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rsdiff import __version__
|
|
12
|
+
from rsdiff.config import DEFAULT_PRESET, PRESETS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cmd_sample(args: argparse.Namespace) -> int:
|
|
16
|
+
from rsdiff.cascade import download_release, generate, load_cascade, seed_everything
|
|
17
|
+
from rsdiff.config import resolve_config
|
|
18
|
+
from rsdiff.datasets.rsicd import sample_captions
|
|
19
|
+
|
|
20
|
+
if args.prompt:
|
|
21
|
+
captions = args.prompt * (args.n or 1)
|
|
22
|
+
else:
|
|
23
|
+
captions = sample_captions(args.rsicd, split=args.split, n=args.n, seed=args.seed)
|
|
24
|
+
cfg = resolve_config(args.config)
|
|
25
|
+
|
|
26
|
+
if args.checkpoint is None:
|
|
27
|
+
if args.config != DEFAULT_PRESET:
|
|
28
|
+
sys.exit("--checkpoint is required unless --config is the released rsdiff1.5")
|
|
29
|
+
print("fetching released checkpoint (1.9 GB on first use)", file=sys.stderr)
|
|
30
|
+
checkpoint = download_release()
|
|
31
|
+
else:
|
|
32
|
+
checkpoint = args.checkpoint
|
|
33
|
+
|
|
34
|
+
seed_everything(args.seed)
|
|
35
|
+
trainer = load_cascade(cfg, checkpoint, cpu=args.cpu)
|
|
36
|
+
print(f"sampling {len(captions)} image(s) on {trainer.device}", file=sys.stderr)
|
|
37
|
+
paths = generate(
|
|
38
|
+
trainer.sample,
|
|
39
|
+
captions,
|
|
40
|
+
args.out_dir,
|
|
41
|
+
cond_scale=args.cond_scale,
|
|
42
|
+
batch_size=args.batch_size,
|
|
43
|
+
stop_at_unet=1 if args.lr_only else None,
|
|
44
|
+
on_batch=lambda done, total: print(f" {done}/{total}", file=sys.stderr),
|
|
45
|
+
)
|
|
46
|
+
print(f"wrote {len(paths)} PNGs to {args.out_dir}", file=sys.stderr)
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_eval_fid(args: argparse.Namespace) -> int:
|
|
51
|
+
from rsdiff.eval.fid import fid
|
|
52
|
+
|
|
53
|
+
res = fid(
|
|
54
|
+
gen_dir=args.gen_dir,
|
|
55
|
+
real_csv=args.rsicd / "dataset_rsicd.csv",
|
|
56
|
+
real_dir=args.rsicd / "RSICD_images",
|
|
57
|
+
split=args.split,
|
|
58
|
+
feature=args.feature,
|
|
59
|
+
image_size=args.size,
|
|
60
|
+
max_n=args.max_n,
|
|
61
|
+
)
|
|
62
|
+
print(json.dumps(asdict(res), indent=2))
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_eval_clip(args: argparse.Namespace) -> int:
|
|
67
|
+
from rsdiff.eval.clip import clip_score
|
|
68
|
+
|
|
69
|
+
print(json.dumps(asdict(clip_score(args.gen_dir, model_id=args.model)), indent=2))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
74
|
+
p = argparse.ArgumentParser(
|
|
75
|
+
prog="rsdiff", description="Text-to-satellite-image diffusion (RSDiff)."
|
|
76
|
+
)
|
|
77
|
+
p.add_argument("--version", action="version", version=f"rsdiff {__version__}")
|
|
78
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
79
|
+
|
|
80
|
+
s = sub.add_parser(
|
|
81
|
+
"sample",
|
|
82
|
+
help="Generate images from text with the cascade.",
|
|
83
|
+
description="Generate synthetic 256x256 images. Downloads the released "
|
|
84
|
+
"rsdiff1.5 checkpoint unless --checkpoint is given.",
|
|
85
|
+
)
|
|
86
|
+
src = s.add_mutually_exclusive_group(required=True)
|
|
87
|
+
src.add_argument("--prompt", action="append", help="Caption to render (repeatable).")
|
|
88
|
+
src.add_argument("--rsicd", type=Path, help="RSICD root; sample captions from its CSV instead.")
|
|
89
|
+
s.add_argument("--split", default="test", help="RSICD split for --rsicd (default: test).")
|
|
90
|
+
s.add_argument(
|
|
91
|
+
"-n",
|
|
92
|
+
type=int,
|
|
93
|
+
default=None,
|
|
94
|
+
help="Images per --prompt (default 1), or captions drawn from --rsicd (default: all).",
|
|
95
|
+
)
|
|
96
|
+
s.add_argument("--out-dir", type=Path, default=Path("generated"))
|
|
97
|
+
s.add_argument(
|
|
98
|
+
"--config",
|
|
99
|
+
default=DEFAULT_PRESET,
|
|
100
|
+
help=f"Preset ({', '.join(PRESETS)}) or YAML path (default: {DEFAULT_PRESET}).",
|
|
101
|
+
)
|
|
102
|
+
s.add_argument("--checkpoint", type=Path, help="ImagenTrainer checkpoint to load.")
|
|
103
|
+
s.add_argument("--cond-scale", type=float, default=5.0, help="CFG scale (default: 5).")
|
|
104
|
+
s.add_argument("--seed", type=int, default=17)
|
|
105
|
+
s.add_argument("--batch-size", type=int, default=2)
|
|
106
|
+
s.add_argument("--lr-only", action="store_true", help="Stop at the 128x128 base UNet.")
|
|
107
|
+
s.add_argument("--cpu", action="store_true", help="Force CPU even if a GPU is available.")
|
|
108
|
+
s.set_defaults(fn=cmd_sample)
|
|
109
|
+
|
|
110
|
+
e = sub.add_parser("eval", help="Score a generation bundle.")
|
|
111
|
+
esub = e.add_subparsers(dest="metric", required=True)
|
|
112
|
+
|
|
113
|
+
f = esub.add_parser("fid", help="FID against the real RSICD split.")
|
|
114
|
+
f.add_argument("gen_dir", type=Path, help="Directory of generated PNGs.")
|
|
115
|
+
f.add_argument("--rsicd", type=Path, required=True, help="RSICD root (CSV + RSICD_images).")
|
|
116
|
+
f.add_argument("--split", default="test")
|
|
117
|
+
f.add_argument("--feature", type=int, choices=[64, 192, 768, 2048], default=2048)
|
|
118
|
+
f.add_argument("--size", type=int, default=256, help="Resize both sides to this first.")
|
|
119
|
+
f.add_argument("--max-n", type=int, default=None, help="Cap images per side (debug only).")
|
|
120
|
+
f.set_defaults(fn=cmd_eval_fid)
|
|
121
|
+
|
|
122
|
+
c = esub.add_parser("clip", help="CLIP score with a shuffled-caption baseline.")
|
|
123
|
+
c.add_argument("gen_dir", type=Path, help="Bundle written by `rsdiff sample`.")
|
|
124
|
+
c.add_argument(
|
|
125
|
+
"--model", default="openai/clip-vit-base-patch32", help="Hugging Face CLIP model id."
|
|
126
|
+
)
|
|
127
|
+
c.set_defaults(fn=cmd_eval_clip)
|
|
128
|
+
return p
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main(argv: list[str] | None = None) -> int:
|
|
132
|
+
args = build_parser().parse_args(argv)
|
|
133
|
+
return args.fn(args)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__": # pragma: no cover
|
|
137
|
+
sys.exit(main())
|
rsdiff/config.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Cascade configuration and the packaged presets (``rsdiff1.5``, ``rsdiff1``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from omegaconf import OmegaConf
|
|
11
|
+
|
|
12
|
+
PRESETS = ("rsdiff1.5", "rsdiff1")
|
|
13
|
+
DEFAULT_PRESET = "rsdiff1.5"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DataConfig:
|
|
18
|
+
name: str = "rsicd"
|
|
19
|
+
root: str = "data/RSICD_optimal"
|
|
20
|
+
image_size: int = 128
|
|
21
|
+
num_workers: int = 4
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class UnetArch:
|
|
26
|
+
"""imagen-pytorch ``Unet`` hyperparameters for one cascade stage.
|
|
27
|
+
|
|
28
|
+
``dim`` through ``layer_cross_attns`` are passed to ``imagen_pytorch.Unet`` unchanged.
|
|
29
|
+
``num_resnet_blocks`` is an int or a per-stage tuple. ``optimizer`` and ``params_m``
|
|
30
|
+
record the training recipe and the measured parameter count (millions).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
dim: int = 128
|
|
34
|
+
cond_dim: int = 256
|
|
35
|
+
dim_mults: tuple[int, ...] = (1, 2, 2, 2)
|
|
36
|
+
num_resnet_blocks: Any = 0
|
|
37
|
+
layer_attns: tuple[bool, ...] = (False, True, True, True)
|
|
38
|
+
layer_cross_attns: tuple[bool, ...] = (False, True, True, True)
|
|
39
|
+
optimizer: str = "adam" # adam | adafactor
|
|
40
|
+
params_m: float | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CascadeConfig:
|
|
45
|
+
"""Imagen-style text-conditioned cascade: a base UNet plus an optional SR UNet."""
|
|
46
|
+
|
|
47
|
+
image_sizes: tuple[int, ...] = (128, 256)
|
|
48
|
+
timesteps: int = 1000
|
|
49
|
+
cond_drop_prob: float = 0.1
|
|
50
|
+
text_encoder: str = "t5-base"
|
|
51
|
+
base: UnetArch = field(default_factory=UnetArch)
|
|
52
|
+
sr: UnetArch | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class OptimConfig:
|
|
57
|
+
lr: float = 1.0e-4
|
|
58
|
+
warmup_steps: int = 10000
|
|
59
|
+
weight_decay: float = 0.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class TrainConfig:
|
|
64
|
+
seed: int = 42
|
|
65
|
+
output_dir: str = "outputs/run"
|
|
66
|
+
batch_size: int = 64
|
|
67
|
+
mixed_precision: str = "bf16" # no | fp16 | bf16
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class MetaConfig:
|
|
72
|
+
"""Preset provenance: model line, parameter count, expected and measured FID."""
|
|
73
|
+
|
|
74
|
+
version: str = ""
|
|
75
|
+
reflects: str = ""
|
|
76
|
+
params_m: float | None = None
|
|
77
|
+
fid_expected: float | None = None
|
|
78
|
+
fid_measured: float | None = None
|
|
79
|
+
notes: str = ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class RunConfig:
|
|
84
|
+
data: DataConfig = field(default_factory=DataConfig)
|
|
85
|
+
cascade: CascadeConfig = field(default_factory=CascadeConfig)
|
|
86
|
+
optim: OptimConfig = field(default_factory=OptimConfig)
|
|
87
|
+
train: TrainConfig = field(default_factory=TrainConfig)
|
|
88
|
+
meta: MetaConfig = field(default_factory=MetaConfig)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_config(path: str | Path) -> RunConfig:
|
|
92
|
+
"""Load a YAML config, validating every key against :class:`RunConfig`."""
|
|
93
|
+
merged = OmegaConf.merge(OmegaConf.structured(RunConfig), OmegaConf.load(path))
|
|
94
|
+
return OmegaConf.to_object(merged) # type: ignore[return-value]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_preset(name: str = DEFAULT_PRESET) -> RunConfig:
|
|
98
|
+
"""Load a packaged preset by name (see :data:`PRESETS`)."""
|
|
99
|
+
if name not in PRESETS:
|
|
100
|
+
raise KeyError(f"unknown preset {name!r}; known: {', '.join(PRESETS)}")
|
|
101
|
+
with resources.as_file(resources.files("rsdiff.configs") / f"{name}.yaml") as path:
|
|
102
|
+
return load_config(path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_config(spec: str | Path) -> RunConfig:
|
|
106
|
+
"""Return the preset named ``spec``, or load ``spec`` as a YAML path."""
|
|
107
|
+
return load_preset(str(spec)) if str(spec) in PRESETS else load_config(spec)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# rsdiff1.5: the released cascade. Lightweight base (the net that produced the thesis
|
|
2
|
+
# FID 66.49) + a shrunk SR unet.
|
|
3
|
+
#
|
|
4
|
+
# base 27.2M + SR 92.7M = 119.9M pipeline (~6x lighter than rsdiff1's 723M).
|
|
5
|
+
# SR shrink: dim_mults deepest 8x->4x (1,2,3,4) + num_resnet_blocks->(2,2,2,2).
|
|
6
|
+
|
|
7
|
+
meta:
|
|
8
|
+
version: rsdiff1.5
|
|
9
|
+
reflects: "thesis actual base (27.2M, FID 66.49 lineage) + optimized SR"
|
|
10
|
+
params_m: 119.9
|
|
11
|
+
fid_expected: 65.70
|
|
12
|
+
fid_measured: 65.70
|
|
13
|
+
notes: "released merged SR ep650 checkpoint; FID-2048 on 1093 RSICD test images, cond_scale=5."
|
|
14
|
+
|
|
15
|
+
data:
|
|
16
|
+
name: rsicd
|
|
17
|
+
root: data/RSICD_optimal
|
|
18
|
+
image_size: 128
|
|
19
|
+
num_workers: 4
|
|
20
|
+
|
|
21
|
+
cascade:
|
|
22
|
+
image_sizes: [128, 256]
|
|
23
|
+
timesteps: 1000
|
|
24
|
+
cond_drop_prob: 0.1
|
|
25
|
+
text_encoder: t5-base
|
|
26
|
+
base: # unet 1 — 27.2M (frozen, seeded from LR run)
|
|
27
|
+
dim: 128
|
|
28
|
+
cond_dim: 256
|
|
29
|
+
dim_mults: [1, 2, 2, 2]
|
|
30
|
+
num_resnet_blocks: 0
|
|
31
|
+
layer_attns: [false, true, true, true]
|
|
32
|
+
layer_cross_attns: [false, true, true, true]
|
|
33
|
+
optimizer: adam
|
|
34
|
+
params_m: 27.2
|
|
35
|
+
sr: # unet 2 — 92.7M (shrunk Efficient-U-Net)
|
|
36
|
+
dim: 128
|
|
37
|
+
cond_dim: 512
|
|
38
|
+
dim_mults: [1, 2, 3, 4]
|
|
39
|
+
num_resnet_blocks: [2, 2, 2, 2]
|
|
40
|
+
layer_attns: [false, false, false, true]
|
|
41
|
+
layer_cross_attns: [false, false, false, true]
|
|
42
|
+
optimizer: adam
|
|
43
|
+
params_m: 92.7
|
|
44
|
+
|
|
45
|
+
optim:
|
|
46
|
+
lr: 1.0e-4
|
|
47
|
+
warmup_steps: 10000
|
|
48
|
+
weight_decay: 0.0
|
|
49
|
+
|
|
50
|
+
train:
|
|
51
|
+
seed: 42
|
|
52
|
+
output_dir: outputs/rsdiff1.5
|
|
53
|
+
batch_size: 64
|
|
54
|
+
mixed_precision: bf16
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# rsdiff1 — paper-faithful cascade (RSDiff, NCAA 2024, as described in §3.3).
|
|
2
|
+
#
|
|
3
|
+
# Two-stage Imagen-style cascade: 260.8M base (128²) + 462.4M SR (128→256²).
|
|
4
|
+
# Pipeline = 723.2M ≈ the abstract's stated "0.75 billion" parameters.
|
|
5
|
+
#
|
|
6
|
+
# NOTE on the paper's internal inconsistency: §3.3 states 260M base + 260M SR
|
|
7
|
+
# (= 520M), but the abstract says 0.75B. The committed code's SR unet is 462.4M,
|
|
8
|
+
# and 260.8M + 462.4M = 723M ≈ 0.75B — so the abstract figure is the consistent
|
|
9
|
+
# one and §3.3's "260M SR" is a misstatement. rsdiff1 encodes the runnable code
|
|
10
|
+
# artifacts (big base + original SR), not the §3.3 prose figure.
|
|
11
|
+
#
|
|
12
|
+
# This is NOT the config that produced the reported FID 66.49 — see rsdiff1.5*.
|
|
13
|
+
|
|
14
|
+
meta:
|
|
15
|
+
version: rsdiff1
|
|
16
|
+
reflects: "paper NCAA 2024 §3.3 / abstract 0.75B"
|
|
17
|
+
params_m: 723.2
|
|
18
|
+
fid_expected: null # paper headline; provenance (stage/feature) unrecoverable
|
|
19
|
+
fid_measured: null
|
|
20
|
+
notes: "big base (nrb=3) + original Efficient-U-Net SR. Adafactor base / Adam SR per paper."
|
|
21
|
+
|
|
22
|
+
data:
|
|
23
|
+
name: rsicd
|
|
24
|
+
root: data/RSICD_optimal
|
|
25
|
+
image_size: 128
|
|
26
|
+
num_workers: 4
|
|
27
|
+
|
|
28
|
+
cascade:
|
|
29
|
+
image_sizes: [128, 256]
|
|
30
|
+
timesteps: 1000
|
|
31
|
+
cond_drop_prob: 0.1
|
|
32
|
+
text_encoder: t5-base
|
|
33
|
+
base: # unet 1 — 260.8M (Imagen_text_big_pytorch.py)
|
|
34
|
+
dim: 128
|
|
35
|
+
cond_dim: 512
|
|
36
|
+
dim_mults: [1, 2, 4, 8]
|
|
37
|
+
num_resnet_blocks: 3
|
|
38
|
+
layer_attns: [false, true, true, true]
|
|
39
|
+
layer_cross_attns: [false, true, true, true]
|
|
40
|
+
optimizer: adafactor
|
|
41
|
+
params_m: 260.8
|
|
42
|
+
sr: # unet 2 — 462.4M (Efficient-U-Net)
|
|
43
|
+
dim: 128
|
|
44
|
+
cond_dim: 512
|
|
45
|
+
dim_mults: [1, 2, 4, 8]
|
|
46
|
+
num_resnet_blocks: [2, 4, 8, 8]
|
|
47
|
+
layer_attns: [false, false, false, true]
|
|
48
|
+
layer_cross_attns: [false, false, false, true]
|
|
49
|
+
optimizer: adam
|
|
50
|
+
params_m: 462.4
|
|
51
|
+
|
|
52
|
+
optim:
|
|
53
|
+
lr: 1.0e-4
|
|
54
|
+
warmup_steps: 10000 # paper: 10k linear warmup
|
|
55
|
+
weight_decay: 0.0
|
|
56
|
+
|
|
57
|
+
train:
|
|
58
|
+
seed: 42
|
|
59
|
+
output_dir: outputs/rsdiff1
|
|
60
|
+
batch_size: 64
|
|
61
|
+
mixed_precision: bf16
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from rsdiff.datasets.base import RSDataset
|
|
2
|
+
from rsdiff.datasets.rsicd import RSICD
|
|
3
|
+
|
|
4
|
+
__all__ = ["RSDataset", "RSICD", "build_dataset"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_REGISTRY = {
|
|
8
|
+
"rsicd": RSICD,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_dataset(name: str, **kwargs) -> RSDataset:
|
|
13
|
+
key = name.lower()
|
|
14
|
+
if key not in _REGISTRY:
|
|
15
|
+
raise KeyError(f"Unknown dataset {name!r}. Known: {sorted(_REGISTRY)}")
|
|
16
|
+
return _REGISTRY[key](**kwargs)
|
rsdiff/datasets/base.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from torch.utils.data import Dataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Sample:
|
|
13
|
+
image: Any
|
|
14
|
+
caption: str | None = None
|
|
15
|
+
label: str | None = None
|
|
16
|
+
meta: dict[str, Any] | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RSDataset(Dataset, ABC):
|
|
20
|
+
"""Common interface for all remote-sensing datasets in rsdiff.
|
|
21
|
+
|
|
22
|
+
Implementations should return a :class:`Sample` from ``__getitem__``.
|
|
23
|
+
A torchvision-style ``transform`` may be applied to ``image`` before return.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name: str = "base"
|
|
27
|
+
splits: tuple[str, ...] = ("train", "val", "test")
|
|
28
|
+
|
|
29
|
+
def __init__(self, root: str | Path, split: str = "train", transform=None) -> None:
|
|
30
|
+
if split not in self.splits:
|
|
31
|
+
raise ValueError(f"split={split!r} not in {self.splits}")
|
|
32
|
+
self.root = Path(root)
|
|
33
|
+
self.split = split
|
|
34
|
+
self.transform = transform
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def __len__(self) -> int: ...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def __getitem__(self, idx: int) -> Sample: ...
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def classes(self) -> list[str]:
|
|
44
|
+
return []
|