SAMSpade / scripts /build_prototypes.py
AthithyaLogan's picture
feat: SamSpade dashboard deployment
eebd638 verified
Raw History Blame Contribute Delete
3.5 kB
"""Build the global K-means prototype corpus from all 2022 construction tiles.
Run this once after downloading tiles, before training.
Usage::
python scripts/build_prototypes.py \\
--tiles data/tiles/ \\
--k 20 \\
--out checkpoints/prototypes.pt
The script:
1. Loads every (tile_2022, mask_2022) pair from the tile directory.
2. Passes each tile through the frozen SAM2 image encoder.
3. Extracts foreground feature vectors (where mask > 0).
4. Runs KMeans(k) across all sites → k cluster centroids.
5. Saves as a dict: {"prototypes": Tensor[k, 256], "k": int, "inertia": float}.
"""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import torch
from omegaconf import OmegaConf
from data.dataset import ConstructionChangeDataset
from models.corpus_prototype import build_corpus_prototypes
from models.detector import ConstructionChangeDetector
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Build SAM2 prototype corpus from 2022 tiles.")
p.add_argument("--tiles", required=True, help="Root tile directory.")
p.add_argument("--k", type=int, default=20, help="Number of prototype clusters.")
p.add_argument("--out", required=True, help="Output path for prototypes.pt.")
p.add_argument("--config", default="configs/base.yaml")
p.add_argument("--max-fg", type=int, default=2000, help="Max foreground features sampled per site.")
p.add_argument("--max-sites", type=int, default=None, help="Randomly sample this many sites (useful on CPU).")
p.add_argument("--sam2-model", default=None, help="Override SAM2 HF model id (e.g. facebook/sam2.1-hiera-tiny).")
return p.parse_args()
def main() -> None:
args = parse_args()
cfg = OmegaConf.load(args.config)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
logger.info("Device: %s", device)
# Load dataset (no augmentation, train split = all)
dataset = ConstructionChangeDataset(
tile_dir=args.tiles,
image_size=cfg.data.image_size,
augment=False,
min_fg_pixels=cfg.data.min_fg_pixels,
)
logger.info("Dataset: %d samples", len(dataset))
# Subsample sites if requested (useful on CPU)
if args.max_sites and args.max_sites < len(dataset):
import random, torch.utils.data as tud
indices = random.sample(range(len(dataset)), args.max_sites)
dataset = tud.Subset(dataset, indices)
logger.info("Subsampled to %d sites (--max-sites)", args.max_sites)
# Build model and load SAM2 encoder
sam2_id = args.sam2_model or cfg.model.sam2_hf_id
logger.info("Loading SAM2: %s", sam2_id)
model = ConstructionChangeDetector(k_prototypes=args.k)
model.load_sam2(sam2_id)
model.to(device)
# Build prototypes
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
prototypes = build_corpus_prototypes(
image_encoder=model.image_encoder,
dataset=dataset,
k=args.k,
max_fg_per_site=args.max_fg,
device=device,
output_path=args.out,
)
logger.info("Prototypes shape: %s", prototypes.shape)
logger.info("Saved to %s", args.out)
if __name__ == "__main__":
main()