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 ADDED
@@ -0,0 +1,3 @@
1
+ """RSDiff: text-to-satellite-image diffusion."""
2
+
3
+ __version__ = "0.1.0"
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)
@@ -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 []