Download train/wrapper_common.py from dineth18/CD-Models: direct link, hf CLI and curl.
- Browser
- Download file 49.2 kB
-
https://huggingface.co/dineth18/CD-Models/resolve/a95dbcec2bd609c4d1263e4e4e17525fa99446dd/train/wrapper_common.py
- Command line
-
hf download hf://dineth18/CD-Models@a95dbcec2bd609c4d1263e4e4e17525fa99446dd/train/wrapper_common.py
-
curl -L -o wrapper_common.py https://huggingface.co/dineth18/CD-Models/resolve/a95dbcec2bd609c4d1263e4e4e17525fa99446dd/train/wrapper_common.py
49.2 kB
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| import torch | |
| ROOT = Path(__file__).resolve().parents[1] | |
| if str(ROOT) not in sys.path: | |
| sys.path.insert(0, str(ROOT)) | |
| from utils.gpu_utils import ( | |
| gpu_mapping_message, | |
| print_gpu_diagnostics, | |
| resolve_gpu, | |
| subprocess_gpu_env, | |
| ) | |
| from utils.dataset_cache import apply_dataloader_cli_overrides, dataset_runtime_summary, print_dataloader_policy | |
| SKIP_EXIT_CODE = 75 | |
| FAILED_SUBPROCESS_EXIT_CODE = 70 | |
| FAILED_NO_CHECKPOINT_EXIT_CODE = 71 | |
| FAILED_EVAL_EXIT_CODE = 72 | |
| EXISTING_LEGACY = { | |
| "bifa": ("BiFA", ["python3", "train_wildfire.py"]), | |
| "bit_cd": ("BIT_CD", ["python3", "train_wildfire.py"]), | |
| "cdmamba": ("CDMamba", ["python3", "train_wildfire.py"]), | |
| "change3d": ("Change3D", ["python3", "train_wildfire.py"]), | |
| "changeformer": ("ChangeFormer", ["python3", "train_wildfire.py"]), | |
| "dsifn": ("IFNet", ["python3", "train_wildfire.py"]), | |
| "ifnet": ("IFNet", ["python3", "train_wildfire.py"]), | |
| "rsm_cd": ("RSM-CD/change_detection_mamba", ["python3", "train_wildfire.py"]), | |
| "schanger": ("SChanger", ["python3", "train_wildfire.py"]), | |
| "siam_nestedunet": ("Siam-NestedUNet", ["python3", "train_wildfire.py"]), | |
| "stanet": ("STANet", ["python3", "train_wildfire.py"]), | |
| } | |
| JSON_LEGACY_TRAINERS = {"bifa", "cdmamba"} | |
| MAIN_CD_TRAINERS = {"bit_cd", "changeformer"} | |
| NEW_REPOS = { | |
| "fc_ef": ("model_repos/fully_convolutional_change_detection", "https://github.com/rcdaudt/fully_convolutional_change_detection"), | |
| "fc_siam_conc": ("model_repos/fully_convolutional_change_detection", "https://github.com/rcdaudt/fully_convolutional_change_detection"), | |
| "fc_siam_diff": ("model_repos/fully_convolutional_change_detection", "https://github.com/rcdaudt/fully_convolutional_change_detection"), | |
| "dsifn": ("model_repos/DSIFN", "original DSIFN repo still requires URL confirmation"), | |
| "changemamba": ("model_repos/ChangeMamba", "https://github.com/ChenHongruixuan/ChangeMamba"), | |
| "elgcnet": ("model_repos/elgcnet", "https://github.com/techmn/elgcnet"), | |
| "changer": ("model_repos/open-cd", "https://github.com/likyoo/open-cd"), | |
| "hanet": ("model_repos/HANet-CD", "https://github.com/ChengxiHAN/HANet-CD"), | |
| "cgnet": ("model_repos/CGNet-CD", "https://github.com/ChengxiHAN/CGNet-CD"), | |
| "dsamnet": ("model_repos/DSAMNet", "https://github.com/liumency/DSAMNet"), | |
| "tinycd": ("model_repos/Tiny_model_4_CD", "https://github.com/AndreaCodegoni/Tiny_model_4_CD"), | |
| } | |
| TORCHVISION_WEIGHTS = { | |
| "changer": "resnet18", | |
| "dsamnet": "resnet18", | |
| "hanet": "resnet50", | |
| "cgnet": "resnet50", | |
| "dsifn": "vgg16", | |
| } | |
| TIMM_WEIGHTS = { | |
| "tinycd": "efficientnet_b4", | |
| "elgcnet": "mit_b0", | |
| "changeformer": "mit_b1", | |
| } | |
| EXTERNAL_TRAINABLE = {"dsamnet", "cgnet", "hanet", "tinycd"} | |
| def parse_args(model_name: str) -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=f"Train/evaluate {model_name} on a cd-models dataset.") | |
| parser.add_argument("--dataset", default="wildfire_s2") | |
| parser.add_argument("--epochs", type=int, default=None) | |
| parser.add_argument("--batch-size", type=int, default=None) | |
| parser.add_argument("--lr", type=float, default=None) | |
| parser.add_argument("--gpu", default="0") | |
| parser.add_argument("--resume", action="store_true") | |
| parser.add_argument("--eval-only", action="store_true") | |
| parser.add_argument("--dry-run", action="store_true") | |
| parser.add_argument("--smoke-test", action="store_true") | |
| parser.add_argument("--force", action="store_true") | |
| parser.add_argument("--output-dir", default=None) | |
| parser.add_argument("--model", default=model_name, help="Variant selector used by FC-Siam wrappers.") | |
| parser.add_argument("--max-iters", type=int, default=None, help="Iteration override for iteration-based upstream trainers.") | |
| parser.add_argument("--num-workers", type=int, default=None) | |
| parser.add_argument("--prefetch-factor", type=int, default=None) | |
| parser.set_defaults(persistent_workers=None, pin_memory=None) | |
| parser.add_argument("--persistent-workers", dest="persistent_workers", action="store_true") | |
| parser.add_argument("--no-persistent-workers", dest="persistent_workers", action="store_false") | |
| parser.add_argument("--pin-memory", dest="pin_memory", action="store_true") | |
| parser.add_argument("--no-pin-memory", dest="pin_memory", action="store_false") | |
| return parser.parse_args() | |
| def _ensure_pretrained(model_name: str, variant: str = "tiny") -> str | None: | |
| from utils.weight_downloader import ensure_timm_weight, ensure_torchvision_weight, ensure_weights | |
| if model_name == "changemamba": | |
| return ensure_weights(f"vmamba_{variant}") | |
| if model_name in TIMM_WEIGHTS: | |
| ensure_timm_weight(TIMM_WEIGHTS[model_name], required=model_name == "tinycd") | |
| if model_name in TORCHVISION_WEIGHTS: | |
| ensure_torchvision_weight(TORCHVISION_WEIGHTS[model_name]) | |
| return None | |
| def _dataset_ready(dataset_name: str, return_format: str = "tuple", dry_run: bool = False) -> dict: | |
| from datasets.cd_dataset import CDDataset | |
| from utils.config_loader import load_dataset_config | |
| cfg = load_dataset_config(dataset_name) | |
| root_exists = Path(cfg["data_root"]).exists() | |
| print( | |
| f"[DATASET] {cfg['name']} root={cfg['data_root']} img_size={cfg.get('img_size')} " | |
| f"channels={cfg.get('channels')} return_format={return_format}" | |
| ) | |
| print(f"[DATASET] {dataset_runtime_summary(cfg)}") | |
| print_dataloader_policy(cfg, torch.cuda.is_available()) | |
| if cfg.get("io_warning"): | |
| print(f"[DATASET-WARNING] {cfg['io_warning']}") | |
| print(f"[DRY-RUN] Dataset root exists: {root_exists} -> {cfg['data_root']}" if dry_run else f"[DATASET] Root exists: {root_exists}") | |
| if dry_run: | |
| return cfg | |
| if not root_exists: | |
| raise FileNotFoundError( | |
| f"Dataset root directory not found for {dataset_name}: {cfg['data_root']}. " | |
| "Set DATA_ROOT or edit the dataset YAML." | |
| ) | |
| try: | |
| from utils.dataset_list_generator import generate_list_files | |
| list_dir = Path(cfg["data_root"]) / "list" | |
| if not (list_dir / "train.txt").exists(): | |
| print(f"[SETUP] Generating list files for {dataset_name}...") | |
| generate_list_files( | |
| cfg["data_root"], | |
| splits=list((cfg.get("splits") or {"train": "train", "val": "val", "test": "test"}).keys()), | |
| img_subdir=cfg.get("image_a_folder", "A"), | |
| ) | |
| except Exception as exc: | |
| print(f"[SETUP] List-file generation skipped: {exc}") | |
| if not Path(cfg["data_root"]).exists(): | |
| print(f"[DATASET] Root does not exist yet, skipping CDDataset construction: {cfg['data_root']}") | |
| return cfg | |
| _ = CDDataset(cfg["data_root"], "train", cfg=cfg, return_format=return_format) | |
| return cfg | |
| def _run_logged(cmd: list[str], cwd: Path, env: dict, stdout_path: Path, stderr_path: Path) -> int: | |
| stdout_path.parent.mkdir(parents=True, exist_ok=True) | |
| stderr_path.parent.mkdir(parents=True, exist_ok=True) | |
| visible = env.get("CUDA_VISIBLE_DEVICES") | |
| if visible: | |
| print(_gpu_mapping_message(env)) | |
| with stdout_path.open("w", encoding="utf-8") as stdout, stderr_path.open("w", encoding="utf-8") as stderr: | |
| return subprocess.run(cmd, cwd=cwd, env=env, stdout=stdout, stderr=stderr, check=False).returncode | |
| def _run_streamed(cmd: list[str], cwd: Path, env: dict, log_path: Path) -> int: | |
| log_path.parent.mkdir(parents=True, exist_ok=True) | |
| with log_path.open("a", encoding="utf-8") as log: | |
| log.write("\n[WRAPPER] cwd=" + str(cwd) + "\n") | |
| log.write("[WRAPPER] " + _gpu_mapping_message(env) + "\n") | |
| log.write("[WRAPPER] command=" + " ".join(cmd) + "\n") | |
| log.flush() | |
| process = subprocess.Popen( | |
| cmd, | |
| cwd=cwd, | |
| env=env, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| bufsize=1, | |
| ) | |
| assert process.stdout is not None | |
| for line in process.stdout: | |
| print(line, end="", flush=True) | |
| log.write(line) | |
| log.flush() | |
| return process.wait() | |
| def _gpu_mapping_message(env: dict) -> str: | |
| return gpu_mapping_message(env=env) | |
| def _print_train_command(cwd: Path, cmd: list[str], env: dict) -> None: | |
| print(_gpu_mapping_message(env)) | |
| print("[TRAIN]", cwd, " ".join(cmd)) | |
| def _evaluate(model_name: str, dataset: str, gpu: str, dry_run: bool) -> int: | |
| cmd = [sys.executable, str(ROOT / "evaluate.py"), "--model", model_name, "--dataset", dataset, "--gpu", gpu] | |
| print("[EVAL]", " ".join(cmd)) | |
| if dry_run: | |
| return 0 | |
| log_dir = ROOT / "results" / model_name / dataset / "logs" | |
| env, _ = subprocess_gpu_env(gpu) | |
| return _run_logged(cmd, ROOT, env, log_dir / "eval_stdout.log", log_dir / "eval_stderr.log") | |
| def _legacy_checkpoint_candidates(model_name: str, dataset_name: str) -> list[Path]: | |
| if model_name != "cdmamba": | |
| return [] | |
| experiments = ROOT / "CDMamba" / "experiments" | |
| if not experiments.exists(): | |
| return [] | |
| candidates: list[Path] = [] | |
| run_dirs = sorted( | |
| ( | |
| path | |
| for path in experiments.glob(f"{dataset_name}-train-cdmamba_*") | |
| if path.is_dir() | |
| ), | |
| key=lambda path: path.stat().st_mtime, | |
| ) | |
| preferred_patterns = [ | |
| "checkpoint/best_cd_model_gen.pth", | |
| "checkpoint/*_gen.pth", | |
| "checkpoint/*best*.pth", | |
| "checkpoint/*.pth", | |
| "**/*.pth", | |
| "**/*.pt", | |
| ] | |
| for run_dir in run_dirs: | |
| for pattern in preferred_patterns: | |
| matches = [ | |
| path | |
| for path in sorted(run_dir.glob(pattern), key=lambda item: item.stat().st_mtime) | |
| if path.is_file() and "_opt" not in path.name.lower() | |
| ] | |
| if matches: | |
| candidates.extend(matches) | |
| break | |
| return candidates | |
| def _canonicalize_checkpoints(model_name: str, dataset_name: str) -> None: | |
| out_dir = ROOT / "results" / model_name / dataset_name | |
| ckpt_dir = out_dir / "checkpoints" | |
| ckpt_dir.mkdir(parents=True, exist_ok=True) | |
| best_out = ckpt_dir / "best_model.pth" | |
| latest_out = ckpt_dir / "latest.pth" | |
| if best_out.exists(): | |
| return | |
| patterns = [ | |
| "**/best_model.pth", | |
| "**/best_ckpt.pth", | |
| "**/best_ckpt.pt", | |
| "**/*best*.pth", | |
| "**/*best*.pt", | |
| "**/*_best_iou.pth", | |
| "**/netCD_epoch_*.pth", | |
| "**/*.pth", | |
| "**/*.pt", | |
| ] | |
| candidates: list[Path] = [] | |
| for pattern in patterns: | |
| for path in sorted(out_dir.glob(pattern)): | |
| if path.is_file() and path.resolve() != best_out.resolve(): | |
| candidates.append(path) | |
| if candidates: | |
| break | |
| if not candidates: | |
| candidates = _legacy_checkpoint_candidates(model_name, dataset_name) | |
| if not candidates: | |
| raise FileNotFoundError( | |
| f"Training completed for {model_name}/{dataset_name}, but no checkpoint file was found under " | |
| f"{out_dir} or known legacy output directories." | |
| ) | |
| source = candidates[-1] | |
| shutil.copy2(source, best_out) | |
| if not latest_out.exists(): | |
| shutil.copy2(source, latest_out) | |
| metadata = { | |
| "model": model_name, | |
| "dataset": dataset_name, | |
| "canonical_best_model": str(best_out), | |
| "source_checkpoint": str(source), | |
| "source_checkpoint_name": source.name, | |
| } | |
| (ckpt_dir / "checkpoint_metadata.json").write_text(json.dumps(metadata, indent=2), encoding="utf-8") | |
| def _checkpoint_files(checkpoint_dir: Path) -> list[Path]: | |
| suffixes = {".pth", ".pt", ".ckpt"} | |
| if not checkpoint_dir.exists(): | |
| return [] | |
| return sorted(path for path in checkpoint_dir.rglob("*") if path.is_file() and path.suffix.lower() in suffixes) | |
| def _print_checkpoint_failure(model_name: str, dataset_name: str, cmd: list[str], checkpoint_dir: Path, result_dir: Path) -> None: | |
| print(f"[FAILED_NO_CHECKPOINT] {model_name}/{dataset_name}: upstream command returned successfully but no checkpoint was found.") | |
| print(f"[FAILED_NO_CHECKPOINT] command={' '.join(cmd)}") | |
| print(f"[FAILED_NO_CHECKPOINT] expected checkpoint dir={checkpoint_dir}") | |
| if result_dir.exists(): | |
| existing = [str(path.relative_to(result_dir)) for path in sorted(result_dir.rglob("*"))[:80]] | |
| print(f"[FAILED_NO_CHECKPOINT] existing result files={existing}") | |
| else: | |
| print(f"[FAILED_NO_CHECKPOINT] result dir does not exist: {result_dir}") | |
| def _iteration_budget(model_name: str, model_cfg: dict, args: argparse.Namespace, default: int) -> tuple[int, str]: | |
| if args.max_iters is not None: | |
| return int(args.max_iters), "cli --max-iters" | |
| if args.smoke_test: | |
| return int(model_cfg.get("smoke_max_iters", min(default, 2))), "model smoke_max_iters" | |
| if "max_iters" in model_cfg: | |
| return int(model_cfg["max_iters"]), "model max_iters" | |
| return default, f"{model_name} wrapper default" | |
| def _model_config(model_name: str) -> dict: | |
| from utils.config_loader import load_model_config | |
| return load_model_config(model_name) | |
| def _legacy_dataset_token(dataset_cfg: dict) -> str: | |
| name = str(dataset_cfg.get("name", "")).lower() | |
| source = str(dataset_cfg.get("source_name", "")).lower() | |
| if "dsifn" in name or "dsifn" in source: | |
| return "DSIFN" | |
| if "levir" in name or "levir" in source: | |
| return "LEVIR" | |
| if "whu" in name or "whu" in source: | |
| return "WHU" | |
| if "wildfire" in name or "wildfire" in source: | |
| return "WildFireS2" | |
| return dataset_cfg.get("source_name", dataset_cfg["name"]) | |
| def _split_dir(dataset_cfg: dict, split: str) -> Path: | |
| return Path(dataset_cfg["data_root"]) / dataset_cfg.get("splits", {}).get(split, split) | |
| def _safe_link_or_copy(src: Path, dst: Path) -> None: | |
| if dst.is_symlink(): | |
| try: | |
| if Path(os.readlink(dst)) == src: | |
| return | |
| except OSError: | |
| pass | |
| dst.unlink() | |
| if dst.exists(): | |
| return | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| if not src.exists(): | |
| print(f"[DATASET-VIEW] Source path is not present yet, skipping generated link: {src}") | |
| return | |
| try: | |
| os.symlink(src, dst, target_is_directory=src.is_dir()) | |
| except OSError: | |
| if src.is_dir(): | |
| shutil.copytree(src, dst, dirs_exist_ok=True) | |
| else: | |
| shutil.copy2(src, dst) | |
| def _safe_mask_link_or_copy(src: Path, dst: Path) -> None: | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| if not src.exists(): | |
| print(f"[DATASET-VIEW] Source mask is not present yet, skipping generated link: {src}") | |
| return | |
| try: | |
| import numpy as np | |
| from PIL import Image | |
| arr = np.asarray(Image.open(src)) | |
| if arr.ndim == 3: | |
| arr = arr[..., 0] | |
| if arr.size and int(arr.max()) <= 1: | |
| if dst.exists() or dst.is_symlink(): | |
| dst.unlink() | |
| Image.fromarray((arr > 0).astype(np.uint8) * 255).save(dst, format="PNG") | |
| return | |
| except Exception as exc: | |
| print(f"[DATASET-VIEW] Could not inspect mask {src}: {exc}") | |
| _safe_link_or_copy(src, dst) | |
| def _scan_images(folder: Path) -> dict[str, Path]: | |
| exts = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"} | |
| if not folder.is_dir(): | |
| return {} | |
| return { | |
| p.stem: p | |
| for p in sorted(folder.iterdir()) | |
| if p.is_file() and p.suffix.lower() in exts | |
| } | |
| def _view_split_counts(dataset_cfg: dict) -> dict[str, int]: | |
| counts: dict[str, int] = {} | |
| for split in ("train", "val", "test"): | |
| split_root = _split_dir(dataset_cfg, split) | |
| a_files = _scan_images(split_root / dataset_cfg.get("image_a_folder", "A")) | |
| b_files = _scan_images(split_root / dataset_cfg.get("image_b_folder", "B")) | |
| label_files = _scan_images(split_root / dataset_cfg.get("mask_folder", "label")) | |
| counts[split] = len(set(a_files) & set(b_files) & set(label_files)) | |
| return counts | |
| def _log_generated_view(dataset_cfg: dict, view: Path) -> None: | |
| counts = _view_split_counts(dataset_cfg) | |
| print(f"[DATASET] dataset name: {dataset_cfg['name']}") | |
| print(f"[DATASET] original root: {dataset_cfg.get('original_data_root', dataset_cfg.get('data_root'))}") | |
| print(f"[DATASET] local root: {dataset_cfg.get('local_root')}") | |
| print(f"[DATASET] using local root: {bool(dataset_cfg.get('using_local_root'))}") | |
| print(f"[DATASET] generated view path: {view}") | |
| print(f"[DATASET] train/val/test counts: {counts}") | |
| def _prepare_matched_split_view(dataset_cfg: dict, split: str) -> Path: | |
| split_root = _split_dir(dataset_cfg, split) | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "matched" / split | |
| a_files = _scan_images(split_root / dataset_cfg.get("image_a_folder", "A")) | |
| b_files = _scan_images(split_root / dataset_cfg.get("image_b_folder", "B")) | |
| label_files = _scan_images(split_root / dataset_cfg.get("mask_folder", "label")) | |
| for folder in ("A", "B", "label"): | |
| (view / folder).mkdir(parents=True, exist_ok=True) | |
| for stem in sorted(set(a_files) & set(b_files) & set(label_files)): | |
| target_name = a_files[stem].name | |
| _safe_link_or_copy(a_files[stem], view / "A" / target_name) | |
| _safe_link_or_copy(b_files[stem], view / "B" / target_name) | |
| _safe_mask_link_or_copy(label_files[stem], view / "label" / target_name) | |
| _log_generated_view(dataset_cfg, view) | |
| return view | |
| def _prepare_split_view(dataset_cfg: dict, split: str) -> Path: | |
| split_root = _split_dir(dataset_cfg, split) | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / split | |
| mapping = { | |
| "A": split_root / dataset_cfg.get("image_a_folder", "A"), | |
| "B": split_root / dataset_cfg.get("image_b_folder", "B"), | |
| "label": split_root / dataset_cfg.get("mask_folder", "label"), | |
| } | |
| for target, src in mapping.items(): | |
| _safe_link_or_copy(src, view / target) | |
| _log_generated_view(dataset_cfg, view) | |
| return view | |
| def _prepare_tinycd_view(dataset_cfg: dict) -> Path: | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "tinycd" | |
| list_dir = view / "list" | |
| list_dir.mkdir(parents=True, exist_ok=True) | |
| for folder in ("A", "B", "label"): | |
| (view / folder).mkdir(parents=True, exist_ok=True) | |
| for split in ("train", "val", "test"): | |
| split_root = _split_dir(dataset_cfg, split) | |
| a_files = _scan_images(split_root / dataset_cfg.get("image_a_folder", "A")) | |
| b_files = _scan_images(split_root / dataset_cfg.get("image_b_folder", "B")) | |
| label_files = _scan_images(split_root / dataset_cfg.get("mask_folder", "label")) | |
| names = [] | |
| for stem in sorted(set(a_files) & set(b_files) & set(label_files)): | |
| target_name = f"{split}__{a_files[stem].name}" | |
| _safe_link_or_copy(a_files[stem], view / "A" / target_name) | |
| _safe_link_or_copy(b_files[stem], view / "B" / target_name) | |
| _safe_mask_link_or_copy(label_files[stem], view / "label" / target_name) | |
| names.append(target_name) | |
| (list_dir / f"{split}.txt").write_text("\n".join(names) + ("\n" if names else ""), encoding="utf-8") | |
| _log_generated_view(dataset_cfg, view) | |
| return view | |
| def _prepare_changemamba_view(dataset_cfg: dict) -> Path: | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "changemamba" | |
| split_map = {"train": "train", "test": "val"} | |
| for target_split, source_split in split_map.items(): | |
| for folder in ("T1", "T2", "GT"): | |
| (view / target_split / folder).mkdir(parents=True, exist_ok=True) | |
| split_root = _split_dir(dataset_cfg, source_split) | |
| a_files = _scan_images(split_root / dataset_cfg.get("image_a_folder", "A")) | |
| b_files = _scan_images(split_root / dataset_cfg.get("image_b_folder", "B")) | |
| label_files = _scan_images(split_root / dataset_cfg.get("mask_folder", "label")) | |
| names = [] | |
| for stem in sorted(set(a_files) & set(b_files) & set(label_files)): | |
| target_name = f"{source_split}__{a_files[stem].name}" | |
| _safe_link_or_copy(a_files[stem], view / target_split / "T1" / target_name) | |
| _safe_link_or_copy(b_files[stem], view / target_split / "T2" / target_name) | |
| _safe_mask_link_or_copy(label_files[stem], view / target_split / "GT" / target_name) | |
| names.append(target_name) | |
| list_name = "train_set.txt" if target_split == "train" else "test_set.txt" | |
| (view / list_name).write_text("\n".join(names) + ("\n" if names else ""), encoding="utf-8") | |
| _log_generated_view(dataset_cfg, view) | |
| return view | |
| def _prepare_opencd_view(dataset_cfg: dict) -> Path: | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "opencd" | |
| for split in ("train", "val", "test"): | |
| for folder in ("A", "B", "label"): | |
| (view / split / folder).mkdir(parents=True, exist_ok=True) | |
| split_root = _split_dir(dataset_cfg, split) | |
| a_files = _scan_images(split_root / dataset_cfg.get("image_a_folder", "A")) | |
| b_files = _scan_images(split_root / dataset_cfg.get("image_b_folder", "B")) | |
| label_files = _scan_images(split_root / dataset_cfg.get("mask_folder", "label")) | |
| for stem in sorted(set(a_files) & set(b_files) & set(label_files)): | |
| image_name = a_files[stem].name | |
| label_name = f"{a_files[stem].stem}{label_files[stem].suffix}" | |
| _safe_link_or_copy(a_files[stem], view / split / "A" / image_name) | |
| _safe_link_or_copy(b_files[stem], view / split / "B" / image_name) | |
| _safe_link_or_copy(label_files[stem], view / split / "label" / label_name) | |
| _log_generated_view(dataset_cfg, view) | |
| return view | |
| def _write_opencd_changer_config(dataset_cfg: dict, model_cfg: dict, view: Path) -> Path: | |
| train_split = _split_dir(dataset_cfg, "train") | |
| n_train = len(_scan_images(train_split / dataset_cfg.get("image_a_folder", "A"))) | |
| batch_size = int(dataset_cfg.get("batch_size", 8)) | |
| steps_per_epoch = max((n_train + batch_size - 1) // batch_size, 1) | |
| max_iters = int(model_cfg.get("max_iters", int(model_cfg.get("num_epochs", 200)) * steps_per_epoch)) | |
| val_interval = max(steps_per_epoch, 1) | |
| cfg_path = ROOT / "generated_configs" / f"{dataset_cfg['name']}__changer_opencd.py" | |
| cfg_path.parent.mkdir(parents=True, exist_ok=True) | |
| cfg_path.write_text( | |
| "\n".join( | |
| [ | |
| f"_base_ = '{ROOT / 'model_repos' / 'open-cd' / 'configs' / 'changer' / 'changer_ex_r18_512x512_40k_levircd.py'}'", | |
| "", | |
| "dataset_type = 'DSIFN_Dataset'", | |
| f"data_root = r'{view}'", | |
| f"crop_size = ({int(dataset_cfg.get('img_size', model_cfg.get('img_size', 256)))}, {int(dataset_cfg.get('img_size', model_cfg.get('img_size', 256)))})", | |
| "", | |
| "train_dataloader = dict(", | |
| f" batch_size={batch_size},", | |
| f" num_workers={int(dataset_cfg.get('num_workers', 4))},", | |
| " dataset=dict(", | |
| " type=dataset_type,", | |
| " data_root=data_root,", | |
| " data_prefix=dict(seg_map_path='train/label', img_path_from='train/A', img_path_to='train/B')))", | |
| "val_dataloader = dict(", | |
| " dataset=dict(", | |
| " type=dataset_type,", | |
| " data_root=data_root,", | |
| " data_prefix=dict(seg_map_path='val/label', img_path_from='val/A', img_path_to='val/B')))", | |
| "test_dataloader = dict(", | |
| " dataset=dict(", | |
| " type=dataset_type,", | |
| " data_root=data_root,", | |
| " data_prefix=dict(seg_map_path='test/label', img_path_from='test/A', img_path_to='test/B')))", | |
| "", | |
| f"train_cfg = dict(type='IterBasedTrainLoop', max_iters={max_iters}, val_interval={val_interval})", | |
| f"work_dir = r'{ROOT / 'results' / 'changer' / dataset_cfg['name'] / 'work_dir'}'", | |
| "", | |
| ] | |
| ), | |
| encoding="utf-8", | |
| ) | |
| return cfg_path | |
| def _write_hanet_metadata(dataset_cfg: dict, model_cfg: dict, view_root: Path) -> Path: | |
| out_dir = ROOT / "results" / "hanet" / dataset_cfg["name"] | |
| metadata = { | |
| "patch_size": int(dataset_cfg.get("img_size", 256)), | |
| "augmentation": True, | |
| "num_gpus": 1, | |
| "num_workers": int(dataset_cfg.get("num_workers", 4)), | |
| "num_channel": 3, | |
| "EF": False, | |
| "epochs": int(model_cfg.get("num_epochs", 50)), | |
| "epochs_threshold": 15, | |
| "gamma": 0.5, | |
| "weight_decay": float(model_cfg.get("weight_decay", 5e-4) or 5e-4), | |
| "batch_size": int(dataset_cfg.get("batch_size", 8)), | |
| "learning_rate": float(model_cfg.get("lr", 5e-4)), | |
| "loss_function": "hybrid", | |
| "dataset_dir": str(view_root) + "/", | |
| "weight_dir": str(out_dir / "weights") + "/", | |
| "Output_dir": str(out_dir / "outputs") + "/", | |
| "log_dir": str(out_dir / "logs"), | |
| } | |
| path = ROOT / "generated_configs" / f"{dataset_cfg['name']}__hanet_metadata.json" | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") | |
| return path | |
| def _existing_train_command(model_name: str, dataset_cfg: dict, args: argparse.Namespace) -> tuple[Path, list[str], dict]: | |
| workdir_rel, default_cmd = EXISTING_LEGACY[model_name] | |
| workdir = ROOT / workdir_rel | |
| env, _ = subprocess_gpu_env(args.gpu) | |
| child_gpu = "0" | |
| env["CD_MODELS_DATASET_ROOT"] = dataset_cfg["data_root"] | |
| env["CD_MODELS_DSIFN_ROOT"] = dataset_cfg["data_root"] | |
| env["CD_MODELS_DATASET_NAME"] = dataset_cfg["name"] | |
| env["CD_MODELS_NUM_WORKERS"] = str(int(dataset_cfg.get("num_workers", 2))) | |
| env["CD_MODELS_PREFETCH_FACTOR"] = str(int(dataset_cfg.get("prefetch_factor", 2))) | |
| env["CD_MODELS_PERSISTENT_WORKERS"] = "1" if dataset_cfg.get("persistent_workers") else "0" | |
| env["CD_MODELS_PIN_MEMORY"] = "1" if dataset_cfg.get("pin_memory", True) else "0" | |
| env["CD_MODELS_BATCH_SIZE"] = str(int(args.batch_size or dataset_cfg.get("batch_size", 8))) | |
| if args.epochs is not None: | |
| env["CD_MODELS_EPOCHS"] = str(int(args.epochs)) | |
| if dataset_cfg["name"] == "wildfire_s2": | |
| return workdir, [sys.executable, default_cmd[1]], env | |
| if model_name in JSON_LEGACY_TRAINERS: | |
| from utils.legacy_config_writer import write_bifa_or_cdmamba_config | |
| model_cfg = _model_config(model_name) | |
| if args.epochs is not None: | |
| model_cfg["num_epochs"] = int(args.epochs) | |
| if args.lr is not None: | |
| model_cfg["lr"] = float(args.lr) | |
| if args.batch_size is not None: | |
| dataset_cfg["batch_size"] = int(args.batch_size) | |
| config_path = write_bifa_or_cdmamba_config( | |
| model_name, | |
| dataset_cfg, | |
| model_cfg, | |
| prepare_view=not args.dry_run, | |
| ) | |
| return workdir, [sys.executable, "train_cd.py", "--config", str(config_path), "--phase", "train", "--gpu_ids", child_gpu], env | |
| if model_name in MAIN_CD_TRAINERS: | |
| from utils.legacy_config_writer import prepare_legacy_list_view | |
| legacy_root = ( | |
| ROOT / "generated_dataset_views" / dataset_cfg["name"] / "legacy_list" | |
| if args.dry_run | |
| else prepare_legacy_list_view(dataset_cfg) | |
| ) | |
| env["CD_MODELS_DATASET_ROOT"] = str(legacy_root) | |
| env["CD_MODELS_DSIFN_ROOT"] = str(legacy_root) | |
| model_cfg = _model_config(model_name) | |
| token = _legacy_dataset_token(dataset_cfg) | |
| project = f"{dataset_cfg['name']}-train-{model_name}" | |
| if model_name == "bit_cd": | |
| net_g = "base_transformer_pos_s4_dd8" | |
| optimizer = "sgd" | |
| loss = "ce" | |
| lr = float(model_cfg.get("lr", 0.01)) | |
| else: | |
| net_g = "ChangeFormerV6" | |
| optimizer = "adamw" | |
| loss = "ce" | |
| lr = float(model_cfg.get("lr", 0.00006)) | |
| cmd = [ | |
| sys.executable, | |
| "main_cd.py", | |
| "--gpu_ids", | |
| child_gpu, | |
| "--project_name", | |
| project, | |
| "--data_name", | |
| token, | |
| "--img_size", | |
| str(int(dataset_cfg.get("img_size", model_cfg.get("img_size", 256)))), | |
| "--batch_size", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--num_workers", | |
| str(int(dataset_cfg.get("num_workers", 4))), | |
| "--max_epochs", | |
| str(int(model_cfg.get("num_epochs", 200))), | |
| "--optimizer", | |
| optimizer, | |
| "--lr", | |
| str(lr), | |
| "--loss", | |
| loss, | |
| "--net_G", | |
| net_g, | |
| ] | |
| return workdir, cmd, env | |
| from utils.legacy_config_writer import prepare_legacy_list_view | |
| legacy_root = ( | |
| ROOT / "generated_dataset_views" / dataset_cfg["name"] / "legacy_list" | |
| if args.dry_run | |
| else prepare_legacy_list_view(dataset_cfg) | |
| ) | |
| out_dir = ROOT / "results" / model_name / dataset_cfg["name"] | |
| env["CD_MODELS_DATASET_ROOT"] = str(legacy_root) | |
| env["CD_MODELS_DSIFN_ROOT"] = str(legacy_root) | |
| env["CD_MODELS_DATASET_NAME"] = dataset_cfg["name"] | |
| env["CD_MODELS_CHECKPOINT_DIR"] = str(out_dir / "checkpoints") | |
| env["CD_MODELS_LOG_PATH"] = str(out_dir / "logs" / "train.log") | |
| if args.lr is not None: | |
| env["CD_MODELS_LR"] = str(float(args.lr)) | |
| else: | |
| env["CD_MODELS_LR"] = str(float(_model_config(model_name).get("lr", 1e-3))) | |
| return workdir, [sys.executable, "train_wildfire.py"], env | |
| def run_existing_model(model_name: str, args: argparse.Namespace) -> int: | |
| from utils.model_adapters import get_model_adapter | |
| from utils.unified_trainer import train_with_adapter | |
| adapter = get_model_adapter(model_name) | |
| if adapter.supports_unified_training: | |
| return train_with_adapter(model_name, args) | |
| dataset_cfg = _dataset_ready(args.dataset, return_format="legacy", dry_run=args.dry_run) | |
| apply_dataloader_cli_overrides(dataset_cfg, args) | |
| print_dataloader_policy(dataset_cfg, torch.cuda.is_available()) | |
| workdir_rel, cmd = EXISTING_LEGACY[model_name] | |
| workdir, cmd, env = _existing_train_command(model_name, dataset_cfg, args) | |
| if not cmd: | |
| print( | |
| f"[SKIP] {model_name}/{args.dataset}: no verified non-WildFire training command is wired for " | |
| f"{workdir_rel} yet." | |
| ) | |
| return 0 if args.dry_run else SKIP_EXIT_CODE | |
| _print_train_command(workdir, cmd, env) | |
| if args.dry_run: | |
| return _evaluate(model_name, args.dataset, args.gpu, dry_run=True) | |
| code = 0 | |
| if not args.eval_only: | |
| log_dir = ROOT / "results" / model_name / dataset_cfg["name"] / "logs" | |
| code = _run_logged(cmd, workdir, env, log_dir / "train_stdout.log", log_dir / "train_stderr.log") | |
| if code == 0: | |
| _canonicalize_checkpoints(model_name, dataset_cfg["name"]) | |
| code = _evaluate(model_name, args.dataset, args.gpu, dry_run=False) | |
| return code | |
| def run_external_model(model_name: str, args: argparse.Namespace) -> int: | |
| from utils.model_adapters import get_model_adapter | |
| from utils.unified_trainer import train_with_adapter | |
| adapter = get_model_adapter(model_name) | |
| if adapter.supports_unified_training: | |
| return train_with_adapter(model_name, args) | |
| cfg = _dataset_ready(args.dataset, return_format="tuple", dry_run=args.dry_run) | |
| apply_dataloader_cli_overrides(cfg, args) | |
| print_dataloader_policy(cfg, torch.cuda.is_available()) | |
| repo_rel, repo_url = NEW_REPOS[model_name] | |
| repo = ROOT / repo_rel | |
| if not repo.exists(): | |
| print( | |
| f"[SKIP] {model_name}/{args.dataset}: source repo is not present at {repo}. " | |
| f"Expected source: {repo_url}." | |
| ) | |
| return 0 if args.dry_run else SKIP_EXIT_CODE | |
| if model_name == "tinycd" and int(cfg.get("channels", 3)) > 3: | |
| raise ValueError("TinyCD supports RGB input only. Set channels: 3 in the dataset config override.") | |
| weight_path = None | |
| if not args.dry_run: | |
| weight_path = _ensure_pretrained(model_name) | |
| print(f"[READY] {model_name} repo found at {repo}") | |
| if weight_path: | |
| print(f"[WEIGHTS] {weight_path}") | |
| train_cmd, train_env, train_cwd = _external_train_command(model_name, cfg, args) | |
| if not train_cmd: | |
| print("[NOTE] Model-specific training command construction must be completed from the cloned repo config.") | |
| print(f"[SKIP] {model_name}/{args.dataset}: repo-specific train command wiring is not implemented yet.") | |
| return 0 if args.dry_run else SKIP_EXIT_CODE | |
| if args.dry_run or args.eval_only: | |
| _print_train_command(train_cwd, train_cmd, train_env) | |
| return _evaluate(model_name, args.dataset, args.gpu, dry_run=args.dry_run) | |
| _print_train_command(train_cwd, train_cmd, train_env) | |
| log_dir = ROOT / "results" / model_name / cfg["name"] / "logs" | |
| if model_name == "changemamba": | |
| result_dir = ROOT / "results" / model_name / cfg["name"] | |
| checkpoint_dir = result_dir / "checkpoints" | |
| code = _run_streamed(train_cmd, train_cwd, train_env, log_dir / "train_wrapper.log") | |
| if code != 0: | |
| return FAILED_SUBPROCESS_EXIT_CODE | |
| checkpoints = _checkpoint_files(checkpoint_dir) | |
| if not checkpoints: | |
| _print_checkpoint_failure(model_name, cfg["name"], train_cmd, checkpoint_dir, result_dir) | |
| return FAILED_NO_CHECKPOINT_EXIT_CODE | |
| _canonicalize_checkpoints(model_name, cfg["name"]) | |
| code = _evaluate(model_name, args.dataset, args.gpu, dry_run=False) | |
| return 0 if code == 0 else FAILED_EVAL_EXIT_CODE | |
| code = _run_logged(train_cmd, train_cwd, train_env, log_dir / "train_stdout.log", log_dir / "train_stderr.log") | |
| if code == 0 and model_name not in {"fc_ef", "fc_siam_conc", "fc_siam_diff"}: | |
| _canonicalize_checkpoints(model_name, cfg["name"]) | |
| code = _evaluate(model_name, args.dataset, args.gpu, dry_run=False) | |
| return code | |
| def run_smoke_test(model_name: str, args: argparse.Namespace) -> int: | |
| from datasets.cd_dataset import CDDataset | |
| from utils.config_loader import load_dataset_config, load_model_config | |
| from utils.metrics import BinaryMetrics | |
| from utils.model_adapters import get_model_adapter | |
| from utils.profiling import count_flops, count_parameters | |
| from utils.qualitative import safe_sample_id, save_binary_prediction, save_probability_map, select_or_load_manifest | |
| cfg = load_dataset_config(args.dataset) | |
| apply_dataloader_cli_overrides(cfg, args) | |
| print_dataloader_policy(cfg, torch.cuda.is_available()) | |
| root = Path(cfg["data_root"]) | |
| print(f"[SMOKE] {model_name}/{args.dataset}") | |
| print(f"[SMOKE] Dataset root exists: {root.is_dir()} -> {root}") | |
| if not root.is_dir(): | |
| return 1 | |
| ds = CDDataset(root, "test", cfg=cfg, return_format="tuple") | |
| a, b, mask, name = ds[0] | |
| print(f"[SMOKE] Batch sample: name={name} A={tuple(a.shape)} B={tuple(b.shape)} mask={tuple(mask.shape)}") | |
| adapter = get_model_adapter(model_name) | |
| if adapter.supports_inprocess_eval: | |
| gpu_resolution = resolve_gpu(args.gpu) | |
| print_gpu_diagnostics(gpu_resolution) | |
| device = torch.device(gpu_resolution.local_device) | |
| model = adapter.build_model(load_model_config(model_name), cfg, device) | |
| ckpt = ROOT / "results" / model_name / cfg["name"] / "checkpoints" / "best_model.pth" | |
| if ckpt.exists(): | |
| adapter.load_checkpoint(model, ckpt, device) | |
| print(f"[SMOKE] Loaded checkpoint: {ckpt}") | |
| else: | |
| print(f"[SMOKE] No checkpoint found at {ckpt}; validating construction and forward pass only.") | |
| batch = (a.unsqueeze(0), b.unsqueeze(0), mask.unsqueeze(0), [name]) | |
| train_step = None | |
| if adapter.supports_unified_training: | |
| model.train() | |
| optimizer = adapter.build_optimizer(model, load_model_config(model_name)) | |
| optimizer.zero_grad(set_to_none=True) | |
| raw_train = adapter.forward(model, batch, device) | |
| loss_dict = adapter.compute_loss(raw_train, batch, load_model_config(model_name), cfg, device) | |
| loss_dict["loss"].backward() | |
| optimizer.step() | |
| train_step = {key: float(value.detach().cpu().item()) for key, value in loss_dict.items()} | |
| model.eval() | |
| with torch.inference_mode(): | |
| raw = adapter.forward(model, batch, device) | |
| normalized = adapter.normalize_output(raw, batch, cfg) | |
| metrics = BinaryMetrics(threshold=float(cfg.get("eval", {}).get("threshold", 0.5))) | |
| metrics.update(normalized.metric_tensor, mask.unsqueeze(0)) | |
| params = count_parameters(model) | |
| flops = None | |
| if adapter.supports_flops: | |
| flops = count_flops(model, lambda: adapter.get_dummy_inputs(cfg, device), device) | |
| manifest = select_or_load_manifest(cfg) | |
| smoke_dir = ROOT / "results" / model_name / cfg["name"] / "smoke_test" | |
| clean_id = safe_sample_id(str(name)) | |
| save_binary_prediction(normalized.binary[0], smoke_dir / f"{clean_id}_pred.png") | |
| if normalized.score is not None: | |
| save_probability_map(normalized.score[0], smoke_dir / f"{clean_id}_prob.png") | |
| print( | |
| f"[SMOKE] PASS {model_name}/{cfg['name']} adapter={adapter.model_class_path} " | |
| f"output={tuple(normalized.metric_tensor.shape)} params_m={params['params_m']:.3f} " | |
| f"flops_g={None if flops is None else flops.get('flops_g')} " | |
| f"manifest_samples={manifest.get('selected_count')} train_step={train_step} metrics={metrics.compute()}" | |
| ) | |
| return 0 | |
| args.dry_run = True | |
| if model_name in EXISTING_LEGACY: | |
| workdir, cmd, env = _existing_train_command(model_name, cfg, args) | |
| else: | |
| cmd, env, workdir = _external_train_command(model_name, cfg, args) | |
| if cmd: | |
| print(_gpu_mapping_message(env)) | |
| print(f"[SMOKE] Command construction OK: cwd={workdir} cmd={' '.join(cmd)}") | |
| else: | |
| print("[SMOKE] Command construction reports this adapter is not wired yet.") | |
| print( | |
| f"[SMOKE-UNAVAILABLE] {model_name}/{args.dataset}: {adapter.notes_or_failure_reason}" | |
| ) | |
| return 1 | |
| def _external_train_command(model_name: str, dataset_cfg: dict, args: argparse.Namespace) -> tuple[list[str], dict, Path]: | |
| from utils.legacy_config_writer import prepare_legacy_list_view | |
| env, _ = subprocess_gpu_env(args.gpu) | |
| child_gpu = "0" | |
| model_cfg = _model_config(model_name) | |
| out_dir = ROOT / "results" / model_name / dataset_cfg["name"] | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| if model_name == "dsamnet": | |
| repo = ROOT / "model_repos" / "DSAMNet" | |
| train_root = _split_dir(dataset_cfg, "train") | |
| val_root = _split_dir(dataset_cfg, "val") | |
| ckpt_dir = out_dir / "checkpoints" | |
| ckpt_dir.mkdir(parents=True, exist_ok=True) | |
| cmd = [ | |
| sys.executable, | |
| "train.py", | |
| "--num_epochs", | |
| str(int(model_cfg.get("num_epochs", 100))), | |
| "--batchsize", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--val_batchsize", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--num_workers", | |
| str(int(dataset_cfg.get("num_workers", 4))), | |
| "--gpu_id", | |
| child_gpu, | |
| "--train1_dir", | |
| str(train_root / dataset_cfg.get("image_a_folder", "A")), | |
| "--train2_dir", | |
| str(train_root / dataset_cfg.get("image_b_folder", "B")), | |
| "--label_train", | |
| str(train_root / dataset_cfg.get("mask_folder", "label")), | |
| "--val1_dir", | |
| str(val_root / dataset_cfg.get("image_a_folder", "A")), | |
| "--val2_dir", | |
| str(val_root / dataset_cfg.get("image_b_folder", "B")), | |
| "--label_val", | |
| str(val_root / dataset_cfg.get("mask_folder", "label")), | |
| "--model_dir", | |
| str(ckpt_dir) + "/", | |
| "--sta_dir", | |
| str(out_dir / "statistics.csv"), | |
| ] | |
| return cmd, env, repo | |
| if model_name == "cgnet": | |
| repo = ROOT / "model_repos" / "CGNet-CD" | |
| if args.dry_run: | |
| train_view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "train" | |
| val_view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "val" | |
| else: | |
| train_view = _prepare_matched_split_view(dataset_cfg, "train") | |
| val_view = _prepare_matched_split_view(dataset_cfg, "val") | |
| cmd = [ | |
| sys.executable, | |
| "train_CGNet.py", | |
| "--epoch", | |
| str(int(model_cfg.get("num_epochs", 50))), | |
| "--batchsize", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--trainsize", | |
| str(int(dataset_cfg.get("img_size", 256))), | |
| "--gpu_id", | |
| child_gpu, | |
| "--data_name", | |
| dataset_cfg["name"], | |
| "--model_name", | |
| "CGNet", | |
| "--save_path", | |
| str(out_dir) + "/", | |
| "--train_root", | |
| str(train_view) + "/", | |
| "--val_root", | |
| str(val_view) + "/", | |
| ] | |
| return cmd, env, repo | |
| if model_name == "hanet": | |
| repo = ROOT / "model_repos" / "HANet-CD" | |
| view_root = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "hanet_matched" | |
| if not args.dry_run: | |
| for split in ("train", "val"): | |
| split_view = _prepare_matched_split_view(dataset_cfg, split) | |
| _safe_link_or_copy(split_view, view_root / split) | |
| env["HANET_METADATA_JSON"] = str(_write_hanet_metadata(dataset_cfg, model_cfg, view_root)) | |
| return [sys.executable, "trainHCX.py"], env, repo | |
| if model_name == "tinycd": | |
| repo = ROOT / "model_repos" / "Tiny_model_4_CD" | |
| view = ROOT / "generated_dataset_views" / dataset_cfg["name"] / "tinycd" if args.dry_run else _prepare_tinycd_view(dataset_cfg) | |
| cmd = [ | |
| sys.executable, | |
| "training.py", | |
| "--datapath", | |
| str(view), | |
| "--log-path", | |
| str(out_dir / "logs"), | |
| "--gpu-id", | |
| child_gpu, | |
| "--batch-size", | |
| str(int(args.batch_size or dataset_cfg.get("batch_size", 8))), | |
| "--epochs", | |
| str(int(args.epochs or model_cfg.get("num_epochs", 100))), | |
| ] | |
| return cmd, env, repo | |
| if model_name == "changer": | |
| repo = ROOT / "model_repos" / "open-cd" | |
| view = ( | |
| ROOT / "generated_dataset_views" / dataset_cfg["name"] / "opencd" | |
| if args.dry_run | |
| else _prepare_opencd_view(dataset_cfg) | |
| ) | |
| cfg_path = _write_opencd_changer_config(dataset_cfg, model_cfg, view) | |
| return [sys.executable, "tools/train.py", str(cfg_path), "--work-dir", str(out_dir / "work_dir")], env, repo | |
| if model_name == "changemamba": | |
| repo = ROOT / "model_repos" / "ChangeMamba" | |
| changedetection = repo / "changedetection" | |
| view = ( | |
| ROOT / "generated_dataset_views" / dataset_cfg["name"] / "changemamba" | |
| if args.dry_run | |
| else _prepare_changemamba_view(dataset_cfg) | |
| ) | |
| variant = str(model_cfg.get("vmamba_variant", "small")).lower() | |
| model_type = str(model_cfg.get("changemamba_model_type", f"MambaBCD_{variant.capitalize()}")) | |
| pretrained_name = { | |
| "tiny": "vssmtiny_dp01_ckpt_epoch_292.pth", | |
| "small": "vssmsmall_dp03_ckpt_epoch_238.pth", | |
| "base": "vssmbase_dp06_ckpt_epoch_241.pth", | |
| }.get(variant) | |
| cfg_name = { | |
| "tiny": "vssm_tiny_224_0229flex.yaml", | |
| "small": "vssm_small_224.yaml", | |
| "base": "vssm_base_224.yaml", | |
| }.get(variant) | |
| if pretrained_name is None or cfg_name is None: | |
| raise ValueError(f"Unsupported ChangeMamba VMamba variant: {variant}") | |
| pretrained = repo / "pretrained_weight" / pretrained_name | |
| cfg_path = changedetection / "configs" / "vssm1" / cfg_name | |
| max_iters, max_iters_source = _iteration_budget("changemamba", model_cfg, args, default=50000) | |
| print(f"[TRAIN-CFG] smoke_test={bool(args.smoke_test)}") | |
| print(f"[TRAIN-CFG] max_iters={max_iters}") | |
| print(f"[TRAIN-CFG] max_iters_source={max_iters_source}") | |
| cmd = [ | |
| sys.executable, | |
| "script/train_MambaBCD.py", | |
| "--dataset", | |
| _legacy_dataset_token(dataset_cfg), | |
| "--batch_size", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--num_workers", | |
| str(int(dataset_cfg.get("num_workers", 4))), | |
| "--crop_size", | |
| str(int(dataset_cfg.get("img_size", model_cfg.get("img_size", 256)))), | |
| "--max_iters", | |
| str(max_iters), | |
| "--model_type", | |
| model_type, | |
| "--model_param_path", | |
| str(out_dir / "checkpoints"), | |
| "--train_dataset_path", | |
| str(view / "train"), | |
| "--train_data_list_path", | |
| str(view / "train_set.txt"), | |
| "--test_dataset_path", | |
| str(view / "test"), | |
| "--test_data_list_path", | |
| str(view / "test_set.txt"), | |
| "--cfg", | |
| str(cfg_path), | |
| "--encoder_pretrained_path", | |
| str(pretrained), | |
| "--learning_rate", | |
| str(float(model_cfg.get("lr", 6e-5))), | |
| "--weight_decay", | |
| str(float(model_cfg.get("weight_decay", 0.01))), | |
| ] | |
| return cmd, env, changedetection | |
| if model_name in {"fc_ef", "fc_siam_conc", "fc_siam_diff"}: | |
| cmd = [ | |
| sys.executable, | |
| str(ROOT / "train" / "fc_adapter.py"), | |
| "--model", | |
| model_name, | |
| "--dataset", | |
| dataset_cfg["name"], | |
| "--gpu", | |
| str(args.gpu), | |
| ] | |
| if args.epochs is not None: | |
| cmd.extend(["--epochs", str(args.epochs)]) | |
| if args.batch_size is not None: | |
| cmd.extend(["--batch-size", str(args.batch_size)]) | |
| if args.lr is not None: | |
| cmd.extend(["--lr", str(args.lr)]) | |
| if args.resume: | |
| cmd.append("--resume") | |
| if args.eval_only: | |
| cmd.append("--eval-only") | |
| if args.force: | |
| cmd.append("--force") | |
| if args.output_dir: | |
| cmd.extend(["--output-dir", args.output_dir]) | |
| return cmd, env, ROOT | |
| if model_name == "elgcnet": | |
| repo = ROOT / "model_repos" / "elgcnet" | |
| view = ( | |
| ROOT / "generated_dataset_views" / dataset_cfg["name"] / "legacy_list" | |
| if args.dry_run | |
| else prepare_legacy_list_view(dataset_cfg) | |
| ) | |
| env["CD_MODELS_DATASET_ROOT"] = str(view) | |
| env["CD_MODELS_DSIFN_ROOT"] = str(view) | |
| cmd = [ | |
| sys.executable, | |
| "main_cd.py", | |
| "--gpu_ids", | |
| child_gpu, | |
| "--project_name", | |
| f"{dataset_cfg['name']}-train-{model_name}", | |
| "--data_name", | |
| _legacy_dataset_token(dataset_cfg), | |
| "--img_size", | |
| str(int(dataset_cfg.get("img_size", model_cfg.get("img_size", 256)))), | |
| "--batch_size", | |
| str(int(dataset_cfg.get("batch_size", 8))), | |
| "--num_workers", | |
| str(int(dataset_cfg.get("num_workers", 4))), | |
| "--max_epochs", | |
| str(int(model_cfg.get("num_epochs", 200))), | |
| "--optimizer", | |
| str(model_cfg.get("optimizer", "adamw")), | |
| "--lr", | |
| str(float(model_cfg.get("lr", 0.00031))), | |
| "--loss", | |
| "ce", | |
| "--net_G", | |
| "ELGCNet", | |
| ] | |
| return cmd, env, repo | |
| return [], env, ROOT | |
| def main_for(model_name: str) -> int: | |
| args = parse_args(model_name) | |
| gpu_resolution = resolve_gpu(args.gpu) | |
| print_gpu_diagnostics(gpu_resolution) | |
| selected = args.model if model_name == "fc_variants" else model_name | |
| if selected == "fc_variants": | |
| print("[SKIP] fc_variants requires --model fc_ef, --model fc_siam_conc, or --model fc_siam_diff.") | |
| return 0 if args.dry_run else SKIP_EXIT_CODE | |
| if args.smoke_test: | |
| return run_smoke_test(selected, args) | |
| if selected in EXISTING_LEGACY: | |
| return run_existing_model(selected, args) | |
| return run_external_model(selected, args) | |