Tri-Netra-AI / src /research /dashboard_integration.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
11.4 kB
"""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")
@dataclass
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