Download setup.py from dineth18/CD-Models: direct link, hf CLI and curl.
- Browser
- Download file 19.3 kB
-
https://huggingface.co/dineth18/CD-Models/resolve/a95dbcec2bd609c4d1263e4e4e17525fa99446dd/setup.py
- Command line
-
hf download hf://dineth18/CD-Models@a95dbcec2bd609c4d1263e4e4e17525fa99446dd/setup.py
-
curl -L -o setup.py https://huggingface.co/dineth18/CD-Models/resolve/a95dbcec2bd609c4d1263e4e4e17525fa99446dd/setup.py
19.3 kB
| #!/usr/bin/env python3 | |
| """ | |
| setup.py - One-command setup for the CD-Models benchmark suite. | |
| Clones all model repositories, downloads pretrained backbone weights, | |
| verifies the Python environment, and optionally prepares datasets. | |
| Usage: | |
| python setup.py # full setup (clone + weights + env check) | |
| python setup.py --skip-weights # clone only, skip weight downloads | |
| python setup.py --env-check-only # only check Python packages | |
| python setup.py --status # print current setup status without changes | |
| python setup.py --dataset levir_cd # prepare list files for a specific dataset | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import importlib | |
| import os | |
| import subprocess | |
| import sys | |
| import urllib.request | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent | |
| DATASET_CONFIGS = ROOT / "configs" / "datasets" | |
| MODEL_REPOS = ROOT / "model_repos" | |
| IMG_EXTS = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"} | |
| ROOT_LEVEL_REPOS = { | |
| "BIT_CD", | |
| "ChangeFormer", | |
| "STANet", | |
| "Change3D", | |
| "BiFA", | |
| "CDMamba", | |
| "RSM-CD", | |
| "SChanger", | |
| "IFNet", | |
| "Siam-NestedUNet", | |
| } | |
| REQUIRED_PACKAGES = [ | |
| ("torch", "torch", "all models", False), | |
| ("torchvision", "torchvision", "all models", False), | |
| ("numpy", "numpy", "all models", True), | |
| ("PIL", "Pillow", "all models", True), | |
| ("cv2", "opencv-python", "all models", True), | |
| ("yaml", "pyyaml", "all models", True), | |
| ("tqdm", "tqdm", "weight downloads", True), | |
| ("timm", "timm", "TinyCD, ELGC-Net, ChangeFormer", True), | |
| ("einops", "einops", "BIT_CD, ChangeFormer", True), | |
| ("sklearn", "scikit-learn", "metrics", True), | |
| ("gdown", "gdown", "GDrive weight fallback", True), | |
| ("mmengine", "mmengine==0.10.1", "ChangeMamba, Changer", False), | |
| ("mmcv", "mmcv==2.1.0", "ChangeMamba, Changer", False), | |
| ("mmseg", "mmsegmentation==1.2.2", "ChangeMamba, Changer", False), | |
| ] | |
| CONFIRMED_REPOS = { | |
| "BIT_CD": "https://github.com/justchenhao/BIT_CD.git", | |
| "ChangeFormer": "https://github.com/wgcban/ChangeFormer.git", | |
| "STANet": "https://github.com/justchenhao/STANet.git", | |
| "Change3D": "https://github.com/Z-Zheng/Change3D.git", | |
| "BiFA": "https://github.com/zmoka-zht/BiFA.git", | |
| "CDMamba": "https://github.com/zmoka-zht/CDMamba.git", | |
| "RSM-CD": "https://github.com/walking-shadow/Official_Remote_Sensing_Mamba.git", | |
| "SChanger": None, | |
| "IFNet": "https://github.com/GeoZcx/A-deeply-supervised-image-fusion-network-for-change-detection.git", | |
| "Siam-NestedUNet": "https://github.com/likyoo/Siam-NestedUNet.git", | |
| "fully_convolutional_change_detection": "https://github.com/rcdaudt/fully_convolutional_change_detection.git", | |
| "ChangeMamba": "https://github.com/ChenHongruixuan/ChangeMamba.git", | |
| "DSAMNet": "https://github.com/liumency/DSAMNet.git", | |
| "Tiny_model_4_CD": "https://github.com/AndreaCodegoni/Tiny_model_4_CD.git", | |
| "HANet-CD": "https://github.com/ChengxiHAN/HANet-CD.git", | |
| "CGNet-CD": "https://github.com/ChengxiHAN/CGNet-CD.git", | |
| "open-cd": "https://github.com/likyoo/open-cd.git", | |
| "elgcnet": "https://github.com/techmn/elgcnet.git", | |
| } | |
| ZENODO_WEIGHTS = { | |
| "vmamba_tiny": { | |
| "record_id": "14037770", | |
| "filename": "vssmtiny_dp01_ckpt_epoch_292.pth", | |
| "dest_dir": MODEL_REPOS / "ChangeMamba" / "pretrained_weight", | |
| "sha256": None, | |
| "notes": "VMamba-Tiny for ChangeMamba", | |
| }, | |
| "vmamba_small": { | |
| "record_id": "14037770", | |
| "filename": "vssmsmall_dp03_ckpt_epoch_238.pth", | |
| "dest_dir": MODEL_REPOS / "ChangeMamba" / "pretrained_weight", | |
| "sha256": None, | |
| "notes": "VMamba-Small for ChangeMamba", | |
| }, | |
| "vmamba_base": { | |
| "record_id": "14037770", | |
| "filename": "vssmbase_dp06_ckpt_epoch_241.pth", | |
| "dest_dir": MODEL_REPOS / "ChangeMamba" / "pretrained_weight", | |
| "sha256": None, | |
| "notes": "VMamba-Base for ChangeMamba", | |
| }, | |
| } | |
| TIMM_WEIGHTS_TO_PREFETCH = [ | |
| "efficientnet_b4", | |
| "mit_b0", | |
| "mit_b1", | |
| "mit_b4", | |
| ] | |
| TORCHVISION_WEIGHTS_TO_PREFETCH = [ | |
| ("resnet18", "IMAGENET1K_V1"), | |
| ("resnet50", "IMAGENET1K_V1"), | |
| ("vgg16", "IMAGENET1K_V1"), | |
| ] | |
| TIMM_ALIASES = { | |
| "efficientnet_b4": ("efficientnet_b4", "tf_efficientnet_b4", "tf_efficientnet_b4_ns"), | |
| "mit_b0": ("mit_b0", "segformer_b0"), | |
| "mit_b1": ("mit_b1", "segformer_b1"), | |
| "mit_b4": ("mit_b4", "segformer_b4"), | |
| } | |
| def run(cmd: list[str]) -> subprocess.CompletedProcess: | |
| return subprocess.run(cmd, capture_output=True, text=True, check=False) | |
| def import_status(import_name: str) -> tuple[bool, str]: | |
| try: | |
| module = importlib.import_module(import_name) | |
| except Exception as exc: | |
| return False, str(exc) | |
| return True, getattr(module, "__version__", "present") | |
| def pip_install(package: str) -> bool: | |
| print(f" [INSTALL] pip install {package}") | |
| result = run([sys.executable, "-m", "pip", "install", package]) | |
| if result.returncode != 0: | |
| print(f" [FAIL] {package}: {result.stderr[-500:]}") | |
| return False | |
| print(f" [OK] {package} installed") | |
| return True | |
| def check_environment(status_only: bool = False) -> dict[str, int]: | |
| print("\nPYTHON ENVIRONMENT") | |
| print("------------------") | |
| print(f"Python executable: {sys.executable}") | |
| print(f"Python version: {sys.version.split()[0]}") | |
| present = missing = installed = warnings = 0 | |
| for import_name, pip_name, required_for, auto_install in REQUIRED_PACKAGES: | |
| ok, detail = import_status(import_name) | |
| if ok: | |
| present += 1 | |
| print(f" [OK] {import_name:<12} {detail:<18} required for {required_for}") | |
| continue | |
| missing += 1 | |
| if auto_install and not status_only: | |
| if pip_install(pip_name): | |
| installed += 1 | |
| continue | |
| if not auto_install: | |
| warnings += 1 | |
| print(f" [WARN] {import_name:<12} missing; install manually: pip install {pip_name}") | |
| else: | |
| print(f" [MISSING] {import_name:<12} install: pip install {pip_name}") | |
| if warnings: | |
| print("\nVersion-sensitive packages are intentionally not auto-installed.") | |
| print("For ChangeMamba / Changer:") | |
| print(" pip install mmengine==0.10.1") | |
| print(" pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu124/torch2.6/index.html") | |
| print(" pip install mmsegmentation==1.2.2 mmdet==3.3.0 mmpretrain==1.2.0") | |
| return {"present": present, "missing": missing, "installed": installed, "warnings": warnings} | |
| def clone_repo(name: str, url: str | None, dest_dir: Path = MODEL_REPOS, status_only: bool = False) -> bool: | |
| dest = (ROOT / name) if name in ROOT_LEVEL_REPOS else (dest_dir / name) | |
| if dest.is_dir(): | |
| print(f" [OK] {name} already cloned") | |
| return True | |
| if url is None: | |
| print(f" [SKIP] {name} - URL not confirmed, clone manually") | |
| return False | |
| if status_only: | |
| print(f" [MISSING] {name} -> {url}") | |
| return False | |
| print(f" [CLONE] {name}...") | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| result = run(["git", "clone", "--depth", "1", url, str(dest)]) | |
| if result.returncode == 0: | |
| print(f" [OK] {name} cloned") | |
| return True | |
| print(f" [FAIL] {name}: {result.stderr[:300]}") | |
| return False | |
| def clone_repositories(status_only: bool = False) -> dict[str, int]: | |
| print("\nMODEL REPOSITORIES") | |
| print("------------------") | |
| ok = missing = skipped = 0 | |
| for name, url in CONFIRMED_REPOS.items(): | |
| result = clone_repo(name, url, status_only=status_only) | |
| if result: | |
| ok += 1 | |
| elif url is None: | |
| skipped += 1 | |
| else: | |
| missing += 1 | |
| return {"ok": ok, "missing": missing, "skipped": skipped, "total": len(CONFIRMED_REPOS)} | |
| def sha256_file(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as f: | |
| for chunk in iter(lambda: f.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def download_zenodo_weight( | |
| record_id: str, | |
| filename: str, | |
| dest_dir: Path, | |
| expected_sha256: str | None = None, | |
| status_only: bool = False, | |
| ) -> Path | None: | |
| dest_dir.mkdir(parents=True, exist_ok=True) | |
| dest_path = dest_dir / filename | |
| if dest_path.exists(): | |
| if expected_sha256: | |
| actual = sha256_file(dest_path) | |
| if actual != expected_sha256: | |
| print(f" [FAIL] {filename} sha256 mismatch") | |
| return None | |
| print(f" [OK] {filename} already present") | |
| return dest_path | |
| if status_only: | |
| print(f" [MISSING] {filename}") | |
| return None | |
| try: | |
| from tqdm import tqdm | |
| except Exception: | |
| tqdm = None | |
| url = f"https://zenodo.org/records/{record_id}/files/{filename}" | |
| print(f" [DOWNLOAD] {filename} from Zenodo record {record_id}") | |
| class ProgressHook: | |
| def __init__(self) -> None: | |
| self.pbar = None | |
| def __call__(self, count: int, block_size: int, total_size: int) -> None: | |
| if tqdm is None: | |
| return | |
| if self.pbar is None: | |
| self.pbar = tqdm(total=total_size, unit="B", unit_scale=True, desc=filename) | |
| self.pbar.update(count * block_size - self.pbar.n) | |
| hook = ProgressHook() | |
| try: | |
| urllib.request.urlretrieve(url, dest_path, reporthook=hook) | |
| if hook.pbar: | |
| hook.pbar.close() | |
| if expected_sha256: | |
| actual = sha256_file(dest_path) | |
| if actual != expected_sha256: | |
| dest_path.unlink(missing_ok=True) | |
| print(f" [FAIL] {filename}: sha256 mismatch") | |
| return None | |
| print(f" [OK] {filename} downloaded") | |
| return dest_path | |
| except Exception as exc: | |
| if hook.pbar: | |
| hook.pbar.close() | |
| dest_path.unlink(missing_ok=True) | |
| print(f" [FAIL] {filename}: {exc}") | |
| return None | |
| def prefetch_timm_weights(model_name: str, status_only: bool = False) -> bool: | |
| ok, detail = import_status("timm") | |
| if not ok: | |
| print(f" [SKIP] timm:{model_name} - timm missing ({detail})") | |
| return False | |
| if status_only: | |
| print(f" [CHECK] timm:{model_name} cache status cannot be proven without instantiation") | |
| return True | |
| import timm | |
| candidates = TIMM_ALIASES.get(model_name, (model_name,)) | |
| for candidate in candidates: | |
| try: | |
| model = timm.create_model(candidate, pretrained=True, num_classes=0) | |
| del model | |
| print(f" [OK] timm {model_name} via {candidate}") | |
| return True | |
| except RuntimeError as exc: | |
| if "Unknown model" not in str(exc): | |
| print(f" [FAIL] timm {model_name}: {exc}") | |
| return False | |
| print(f" [WARN] timm {getattr(timm, '__version__', 'unknown')} does not provide {model_name}") | |
| return False | |
| def prefetch_torchvision_weights(model_name: str, weight_name: str, status_only: bool = False) -> bool: | |
| ok, detail = import_status("torchvision.models") | |
| if not ok: | |
| print(f" [SKIP] torchvision:{model_name} - torchvision missing ({detail})") | |
| return False | |
| if status_only: | |
| print(f" [CHECK] torchvision:{model_name} cache status cannot be proven without instantiation") | |
| return True | |
| import torchvision.models as models | |
| builder = getattr(models, model_name) | |
| enum_name = "".join(part.capitalize() for part in model_name.split("_")) + "_Weights" | |
| weights_enum = getattr(models, enum_name, None) | |
| try: | |
| if weights_enum is not None: | |
| weights = getattr(weights_enum, weight_name, weights_enum.DEFAULT) | |
| model = builder(weights=weights) | |
| else: | |
| model = builder(pretrained=True) | |
| del model | |
| print(f" [OK] torchvision {model_name}") | |
| return True | |
| except Exception as exc: | |
| print(f" [FAIL] torchvision {model_name}: {exc}") | |
| return False | |
| def download_weights(status_only: bool = False) -> dict[str, int]: | |
| print("\nPRETRAINED WEIGHTS") | |
| print("------------------") | |
| ok = missing = 0 | |
| for spec in ZENODO_WEIGHTS.values(): | |
| path = download_zenodo_weight( | |
| spec["record_id"], | |
| spec["filename"], | |
| spec["dest_dir"], | |
| expected_sha256=spec["sha256"], | |
| status_only=status_only, | |
| ) | |
| ok += int(path is not None) | |
| missing += int(path is None) | |
| for model_name in TIMM_WEIGHTS_TO_PREFETCH: | |
| ok += int(prefetch_timm_weights(model_name, status_only=status_only)) | |
| for model_name, weight_name in TORCHVISION_WEIGHTS_TO_PREFETCH: | |
| ok += int(prefetch_torchvision_weights(model_name, weight_name, status_only=status_only)) | |
| return {"ok": ok, "missing": missing, "total": len(ZENODO_WEIGHTS) + len(TIMM_WEIGHTS_TO_PREFETCH) + len(TORCHVISION_WEIGHTS_TO_PREFETCH)} | |
| def parse_simple_yaml(path: Path) -> dict[str, object]: | |
| data: dict[str, object] = {} | |
| stack: list[tuple[int, dict[str, object]]] = [(-1, data)] | |
| for raw in path.read_text(encoding="utf-8").splitlines(): | |
| line = raw.split("#", 1)[0].rstrip() | |
| if not line.strip() or ":" not in line: | |
| continue | |
| indent = len(line) - len(line.lstrip(" ")) | |
| key, value = line.strip().split(":", 1) | |
| while stack and indent <= stack[-1][0]: | |
| stack.pop() | |
| current = stack[-1][1] | |
| value = value.strip() | |
| if not value: | |
| child: dict[str, object] = {} | |
| current[key] = child | |
| stack.append((indent, child)) | |
| continue | |
| if value.startswith("[") and value.endswith("]"): | |
| current[key] = [x.strip().strip("'\"") for x in value[1:-1].split(",") if x.strip()] | |
| elif value.isdigit(): | |
| current[key] = int(value) | |
| else: | |
| current[key] = value.strip("'\"") | |
| return data | |
| def dataset_root_from_config(cfg: dict[str, object]) -> Path: | |
| value = str(cfg.get("data_root", "")) | |
| data_root = os.environ.get("DATA_ROOT", str(ROOT.parent / "Datasets")) | |
| return Path(value.replace("${DATA_ROOT}", data_root)).expanduser() | |
| def prepare_dataset_lists(data_root: Path, image_a_folder: str = "A", splits: tuple[str, ...] = ("train", "val", "test")) -> dict[str, int]: | |
| list_dir = data_root / "list" | |
| list_dir.mkdir(parents=True, exist_ok=True) | |
| counts: dict[str, int] = {} | |
| for split in splits: | |
| out = list_dir / f"{split}.txt" | |
| if out.exists(): | |
| counts[split] = sum(1 for _ in out.open("r", encoding="utf-8")) | |
| print(f" [OK] {out} already exists ({counts[split]} entries)") | |
| continue | |
| split_dir = data_root / split | |
| img_dir = None | |
| for candidate in (image_a_folder, "A", "T1", "t1", "img", "images", "image"): | |
| if (split_dir / candidate).is_dir(): | |
| img_dir = split_dir / candidate | |
| break | |
| if img_dir is None: | |
| counts[split] = 0 | |
| print(f" [WARN] no image folder found under {split_dir}") | |
| continue | |
| names = sorted(p.name for p in img_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS) | |
| out.write_text("\n".join(names) + ("\n" if names else ""), encoding="utf-8") | |
| counts[split] = len(names) | |
| print(f" [OK] {out}: {len(names)} entries") | |
| return counts | |
| def prepare_datasets(selected: str | None = None, status_only: bool = False) -> dict[str, int]: | |
| print("\nDATASETS") | |
| print("--------") | |
| exists = missing = prepared = 0 | |
| paths = sorted(DATASET_CONFIGS.glob("*.yaml")) | |
| for path in paths: | |
| if selected and path.stem != selected: | |
| continue | |
| cfg = parse_simple_yaml(path) | |
| root = dataset_root_from_config(cfg) | |
| if root.is_dir(): | |
| exists += 1 | |
| print(f" [EXISTS] {path.stem}: {root}") | |
| if not status_only: | |
| split_keys = tuple((cfg.get("splits") or {"train": "train", "val": "val", "test": "test"}).keys()) | |
| prepare_dataset_lists(root, image_a_folder=str(cfg.get("image_a_folder", "A")), splits=split_keys) | |
| prepared += 1 | |
| else: | |
| missing += 1 | |
| print(f" [MISSING] {path.stem}: {root}") | |
| return {"exists": exists, "missing": missing, "prepared": prepared, "total": len(paths)} | |
| def print_final_status(env: dict[str, int], repos: dict[str, int], weights: dict[str, int] | None, datasets: dict[str, int]) -> None: | |
| print("\nSETUP STATUS") | |
| print("Component Status Notes") | |
| print("-----------------------------|-------------|----------------------------------") | |
| env_status = "COMPLETE" if env["missing"] == 0 else "CHECK" | |
| print(f"Python environment {env_status:<12} {env['installed']} packages auto-installed; {env['warnings']} manual warnings") | |
| print(f"Model repos ({repos['total']} total) {repos['ok']} / {repos['total']:<6} {repos['skipped']} URL-unconfirmed entries") | |
| if weights is None: | |
| print("Backbone weights SKIPPED run: python setup.py --weights-only") | |
| else: | |
| print(f"Backbone weights {weights['ok']} / {weights['total']:<6} Zenodo, timm, and torchvision warmups") | |
| print(f"Datasets {datasets['exists']} / {datasets['total']:<6} roots found under DATA_ROOT/default search") | |
| print("\nNext steps:") | |
| print(" export DATA_ROOT=/path/to/datasets") | |
| print(" python run_training.py --model bifa --dataset levir_cd --dry-run") | |
| print(" python run_training.py --model bifa --dataset levir_cd") | |
| print(" For ChangeMamba/Changer, install the MMSeg stack shown above.") | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="One-command setup for the CD-Models benchmark suite.") | |
| parser.add_argument("--skip-weights", action="store_true") | |
| parser.add_argument("--weights-only", action="store_true") | |
| parser.add_argument("--env-check-only", action="store_true") | |
| parser.add_argument("--status", action="store_true") | |
| parser.add_argument("--dataset", default=None, help="Prepare list files for one dataset config name.") | |
| return parser.parse_args() | |
| def main() -> int: | |
| args = parse_args() | |
| status_only = args.status | |
| env = check_environment(status_only=status_only) | |
| if args.env_check_only: | |
| print_final_status(env, {"ok": 0, "missing": 0, "skipped": 0, "total": len(CONFIRMED_REPOS)}, None, {"exists": 0, "missing": 0, "prepared": 0, "total": 0}) | |
| return 0 if env["warnings"] == 0 else 1 | |
| repos = {"ok": 0, "missing": 0, "skipped": 0, "total": len(CONFIRMED_REPOS)} | |
| if not args.weights_only: | |
| repos = clone_repositories(status_only=status_only) | |
| weights = None | |
| if not args.skip_weights: | |
| weights = download_weights(status_only=status_only) | |
| datasets = prepare_datasets(selected=args.dataset, status_only=status_only) | |
| print_final_status(env, repos, weights, datasets) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |