from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path from typing import Any import yaml DEFAULT_CHECKPOINT = Path("checkpoints/detective_sam_v2.pth") CHECKPOINT_ALIASES = {"detective_sam_v2": DEFAULT_CHECKPOINT} V2_ADAPTER_TYPE = "spatial_cross_attention" V2_MASK_ADAPTER_TYPE = "transformer" @dataclass(frozen=True) class InferenceConfig: img_size: int prompt_dim: int downscale: int dropout_rate: float perturbation_type: str perturbation_intensity: float sam_config_file: str sam_checkpoint: str adapter_type: str = V2_ADAPTER_TYPE mask_adapter_type: str = V2_MASK_ADAPTER_TYPE @property def max_streams(self) -> int: return count_perturbation_streams(self.perturbation_type) def resolve_checkpoint_path(checkpoint_value: str | Path | None, repo_root: str | Path) -> Path: repo_root = Path(repo_root) if checkpoint_value is None: return repo_root / DEFAULT_CHECKPOINT checkpoint_str = str(checkpoint_value) if checkpoint_str in CHECKPOINT_ALIASES: return repo_root / CHECKPOINT_ALIASES[checkpoint_str] checkpoint_path = Path(checkpoint_value) if checkpoint_path.is_absolute(): return checkpoint_path if checkpoint_path.exists(): return checkpoint_path.resolve() repo_candidate = repo_root / checkpoint_path if repo_candidate.exists(): return repo_candidate aliased_checkpoint = repo_root / "checkpoints" / f"{checkpoint_str}.pth" if aliased_checkpoint.exists(): return aliased_checkpoint return repo_candidate def resolve_repo_path(path_value: str | Path, repo_root: str | Path) -> Path: path = Path(path_value) if path.is_absolute(): return path repo_root = Path(repo_root) direct = repo_root / path if direct.exists(): return direct sam_config = repo_root / "sam2configs" / path.name if sam_config.exists(): return sam_config return direct def load_inference_config(checkpoint_path: str | Path) -> InferenceConfig: params = _load_params_file(checkpoint_path) config = InferenceConfig( img_size=int(_resolve_param(params, "img_size", section="training_config", default=512)), prompt_dim=int( _resolve_param( params, "prompt_dim", section="model_config", default=_resolve_param(params, "prompt", section="model_config", default=128), ) ), downscale=int(_resolve_param(params, "downscale", section="model_config", default=16)), dropout_rate=float( _resolve_param( params, "dropout_rate", section="model_config", default=_resolve_param(params, "dropout", section="model_config", default=0.1), ) ), perturbation_type=str(_resolve_param(params, "perturbation_type", section="data_config", default="none")), perturbation_intensity=float( _resolve_param(params, "perturbation_intensity", section="data_config", default=0.0) ), sam_config_file=str( _resolve_param(params, "sam_config_file", section="sam_config", default="sam2.1_hiera_b+.yaml") ), sam_checkpoint=str( _resolve_param( params, "sam_checkpoint", section="sam_config", default="sam2configs/sam2.1_hiera_base_plus.pt", ) ), adapter_type=str(_resolve_param(params, "adapter_type", section="model_config", default=V2_ADAPTER_TYPE)), mask_adapter_type=str( _resolve_param(params, "mask_adapter_type", section="model_config", default=V2_MASK_ADAPTER_TYPE) ), ) _validate_v2_config(config) return config def count_perturbation_streams(perturbation_type: str) -> int: if perturbation_type == "none": return 0 if "+" in perturbation_type: return len([item for item in perturbation_type.split("+") if item.strip()]) if "/" in perturbation_type: return len([item for item in perturbation_type.split("/") if item.strip()]) return 1 def _load_params_file(checkpoint_path: str | Path) -> dict[str, Any]: checkpoint_path = Path(checkpoint_path) candidate_paths = [ checkpoint_path.with_name(f"{checkpoint_path.stem}_params.yaml"), checkpoint_path.with_name(f"{checkpoint_path.stem}_params.yml"), checkpoint_path.with_name(f"{checkpoint_path.stem}_params.json"), checkpoint_path.parent / "model_params.yaml", checkpoint_path.parent / "model_params.yml", checkpoint_path.parent / "model_params.json", ] for candidate in candidate_paths: if candidate.exists(): with candidate.open("r", encoding="utf-8") as handle: loaded = json.load(handle) if candidate.suffix == ".json" else yaml.safe_load(handle) if not isinstance(loaded, dict): raise ValueError(f"Checkpoint params file must deserialize to a mapping: {candidate}") return loaded raise FileNotFoundError( f"Could not find a params file for checkpoint {checkpoint_path}. " f"Checked: {', '.join(str(path) for path in candidate_paths)}" ) def _resolve_param( params: dict[str, Any], key: str, *, section: str, default: Any, ) -> Any: if key in params: return params[key] return params.get(section, {}).get(key, default) def _validate_v2_config(config: InferenceConfig) -> None: if config.adapter_type != V2_ADAPTER_TYPE or config.mask_adapter_type != V2_MASK_ADAPTER_TYPE: raise ValueError( "This release is DetectiveSAMv2-only and requires " f"adapter_type={V2_ADAPTER_TYPE!r}, mask_adapter_type={V2_MASK_ADAPTER_TYPE!r}. " f"Got adapter_type={config.adapter_type!r}, mask_adapter_type={config.mask_adapter_type!r}." )