PV-PRISM / runtime.py
XiaoShengpeng's picture
Release complete PV-PRISM code, weights and reference assets
1fde8ef verified
Raw History Blame Contribute Delete
14.4 kB
"""PV-PRISM: complete reference-guided, multi-scale defect segmentation."""
from collections import OrderedDict
import copy
import hashlib
import json
import os
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parent
sys.dont_write_bytecode = True
os.environ.setdefault("MKL_THREADING_LAYER", "GNU")
os.environ.setdefault("USE_TF", "0")
os.environ.setdefault("HF_HUB_OFFLINE", "1")
sys.path.insert(0, str(ROOT))
import torch
import torch._dynamo
if torch.__version__.split("+")[0] != "2.7.1":
raise RuntimeError("Use the verified PyTorch 2.7.1 environment; see launch.py and requirements.txt")
from torch.nn import functional as F
from prism.pipeline.common import Dataset, flips
from prism.pipeline.evidence import Evidence
from prism.pipeline.components import CachedReferenceSegmenter, LocalExpert, FeatureCache, RegionVerifier, probabilities
from prism.models.encoder import FrozenEncoder
from prism.models.reference_bank import NormalBank
from prism.training.source_pretraining import load_heads
from prism.data.geometry import native_logits
from prism.data.geometry import read_image, make_canvas
from prism.data.loading import load_record
from prism.models.reference_segmenter import ReferenceSegmenter
def read(path):
return json.loads(Path(path).read_text())
def sha(path):
h = hashlib.sha256()
with Path(path).open("rb") as f:
for block in iter(lambda: f.read(8 << 20), b""):
h.update(block)
return h.hexdigest()
def asset(relative):
p = (ROOT / relative).resolve()
if not p.is_relative_to(ROOT) or not p.is_file():
raise ValueError(f"Missing or external asset: {relative}")
return p
class SupportDataset(Dataset):
"""Query images and annotated training supports have separate interfaces."""
def __init__(self, dataset, config):
self.name, self.spec = dataset, config["specification"]
self.train = []
for entry in config["supports"]:
row = dict(entry)
row["image_path"] = str(asset(row["image_path"]))
row["mask_path"] = str(asset(row["mask_path"]))
self.train.append(row)
self.dev = []
self.by_id = {r["sample_id"]: r for r in self.train}
self.image_cache, self.mask_cache = OrderedDict(), OrderedDict()
self.limit, self.checked = 48, set()
self.support_ids = set(self.by_id)
self.known_groups = config["known_image_groups"]
def target(self, row):
if row["sample_id"] not in self.support_ids:
raise ValueError("Query labels are inaccessible to PV-PRISM inference")
return super().target(row)
def query(self, path):
path = Path(path).resolve()
digest = sha(path)
return dict(sample_id="query-" + digest, image_path=str(path), sha256=digest,
group_id=self.known_groups.get(digest, "external-" + digest))
class ReferenceEvidence(Evidence):
"""Uses the unchanged registered evidence computation with local assets."""
def __init__(self, dataset, device, registry):
cfg = registry["datasets"][dataset]
self.data, self.device = SupportDataset(dataset, cfg), device
config = copy.deepcopy(registry["segmenter_config"])
for key in ("weights", "metadata"):
config["encoder"][key] = str(asset(config["encoder"][key]))
config["encoder"]["vendor"] = str((ROOT / config["encoder"]["vendor"]).resolve())
config["encoder"]["global_long_side"] = 1024
config["pv"]["classes"] = cfg["specification"]["classes"][1:]
self.core = CachedReferenceSegmenter(FrozenEncoder(config["encoder"]), config).to(device)
load_heads(self.core, asset(cfg["weights"]["segmenter"]))
self.core.eval().requires_grad_(False)
obj = torch.load(asset(registry["normal_bank"]), map_location="cpu", weights_only=True)
self.normal = NormalBank(**obj).validate(8).to(device)
self.features = FeatureCache(96)
self.expert = LocalExpert(len(self.data.spec["classes"]), False).to(device).eval().requires_grad_(False)
cp = torch.load(asset(cfg["weights"]["expert"]), map_location="cpu", weights_only=True)
self.expert.load_state_dict(cp["model"], strict=True)
self.expert_calls, self.support_items = 0, {}
self.support_selection = cfg["support_selection"]
def supports(self, c, query_group):
rows = [self.data.by_id[i] for i in self.support_selection[str(c)]
if self.data.by_id[i]["group_id"] != query_group][:5]
if len(rows) != 5 or len({r["group_id"] for r in rows}) != 5:
raise ValueError("Five distinct non-query training support groups are required")
out = []
for row in rows:
key = row["sample_id"], c
if key not in self.support_items:
self.support_items[key] = self.data.item(row, c, device=self.device)
out.append(self.support_items[key])
return out
class FrozenSupportSegmenter(ReferenceSegmenter):
"""Memoize immutable support prototypes for the contact refinement branch."""
def __init__(self, encoder, config):
super().__init__(encoder, config)
self.memo = {}
def _support(self, item, bank, preencoded=None):
if self.training or any(p.requires_grad for p in self.parameters()) or preencoded is not None:
raise ValueError("Support memoization requires a completely frozen model")
key = item["image_id"]
if key not in self.memo:
self.memo[key] = super()._support(item, bank)
return self.memo[key]
class DevelopmentEvidence:
"""The original two-channel task preserves canonical crack and any heads."""
def __init__(self, cfg, registry, device):
self.device, self.config = device, cfg
config = copy.deepcopy(cfg["segmenter_config"])
for key in ("weights", "metadata"):
config["encoder"][key] = str(asset(config["encoder"][key]))
config["encoder"]["vendor"] = str(ROOT / config["encoder"]["vendor"])
encoder = FrozenEncoder(config["encoder"])
self.canonical = ReferenceSegmenter(encoder, copy.deepcopy(config)).to(device).eval().requires_grad_(False)
load_heads(self.canonical, asset(cfg["weights"]["segmenter"]))
config["encoder"]["global_long_side"] = 1024
self.high = FrozenSupportSegmenter(encoder, config).to(device).eval().requires_grad_(False)
load_heads(self.high, asset(cfg["weights"]["segmenter"]))
self.normal = NormalBank(**torch.load(asset(registry["normal_bank"]), map_location="cpu", weights_only=True)).validate(8).to(device)
self.expert = LocalExpert(2, False).to(device).eval().requires_grad_(False)
self.expert.load_state_dict(torch.load(asset(cfg["weights"]["expert"]), map_location="cpu", weights_only=True)["model"], strict=True)
self.supports = {}
for category, records in cfg["supports"].items():
self.supports[category] = []
for record in records:
row = dict(record)
row["image_path"], row["mask_path"] = str(asset(row["image_path"])), str(asset(row["mask_path"]))
self.supports[category].append(load_record(row, category, device))
def query(self, path, view=0):
digest = sha(path)
group = self.config["known_image_groups"].get(digest, "external-" + digest)
if group in {s["group_id"] for rr in self.supports.values() for s in rr}:
raise ValueError("A fixed training support cannot be its own query group")
row = dict(image_id="anonymous", group_id=group, image_path=str(path))
item = load_record(row, device=self.device, augmentation=dict(horizontal=bool(view & 1), vertical=bool(view & 2), gain=1., gamma=1.))
if set(item) != {"image", "geometry_valid", "geometry", "group_id", "image_id"}:
raise ValueError("Query loader exposed annotation")
return item
def expert_evidence(self, path, view):
image, bits = read_image(path)
image = flips(image, view)
h, w = image.shape[-2:]
scale = 768 / max(h, w)
nh, nw = max(1, round(h * scale)), max(1, round(w * scale))
x = F.interpolate(image[None], (nh, nw), mode="bilinear", align_corners=False)[0]
x = F.pad(x, (0, 768 - nw, 0, 768 - nh))
_, _, geometry = make_canvas(image, bits)
with torch.autocast("cuda", dtype=torch.bfloat16):
z = self.expert(x[None].to(self.device))
z = F.interpolate(z, x.shape[-2:], mode="bilinear", align_corners=False)[:, :1].float()
content = F.interpolate(z[:, :, :nh, :nw], (geometry.content_h, geometry.content_w), mode="bilinear", align_corners=False)
return F.pad(content, (0, geometry.canvas_w - geometry.content_w, 0, geometry.canvas_h - geometry.content_h))[0], geometry
@torch.no_grad()
def predict(self, path, refiners):
q = self.query(path)
with torch.autocast("cuda", dtype=torch.bfloat16):
out = self.canonical(q["image"], q["geometry_valid"], [self.supports[c] for c in ("contact", "crack")], self.normal, subbank_predictions=False)
protected = native_logits(out["final"].float(), q["geometry"])[0]
any_logit = native_logits(out["any"].float(), q["geometry"])[0]
members = []
for view in (0, 1, 2, 3):
item = self.query(path, view)
with torch.autocast("cuda", dtype=torch.bfloat16):
out = self.high(item["image"], item["geometry_valid"], [self.supports["contact"]], self.normal, subbank_predictions=False)
evidence = torch.cat((out["final"][:, :1].float(), out["absolute"][:, :1].float(), out["any"].float(), out["gate"][:, :1].float()), 1)[0]
independent, geometry = self.expert_evidence(path, view)
if geometry != item["geometry"]:
raise ValueError("Expert alignment changed")
evidence = torch.cat((evidence, independent), 0)
for refiner in refiners:
delta = refiner.canvas_delta(item["image"], evidence, item["geometry_valid"])
z = native_logits((evidence[:1] + delta)[None], geometry)[0, 0]
members.append(flips(z.float(), view))
logits = torch.stack((torch.stack(members).mean(0), protected[1]))
if not torch.isfinite(logits).all() or not torch.isfinite(any_logit).all():
raise FloatingPointError("Nonfinite prediction")
return logits.sigmoid().cpu().numpy(), any_logit.sigmoid().cpu().numpy(), q
class PVPRISM:
"""One released architecture: high resolution, four views, two refiners."""
def __init__(self, dataset, device="cuda:0", memory_fraction=.5):
registry = read(ROOT / "configs/model.json")
if dataset not in registry["datasets"]:
raise ValueError(dataset)
self.dataset, self.device = dataset, torch.device(device)
self.config = registry["datasets"][dataset]
if self.device.type != "cuda" or not torch.cuda.is_available():
raise ValueError("The verified full model requires CUDA BF16; CPU parity is not claimed")
if not 0 < memory_fraction <= 1:
raise ValueError("memory_fraction must be in (0,1]")
torch.set_num_threads(2)
torch.manual_seed(17007)
torch.cuda.set_device(self.device)
torch.cuda.set_per_process_memory_fraction(memory_fraction, self.device)
torch.backends.cudnn.benchmark = False
torch.backends.cuda.matmul.allow_tf32 = False
torch.backends.cudnn.allow_tf32 = False
self.engine = (DevelopmentEvidence(self.config, registry, self.device) if dataset == "development"
else ReferenceEvidence(dataset, self.device, registry))
self.last_any_probability = None
self.refiners = []
for relative in self.config["weights"]["refiners"]:
model = RegionVerifier().to(self.device).eval().requires_grad_(False)
checkpoint = torch.load(asset(relative), map_location="cpu", weights_only=True)
model.load_state_dict(checkpoint["editor"], strict=True)
self.refiners.append(model)
@torch.no_grad()
def predict(self, image):
if self.dataset == "development":
p, self.last_any_probability, q = self.engine.predict(image, self.refiners)
return p, dict(model="PV-PRISM", dataset=self.dataset, classes=["contact", "crack"],
image_sha256=sha(image), group_id=q["group_id"], geometry=q["geometry"].to_dict(),
contact_global_resolution=1024, protected_global_resolution=768,
native_window=512, views=["I", "H", "V", "HV"], refiners=2,
query_gt_used=False, teacher_and_references_required=True,
inverse_map="native continuous logits -> independent sigmoid -> threshold 0.5",
any_output="independent canonical head, not a union of contact/crack")
row = self.engine.data.query(image)
classes = list(range(1, len(self.engine.data.spec["classes"])))
total = None
for view in (0, 1, 2, 3):
q, evidence = self.engine.encode(row, classes, view)
for refiner in self.refiners:
logits = []
for c, e in evidence.items():
delta = refiner.canvas_delta(q["image"], e, q["geometry_valid"])
z = native_logits((e[:1] + delta)[None], q["geometry"])[0]
logits.append(flips(z, view))
z = torch.cat(logits, 0).float() / 8
total = z if total is None else total + z
p = probabilities(total).cpu().numpy()
meta = dict(model="PV-PRISM", dataset=self.dataset, image_sha256=row["sha256"],
group_id=row["group_id"], classes=self.engine.data.spec["classes"],
global_resolution=1024, native_window=512, views=["I", "H", "V", "HV"],
refiners=2, geometry=q["geometry"].to_dict(), query_gt_used=False,
teacher_and_references_required=True,
inverse_map="native continuous logits -> softmax([0,foreground]) -> argmax")
return p, meta