Spaces:
Sleeping
Sleeping
File size: 8,191 Bytes
5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd 5673379 add24bd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | """Prebackbone enrichment inference (A11_CA) — standalone, no ultralytics."""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import torch
from PIL import Image
from a11_ca import build_prebackbone
PREBACKBONE_ONLY_NAME = "prebackbone_a11_ca.pt"
def _to_numpy_u8(arr) -> np.ndarray:
"""Canonical uint8 HWC in conda numpy (avoids ~/.local numpy vs torch/opencv)."""
if isinstance(arr, Image.Image):
arr = arr.convert("RGB")
w, h = arr.size
return np.frombuffer(arr.tobytes(), dtype=np.uint8).reshape((h, w, 3)).copy()
raw = np.asarray(arr)
if raw.ndim == 2:
raw = np.stack([raw, raw, raw], axis=-1)
elif raw.shape[-1] > 3:
raw = raw[..., :3]
return np.array(raw.tolist(), dtype=np.uint8, order="C")
def _here() -> Path:
return Path(__file__).resolve().parent
def _prebackbone_only_path() -> Path:
env = os.environ.get("PREBACKBONE_ONLY_WEIGHTS", "").strip()
if env:
return Path(env).expanduser()
return _here() / "weights" / PREBACKBONE_ONLY_NAME
def _full_checkpoint_path() -> Path | None:
env = os.environ.get("PREBACKBONE_FULL_CKPT", "").strip()
if env:
p = Path(env).expanduser()
return p if p.exists() else None
for candidate in (
_here() / "weights" / "best.pt",
_here().parent
/ "ultralytics"
/ "Proposed"
/ "yolo12_training"
/ "HRIPCB_Results"
/ "yolo12n_hripcb_200epochs_batch16"
/ "weights"
/ "best.pt",
):
if candidate.exists():
return candidate
return None
def _download_hf_file(repo_id: str, filename: str) -> Path:
from huggingface_hub import hf_hub_download
dest_dir = _here() / "weights"
dest_dir.mkdir(parents=True, exist_ok=True)
return Path(hf_hub_download(repo_id=repo_id, filename=filename, local_dir=str(dest_dir)))
def _resolve_weights_path() -> Path:
pb_only = _prebackbone_only_path()
if pb_only.exists():
return pb_only
hf_repo = os.environ.get("HF_MODEL_REPO", "").strip()
if hf_repo:
try:
return _download_hf_file(hf_repo, PREBACKBONE_ONLY_NAME)
except Exception:
pass
env_weights = os.environ.get("PREBACKBONE_WEIGHTS", PREBACKBONE_ONLY_NAME)
return _download_hf_file(hf_repo, env_weights)
env = os.environ.get("PREBACKBONE_WEIGHTS", "").strip()
if env and Path(env).expanduser().exists():
return Path(env).expanduser()
return pb_only
def _maybe_extract_from_full_ckpt(pb_only_path: Path) -> Path:
if pb_only_path.exists():
return pb_only_path
full = _full_checkpoint_path()
if full is None:
return pb_only_path
from extract_prebackbone_weights import extract
print(f"[prebackbone] Extracting weights from {full} -> {pb_only_path}")
return extract(full, pb_only_path)
def _filter_state_dict(state: dict, module: torch.nn.Module) -> dict:
expected = set(module.state_dict().keys())
filtered = {k: v for k, v in state.items() if k in expected}
if len(filtered) < len(expected):
missing = expected - set(filtered.keys())
raise RuntimeError(f"Prebackbone weights missing keys: {sorted(missing)[:8]}...")
return filtered
def _load_prebackbone_module(weights_path: Path, device: torch.device) -> torch.nn.Module:
if not weights_path.exists():
weights_path = _maybe_extract_from_full_ckpt(weights_path)
if not weights_path.exists():
raise FileNotFoundError(
f"Prebackbone weights not found: {weights_path}\n"
"Run: python extract_prebackbone_weights.py --ckpt weights/best.pt\n"
"Or set PREBACKBONE_ONLY_WEIGHTS / HF_MODEL_REPO."
)
try:
payload = torch.load(weights_path, map_location="cpu", weights_only=True)
except TypeError:
payload = torch.load(weights_path, map_location="cpu")
if isinstance(payload, dict) and "state_dict" in payload:
name = str(payload.get("prebackbone", "A11_CA")).upper()
channels = int(payload.get("channels", 3))
state = payload["state_dict"]
else:
name, channels, state = "A11_CA", 3, payload
module = build_prebackbone(name, channels=channels)
if module is None:
raise RuntimeError(f"build_prebackbone({name}) returned None")
state = _filter_state_dict(state, module)
missing, unexpected = module.load_state_dict(state, strict=True)
if missing or unexpected:
raise RuntimeError(f"State dict mismatch: missing={missing}, unexpected={unexpected}")
return module.to(device).eval()
def _load_image_rgb(image: str | Path | Image.Image | np.ndarray) -> np.ndarray:
if isinstance(image, Image.Image):
return _to_numpy_u8(image.convert("RGB"))
if isinstance(image, np.ndarray):
arr = image
if arr.ndim == 2:
return _to_numpy_u8(np.stack([arr, arr, arr], axis=-1))
if arr.shape[2] == 4:
return _to_numpy_u8(arr[..., :3])
return _to_numpy_u8(arr[..., :3] if arr.shape[2] >= 3 else arr)
path = Path(image)
if not path.exists():
raise FileNotFoundError(f"Unable to read image: {path}")
return _to_numpy_u8(Image.open(path).convert("RGB"))
def _img_to_tensor_rgb(im_rgb: np.ndarray, device: torch.device) -> torch.Tensor:
arr = np.ascontiguousarray(_to_numpy_u8(im_rgb), dtype=np.uint8)
x = torch.tensor(arr, device=device, dtype=torch.float32)
return x.permute(2, 0, 1).contiguous().unsqueeze(0) / 255.0
def _tensor_to_rgb_u8(x: torch.Tensor) -> np.ndarray:
if x.ndim == 4:
x = x[0]
hwc = x.detach().float().clamp(0.0, 1.0).mul(255.0).round().byte().permute(1, 2, 0).cpu()
return np.array(hwc.tolist(), dtype=np.uint8)
class PreBackboneEnricher:
"""Runs A11_CA prebackbone only (defect + golden -> enriched, same spatial size)."""
def __init__(self, weights: str | Path | None = None, device: str | None = None):
if device is None:
device = os.environ.get("PREBACKBONE_DEVICE") or (
"cuda" if torch.cuda.is_available() else "cpu"
)
self.device = torch.device(device)
self.weights = Path(weights) if weights else _resolve_weights_path()
self.prebackbone = _load_prebackbone_module(self.weights, self.device)
@torch.inference_mode()
def enrich(
self,
defect: str | Path | Image.Image | np.ndarray,
reference: str | Path | Image.Image | np.ndarray,
*,
return_reference: bool = False,
) -> np.ndarray | tuple[np.ndarray, np.ndarray, np.ndarray]:
defect_rgb = _load_image_rgb(defect)
golden_rgb = _load_image_rgb(reference)
if defect_rgb.shape != golden_rgb.shape:
raise ValueError(
f"Defect and reference must have the same shape (HxWxC), "
f"got {defect_rgb.shape} vs {golden_rgb.shape}. "
"Use pre-aligned pairs (e.g. training prebackbone_samples) with no extra resizing."
)
defect_t = _img_to_tensor_rgb(defect_rgb, self.device)
golden_t = _img_to_tensor_rgb(golden_rgb, self.device)
_ = self.prebackbone(defect_t, golden_t)
dbg: dict[str, Any] = getattr(self.prebackbone, "_debug", {})
enriched = dbg.get("enriched")
if enriched is None:
raise RuntimeError("Prebackbone did not populate _debug['enriched'].")
enriched_rgb = _tensor_to_rgb_u8(enriched)
if return_reference:
return defect_rgb, golden_rgb, enriched_rgb
return enriched_rgb
_enricher: PreBackboneEnricher | None = None
def get_enricher() -> PreBackboneEnricher:
global _enricher
if _enricher is None:
_enricher = PreBackboneEnricher()
return _enricher
def enrich_pair(
defect: str | Path | Image.Image | np.ndarray,
reference: str | Path | Image.Image | np.ndarray,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
return get_enricher().enrich(defect, reference, return_reference=True) # type: ignore[return-value]
|