Spaces:
Running
Running
Signs on the film, draw tool, My search off by default, example films and Radiopaedia links
66234d2 verified Download backend/app/reference.py from yashwrites/blindspot: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/backend/app/reference.py
- Command line
-
hf download hf://spaces/yashwrites/blindspot/backend/app/reference.py
-
curl -L -o reference.py https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/backend/app/reference.py
15.2 kB
| """Reference bank ("what does X look like"): teaching-card text + a few outlined example films per label. | |
| Examples are listed in config/reference_bank.yaml (written by pipeline/reference_bank.py, clinician-editable) and come | |
| from the BENCH split only. Volumetric (CT / MR) picks live in <processed>/reference_bank_volumetric.json | |
| ({"labels": {label: [{case_id, finding_id}]}}; `build_volumetric_bank` writes it); when that file is absent the bench | |
| volumetric cases are picked deterministically (sorted case ids, up to MAX_EXAMPLES per label). A volumetric example | |
| ships its label volume (`mask_url`, bench cases only: never served as cases). | |
| Bench cases are evaluation-only and are never served to learners, so their outlines are | |
| not a ground-truth leak. Defence in depth: whatever the YAML says, an entry is dropped here unless its case exists in | |
| this data bundle, is a bench case with no disqualifying QA flag, has its image on disk, and (for a label example) | |
| carries that finding with that label. Nothing here raises: a missing or broken YAML gives labels with no examples. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import os | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Any | |
| import yaml | |
| from pydantic import BaseModel | |
| from backend.app import config, tutor_bridge | |
| from backend.app.adaptive.selector import qa_ok | |
| from backend.app.cases import CaseRepository, get_repo, processed_root | |
| from backend.app.settings import get_settings | |
| from backend.app.signs import schematics_for_label | |
| from backend.app.volumes import provenance_badge, volume_url | |
| from shared.contracts import VOLUME_LABELS, Case, Component, Measure, TeachingCard, Window | |
| log = logging.getLogger("blindspot.reference") | |
| REFERENCE_SPLIT = "bench" | |
| MAX_EXAMPLES = 3 | |
| # ------------------------------------------------------------------ response models (GET /api/reference) | |
| class ReferenceFinding(BaseModel): | |
| finding_id: str # short id, e.g. "F1" | |
| polygon: list[tuple[float, float]] | None | |
| bbox: tuple[float, float, float, float] | |
| relative_location: str | None | |
| side: str | None | |
| class ReferenceNormal(BaseModel): | |
| case_id: str | |
| image_url: str | |
| width: int | |
| height: int | |
| class ReferenceExample(ReferenceNormal): | |
| finding: ReferenceFinding | |
| class ReferenceVolumeFinding(BaseModel): | |
| finding_id: str # short id | |
| bbox: tuple[float, float, float, float] # axial in-plane box (x0, y0, x1, y1) | |
| slice_range: list[int] | None | |
| centroid3: list[float] | None | |
| label_values: list[int] | None | |
| components: list[Component] | None | |
| measure: Measure | None | |
| relative_location: str | None | |
| side: str | None | |
| class ReferenceVolumeExample(BaseModel): | |
| """A bench CT/MR case with its label volume: volume_url = voxels, mask_url = labels (bench only).""" | |
| case_id: str | |
| modality: str | |
| body_region: str | None | |
| volume_url: str | |
| mask_url: str | |
| shape: list[int] | |
| spacing: list[float] | |
| window: Window | |
| sequence: str | None | |
| labels: dict[str, str] # mask value → id (organs and finding labels) | |
| measure_slice: int | None | |
| provenance: dict[str, Any] | None | |
| finding: ReferenceVolumeFinding | |
| class ReferenceLabel(BaseModel): | |
| label: str | |
| display: str | |
| kind: str | |
| one_liner: str | |
| key_signs: list[str] | |
| mimics: list[str] | |
| commonly_confused_with: list[str] | |
| search_tip: str | |
| radiopaedia_url: str | None | |
| review_status: str | |
| modality: str = "cxr" # the label's modality (taxonomy): cxr | ct | mr | |
| signs: list[str] = [] # sign schematic ids for this label (GET /api/signs/{id}) | |
| examples: list[ReferenceExample] | |
| volume_examples: list[ReferenceVolumeExample] = [] | |
| class ReferenceBank(BaseModel): | |
| labels: list[ReferenceLabel] | |
| normal_examples: list[ReferenceNormal] | |
| # ------------------------------------------------------------------ bank file | |
| def bank_path() -> Path: | |
| env = os.environ.get("BLINDSPOT_REFERENCE_BANK") | |
| return Path(env) if env else config.CONFIG_DIR / "reference_bank.yaml" | |
| def _read_bank(path: str, _mtime_ns: int) -> dict[str, Any]: | |
| try: | |
| doc = yaml.safe_load(Path(path).read_text()) | |
| except (OSError, yaml.YAMLError) as e: | |
| log.warning("reference bank %s unreadable: %s", path, e) | |
| return {} | |
| return doc if isinstance(doc, dict) else {} | |
| def load_bank(path: Path | None = None) -> dict[str, Any]: | |
| """Parsed YAML ({} when missing or malformed). Re-read when the file changes (clinician edits).""" | |
| p = Path(path) if path else bank_path() | |
| try: | |
| mtime = p.stat().st_mtime_ns | |
| except OSError: | |
| return {} | |
| return _read_bank(str(p), mtime) | |
| def _entries(x: Any) -> list[dict[str, Any]]: | |
| return [e for e in x if isinstance(e, dict)] if isinstance(x, list) else [] | |
| # ------------------------------------------------------------------ building the payload | |
| def image_url(case_id: str) -> str: | |
| """Same URL the reading room uses (base-path aware); see services.image_url.""" | |
| return f"{get_settings().api_prefix}/cases/{case_id}/image" | |
| def _usable_case(repo: CaseRepository, case_id: Any) -> Case | None: | |
| """The case if it may be shown as a reference example, else None (logged at debug; never raises).""" | |
| c = repo.get(case_id) if isinstance(case_id, str) else None | |
| if c is None: | |
| log.debug("reference: case %r not in this data bundle; skipped", case_id) | |
| return None | |
| if c.split != REFERENCE_SPLIT: | |
| log.warning("reference: %s is in split %r, not %r; refused", c.case_id, c.split, REFERENCE_SPLIT) | |
| return None | |
| if not qa_ok(c.qa_flags): | |
| log.warning("reference: %s has qa_flags %s; refused", c.case_id, c.qa_flags) | |
| return None | |
| try: | |
| if not repo.image_path(c).exists(): | |
| log.debug("reference: image for %s missing; skipped", c.case_id) | |
| return None | |
| except OSError: | |
| return None | |
| return c | |
| def _shape(c: Case) -> dict[str, Any]: | |
| return {"case_id": c.case_id, "image_url": image_url(c.case_id), "width": c.width, "height": c.height} | |
| def _example(repo: CaseRepository, label: str, entry: dict[str, Any]) -> dict[str, Any] | None: | |
| c = _usable_case(repo, entry.get("case_id")) | |
| fid = entry.get("finding_id") | |
| if c is None or not isinstance(fid, str): | |
| return None | |
| f = repo.finding(c, fid) | |
| if f is None or f.label != label: | |
| log.warning("reference: %s is not a %s finding on %s; skipped", fid, label, c.case_id) | |
| return None | |
| return { | |
| **_shape(c), | |
| "finding": { | |
| "finding_id": f.short_id, | |
| "polygon": [list(p) for p in f.geometry.polygon] if f.geometry.polygon else None, | |
| "bbox": list(f.geometry.bbox), | |
| "relative_location": f.relative_location, | |
| "side": f.side, | |
| }, | |
| } | |
| def _label_entry( | |
| repo: CaseRepository, label: str, card: TeachingCard | None, listed: list[dict[str, Any]] | |
| ) -> dict[str, Any]: | |
| examples: list[dict[str, Any]] = [] | |
| seen: set[str] = set() | |
| for e in listed: | |
| ex = _example(repo, label, e) | |
| if ex is None or ex["case_id"] in seen: | |
| continue | |
| seen.add(ex["case_id"]) | |
| examples.append(ex) | |
| if len(examples) >= MAX_EXAMPLES: | |
| break | |
| return { | |
| "label": label, | |
| "display": config.display(label), | |
| "kind": config.label_kind(label), | |
| "one_liner": card.one_liner if card else "", | |
| "key_signs": list(card.key_signs) if card else [], | |
| "mimics": list(card.mimics) if card else [], | |
| "commonly_confused_with": list(card.commonly_confused_with) if card else [], | |
| "search_tip": card.search_tip if card else "", | |
| "radiopaedia_url": card.radiopaedia_url if card else None, | |
| "review_status": card.review.status if card else "ai_draft", | |
| "modality": config.label_modality(label), | |
| "signs": schematics_for_label(label), | |
| "examples": examples, | |
| "volume_examples": volume_examples(repo, label), | |
| } | |
| # ------------------------------------------------------------------ volumetric examples | |
| def volumetric_bank_path(repo: CaseRepository | None = None) -> Path: | |
| env = os.environ.get("BLINDSPOT_REFERENCE_BANK_VOLUMETRIC") | |
| if env: | |
| return Path(env) | |
| return (repo.root if repo else processed_root()) / "reference_bank_volumetric.json" | |
| def load_volumetric_bank(repo: CaseRepository | None = None) -> dict[str, Any] | None: | |
| """Parsed JSON, or None when the file is missing/broken (then bench cases are picked automatically).""" | |
| p = volumetric_bank_path(repo) | |
| try: | |
| doc = json.loads(p.read_text()) | |
| except (OSError, ValueError) as e: | |
| if p.exists(): | |
| log.warning("volumetric reference bank %s unreadable: %s", p, e) | |
| return None | |
| return doc if isinstance(doc, dict) else None | |
| def auto_volumetric_picks(repo: CaseRepository, label: str, per_label: int = MAX_EXAMPLES) -> list[dict[str, Any]]: | |
| """Deterministic fallback: bench volumetric cases carrying `label`, sorted by case id.""" | |
| out = [] | |
| for c in sorted(repo.by_split(REFERENCE_SPLIT), key=lambda c: c.case_id): | |
| if c.volume is None or not qa_ok(c.qa_flags): | |
| continue | |
| f = next((f for f in c.findings if f.label == label), None) | |
| if f is not None: | |
| out.append({"case_id": c.case_id, "finding_id": f.finding_id}) | |
| if len(out) >= per_label: | |
| break | |
| return out | |
| def build_volumetric_bank(repo: CaseRepository, per_label: int = MAX_EXAMPLES) -> dict[str, Any]: | |
| """{"version": 1, "split": "bench", "labels": {label: [{case_id, finding_id}]}} for every volumetric label.""" | |
| return { | |
| "version": 1, | |
| "split": REFERENCE_SPLIT, | |
| "labels": {lab: auto_volumetric_picks(repo, lab, per_label) for lab in VOLUME_LABELS}, | |
| } | |
| def write_volumetric_bank(repo: CaseRepository, path: Path | None = None) -> Path: | |
| p = path or volumetric_bank_path(repo) | |
| p.write_text(json.dumps(build_volumetric_bank(repo), indent=2) + "\n") | |
| return p | |
| def mask_url(case_id: str) -> str: | |
| return f"{get_settings().api_prefix}/reference/{case_id}/maskvol" | |
| def _volume_example(repo: CaseRepository, label: str, entry: dict[str, Any]) -> dict[str, Any] | None: | |
| c = _usable_case(repo, entry.get("case_id")) | |
| fid = entry.get("finding_id") | |
| if c is None or c.volume is None or not isinstance(fid, str): | |
| return None | |
| f = repo.finding(c, fid) | |
| if f is None or f.label != label: | |
| log.warning("reference: %s is not a %s finding on %s; skipped", fid, label, c.case_id) | |
| return None | |
| p = repo.volume_path(c) | |
| if p is None or not p.exists(): | |
| return None | |
| return { | |
| "case_id": c.case_id, | |
| "modality": c.modality, | |
| "body_region": c.body_region, | |
| "volume_url": volume_url(c.case_id), | |
| "mask_url": mask_url(c.case_id), | |
| "shape": [int(n) for n in c.volume.shape], | |
| "spacing": [float(x) for x in c.volume.spacing], | |
| "window": c.volume.window, | |
| "sequence": c.volume.sequence, | |
| "labels": dict(c.volume.labels), | |
| "measure_slice": f.measure.slice if f.measure else None, | |
| "provenance": provenance_badge(c), | |
| "finding": { | |
| "finding_id": f.short_id, | |
| "bbox": list(f.geometry.bbox), | |
| "slice_range": list(f.slice_range) if f.slice_range else None, | |
| "centroid3": list(f.centroid3) if f.centroid3 else None, | |
| "label_values": list(f.label_values) if f.label_values else None, | |
| "components": f.components, | |
| "measure": f.measure, | |
| "relative_location": f.relative_location, | |
| "side": f.side, | |
| }, | |
| } | |
| def volume_examples(repo: CaseRepository, label: str) -> list[dict[str, Any]]: | |
| if label not in VOLUME_LABELS: | |
| return [] | |
| bank = load_volumetric_bank(repo) | |
| listed = _entries((bank.get("labels") or {}).get(label)) if bank else auto_volumetric_picks(repo, label) | |
| out: list[dict[str, Any]] = [] | |
| seen: set[str] = set() | |
| for e in listed: | |
| ex = _volume_example(repo, label, e) | |
| if ex is None or ex["case_id"] in seen: | |
| continue | |
| seen.add(ex["case_id"]) | |
| out.append(ex) | |
| if len(out) >= MAX_EXAMPLES: | |
| break | |
| return out | |
| def reference_maskvol(case_id: str, repo: CaseRepository | None = None) -> bytes | None: | |
| """Gzipped label volume of a BENCH volumetric case (reference examples only); None otherwise.""" | |
| repo = repo or get_repo() | |
| c = _usable_case(repo, case_id) | |
| p = repo.maskvol_path(c) if c and c.volume else None | |
| return p.read_bytes() if p and p.exists() else None | |
| def _cards() -> dict[str, TeachingCard]: | |
| try: | |
| from backend.app.tutor.cards import load_cards | |
| d = config.cards_dir() | |
| return load_cards(d) if d.exists() else {} | |
| except Exception: # noqa: BLE001 — tutor module missing or a broken card: fall back to the bridge loader | |
| log.exception("reference: tutor.cards.load_cards failed") | |
| try: | |
| return tutor_bridge.load_cards() | |
| except Exception: # noqa: BLE001 | |
| return {} | |
| def label_ids() -> list[str]: | |
| return list(config.labels()) | |
| def _listed(bank: dict[str, Any]) -> dict[str, Any]: | |
| labels = bank.get("labels") | |
| return labels if isinstance(labels, dict) else {} | |
| def reference_label(label: str, repo: CaseRepository | None = None) -> dict[str, Any] | None: | |
| """One `labels[]` entry, or None for a label outside the taxonomy.""" | |
| if label not in config.labels(): | |
| return None | |
| return _label_entry(repo or get_repo(), label, _cards().get(label), _entries(_listed(load_bank()).get(label))) | |
| def reference(repo: CaseRepository | None = None) -> dict[str, Any]: | |
| """{labels: [...13 in taxonomy order...], normal_examples: [...]}.""" | |
| repo = repo or get_repo() | |
| bank = load_bank() | |
| listed = _listed(bank) | |
| cards = _cards() | |
| labels = [_label_entry(repo, lab, cards.get(lab), _entries(listed.get(lab))) for lab in label_ids()] | |
| normals: list[dict[str, Any]] = [] | |
| for e in _entries(bank.get("normal")): | |
| c = _usable_case(repo, e.get("case_id")) | |
| if c is None or not c.is_normal or c.findings or any(n["case_id"] == c.case_id for n in normals): | |
| continue | |
| normals.append(_shape(c)) | |
| if len(normals) >= MAX_EXAMPLES: | |
| break | |
| return {"labels": labels, "normal_examples": normals} | |
| if ( | |
| __name__ == "__main__" | |
| ): # uv run python -m backend.app.reference [--processed DIR] → writes reference_bank_volumetric.json | |
| import argparse | |
| ap = argparse.ArgumentParser( | |
| description="Write <processed>/reference_bank_volumetric.json from the bench CT/MR cases." | |
| ) | |
| ap.add_argument("--processed", type=Path, default=None) | |
| a = ap.parse_args() | |
| _repo = CaseRepository(a.processed) if a.processed else get_repo() | |
| _out = write_volumetric_bank(_repo) | |
| print(f"wrote {_out}: " + json.dumps({k: len(v) for k, v in json.loads(_out.read_text())["labels"].items()})) | |