Spaces:
Runtime error
Runtime error
| """Glue layer between the conformal-counterfactual module and dashboard.py. | |
| Keeps the dashboard import surface tiny: a single | |
| `analyze(image_bytes, factual_prob_map) -> dict | None` call that handles | |
| artifact lookup, batched intervention inference, and result packing. | |
| Behaviour | |
| --------- | |
| - If `conformal_artifacts/` does not exist, returns None. The dashboard | |
| will see no `conformal_counterfactual` field and the UI degrades cleanly. | |
| - Otherwise loads every `*.json` calibration file, runs the corresponding | |
| intervention through the same ONNX session used for the factual | |
| segmentation, and returns a compact summary plus optional PNG heatmaps | |
| of (cf_prob, abstain, certified_disagree) per intervention. | |
| The point of integration in the dashboard is **after** the cascade pick. | |
| We re-use the factual probability map that the cascade already computed, | |
| so the only added cost is one forward pass per intervention. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import io | |
| import os | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Callable, Dict, List, Optional | |
| import numpy as np | |
| from .conformal_counterfactual_seg import ( | |
| ConformalCounterfactualSegmenter, | |
| IdentityIntervention, | |
| intervention_from_dict, | |
| ) | |
| ARTIFACTS_DIR_ENV = "CONFORMAL_ARTIFACTS_DIR" | |
| DEFAULT_ARTIFACTS_DIR = "conformal_artifacts" | |
| RETURN_HEATMAPS_ENV = "CONFORMAL_RETURN_HEATMAPS" | |
| def _artifacts_dir() -> Optional[Path]: | |
| p = Path(os.environ.get(ARTIFACTS_DIR_ENV, DEFAULT_ARTIFACTS_DIR)) | |
| if not p.exists() or not p.is_dir(): | |
| return None | |
| if not any(p.glob("*.json")): | |
| return None | |
| return p | |
| def _viridis_rgb(g: np.ndarray) -> np.ndarray: | |
| """Cheap 5-anchor viridis approximation, numpy-only. | |
| Matches the colormap helper used elsewhere in dashboard.py so the | |
| conformal heatmaps look consistent with the segmentation overlays. | |
| """ | |
| g = np.clip(g.astype(np.float32), 0.0, 1.0) | |
| anchors = np.array( | |
| [ | |
| [0.267, 0.005, 0.329], # 0.00 | |
| [0.282, 0.140, 0.458], # 0.25 | |
| [0.254, 0.265, 0.530], # 0.50 | |
| [0.207, 0.372, 0.553], # 0.75 | |
| [0.993, 0.906, 0.144], # 1.00 | |
| ], | |
| dtype=np.float32, | |
| ) | |
| t = g * 4.0 | |
| lo = np.clip(np.floor(t).astype(np.int32), 0, 3) | |
| hi = np.clip(lo + 1, 0, 4) | |
| frac = (t - lo)[..., None] | |
| out = anchors[lo] * (1.0 - frac) + anchors[hi] * frac | |
| return (out * 255).astype(np.uint8) | |
| def _encode_png_data_url(arr_uint8: np.ndarray) -> str: | |
| from PIL import Image | |
| buf = io.BytesIO() | |
| Image.fromarray(arr_uint8).save(buf, format="PNG") | |
| return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("utf-8") | |
| class _LoadedArtifact: | |
| slug: str | |
| state: dict | |
| def _load_artifacts(dirpath: Path) -> List[_LoadedArtifact]: | |
| out: List[_LoadedArtifact] = [] | |
| for p in sorted(dirpath.glob("*.json")): | |
| if p.name == "summary.json": | |
| continue | |
| try: | |
| import json | |
| d = json.loads(p.read_text()) | |
| out.append(_LoadedArtifact(slug=p.stem, state=d)) | |
| except Exception: | |
| continue | |
| return out | |
| def analyze( | |
| *, | |
| image_array: np.ndarray, | |
| factual_prob: np.ndarray, | |
| seg_fn: Callable[[np.ndarray], np.ndarray], | |
| threshold: float, | |
| return_heatmaps: Optional[bool] = None, | |
| seg_fn_batched: Optional[Callable[[np.ndarray], np.ndarray]] = None, | |
| ) -> Optional[Dict]: | |
| """Run the calibrated intervention battery on one image. | |
| Parameters | |
| ---------- | |
| image_array: HxWx3 float in [0,1] - the same input the factual | |
| segmenter consumed (post-resize, pre-normalisation, so the | |
| intervention acts on raw pixel space, not on normalised tensors). | |
| factual_prob: HxW float in [0,1] - the factual probability map the | |
| cascade winner produced. Used as the baseline against which each | |
| counterfactual is compared (effect = cf_prob - factual). | |
| seg_fn: callable(image HxWx3) -> prob HxW. The dashboard wraps the | |
| existing ONNX/PyTorch path so each intervention reuses the cached | |
| session and incurs only the cost of one forward pass. | |
| threshold: same threshold the factual segmenter used (e.g. 0.5). | |
| return_heatmaps: when True, embed PNG data-URLs for cf_prob and | |
| certified_disagree maps so the UI can render them. When False, | |
| return only scalar summary metrics. Default reads | |
| CONFORMAL_RETURN_HEATMAPS env var (off in production for payload | |
| size, on for local debugging). | |
| Returns | |
| ------- | |
| None when no calibration artifacts are found (graceful no-op). | |
| Otherwise a dict with: | |
| { | |
| "available": True, | |
| "artifacts_dir": str, | |
| "n_interventions": int, | |
| "interventions": [ | |
| { | |
| "slug": str, # filename minus .json | |
| "label": str, # human-readable | |
| "q": float, # calibrated conformal margin | |
| "alpha": float, | |
| "abstain_fraction": float, # voxelwise share in [0,1] | |
| "certified_disagree_fraction": float, # |cf - factual| > q | |
| "intervention_cf_area_px": int, # |cf >= threshold| | |
| "intervention_effect_mean": float, # mean(cf - factual) | |
| "cf_heatmap"?: data-url, | |
| "disagree_heatmap"?: data-url, | |
| }, | |
| ... | |
| ], | |
| "factual_baseline_area_px": int, | |
| "summary": { | |
| "max_disagree_fraction": float, | |
| "max_disagree_intervention": str, | |
| "most_robust_intervention": str, # lowest disagree fraction | |
| } | |
| } | |
| """ | |
| artifacts = _artifacts_dir() | |
| if artifacts is None: | |
| return None | |
| loaded = _load_artifacts(artifacts) | |
| if not loaded: | |
| return None | |
| if return_heatmaps is None: | |
| flag = os.environ.get(RETURN_HEATMAPS_ENV, "").strip().lower() | |
| return_heatmaps = flag in ("1", "true", "yes") | |
| factual_area = int((factual_prob >= float(threshold)).sum()) | |
| # ---- FAST PATH: batch all N interventions into ONE ORT call ---- | |
| # Apply each intervention to image_array, stack to (N, H, W, 3), pass | |
| # through seg_fn_batched in a single forward pass. Saves N-1 ORT calls | |
| # = ~5-6 sec on cpu-basic (vs 8 sequential ~6-8 sec). Falls back to | |
| # the slow per-intervention loop if seg_fn_batched is not provided. | |
| batched_cf_probs = None | |
| if seg_fn_batched is not None: | |
| try: | |
| interventions = [intervention_from_dict(la.state["intervention"]) | |
| for la in loaded] | |
| stacked = np.stack( | |
| [iv.apply(image_array) for iv in interventions], axis=0 | |
| ) # (N, H, W, 3) | |
| batched_cf_probs = seg_fn_batched(stacked) # (N, H, W) | |
| assert batched_cf_probs.shape[0] == len(loaded), \ | |
| f"batched seg_fn returned {batched_cf_probs.shape[0]} != {len(loaded)} probs" | |
| except Exception: | |
| # If anything in the fast path fails, fall back to slow path. | |
| batched_cf_probs = None | |
| rows: List[Dict] = [] | |
| for idx, la in enumerate(loaded): | |
| iv = intervention_from_dict(la.state["intervention"]) | |
| seg = ConformalCounterfactualSegmenter( | |
| seg_fn=seg_fn, | |
| intervention=iv, | |
| alpha=float(la.state["alpha"]), | |
| threshold=float(la.state["threshold"]), | |
| ) | |
| seg.q = float(la.state["q"]) | |
| seg.band_low = float(la.state["band_low"]) | |
| seg.band_high = float(la.state["band_high"]) | |
| seg.include_positive_in_pool = bool(la.state["include_positive_in_pool"]) | |
| # Use the pre-computed batched cf_prob if available; otherwise the | |
| # slow per-intervention seg_fn call inside seg.predict(). | |
| if batched_cf_probs is not None: | |
| cf_prob_pre = batched_cf_probs[idx].astype(np.float32) | |
| lower = np.clip(cf_prob_pre - seg.q, 0.0, 1.0) | |
| upper = np.clip(cf_prob_pre + seg.q, 0.0, 1.0) | |
| certain_fg = lower > seg.threshold | |
| certain_bg = upper < seg.threshold | |
| abstain = ~(certain_fg | certain_bg) | |
| out = { | |
| "cf_prob": cf_prob_pre, | |
| "lower": lower, "upper": upper, | |
| "certain_fg": certain_fg, "certain_bg": certain_bg, | |
| "abstain": abstain, | |
| "intervention": iv.to_dict(), | |
| "q": float(seg.q), | |
| "alpha": float(seg.alpha), | |
| } | |
| else: | |
| out = seg.predict(image_array) | |
| cf_prob = out["cf_prob"] | |
| effect = cf_prob - factual_prob | |
| certified_disagree = np.abs(effect) > out["q"] | |
| cf_area = int((cf_prob >= float(threshold)).sum()) | |
| row: Dict = { | |
| "slug": la.slug, | |
| "label": _human_label(la.slug), | |
| "q": float(out["q"]), | |
| "alpha": float(out["alpha"]), | |
| "abstain_fraction": float(out["abstain"].mean()), | |
| "certified_disagree_fraction": float(certified_disagree.mean()), | |
| "intervention_cf_area_px": cf_area, | |
| "intervention_effect_mean": float(effect.mean()), | |
| "intervention_effect_abs_max": float(np.abs(effect).max()), | |
| } | |
| if return_heatmaps: | |
| row["cf_heatmap"] = _encode_png_data_url(_viridis_rgb(cf_prob)) | |
| row["disagree_heatmap"] = _encode_png_data_url( | |
| _viridis_rgb(certified_disagree.astype(np.float32)) | |
| ) | |
| rows.append(row) | |
| # Pick most-disagreeing and most-robust interventions for at-a-glance. | |
| most_disagree = max(rows, key=lambda r: r["certified_disagree_fraction"], default=None) | |
| most_robust = min(rows, key=lambda r: r["certified_disagree_fraction"], default=None) | |
| return { | |
| "available": True, | |
| "artifacts_dir": str(artifacts), | |
| "n_interventions": len(rows), | |
| "interventions": rows, | |
| "factual_baseline_area_px": factual_area, | |
| "summary": { | |
| "max_disagree_fraction": ( | |
| float(most_disagree["certified_disagree_fraction"]) if most_disagree else 0.0 | |
| ), | |
| "max_disagree_intervention": most_disagree["slug"] if most_disagree else "", | |
| "most_robust_intervention": most_robust["slug"] if most_robust else "", | |
| }, | |
| "_method": "weighted-conformal-counterfactual (CONSeg + CausalX-Net unified)", | |
| } | |
| def _human_label(slug: str) -> str: | |
| if slug == "identity": | |
| return "Factual (no intervention)" | |
| if slug.startswith("modality_keep_"): | |
| ch = slug.replace("modality_keep_", "") | |
| return f"do(M = {ch}-only)" | |
| if slug.startswith("intensity_shift_"): | |
| sign = "+" if "+" in slug else "-" | |
| val = slug.split("_")[-1].replace("+", "").replace("-", "") | |
| return f"do(I = I {sign} {val})" | |
| if slug.startswith("contrast_scale_"): | |
| val = slug.split("_")[-1] | |
| return f"do(gamma = {val})" | |
| return slug | |