{"cells":[{"cell_type":"markdown","id":"2692be6f","metadata":{},"source":"# RSNA 2026 Knee — Geometry to 6 Slots\n\n> After this notebook you should know **where this contest is actually hard, and why**, and how a pipeline that can reach LB 0.90+ is derived one step at a time. Every number here is measured on this competition, not folklore. The arithmetic is the production pipeline (`src/knee/`), inlined so the notebook runs standalone on Kaggle.\n\nThis is a **data-engineering starter**, not a model zoo. The figures are live: one sample test knee, with the same anatomical callouts the production write-up uses.\n"},{"cell_type":"code","execution_count":1,"id":"82fa7329","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:33.830791Z","iopub.status.busy":"2026-08-23T11:09:33.83005Z","iopub.status.idle":"2026-08-23T11:09:34.304983Z","shell.execute_reply":"2026-08-23T11:09:34.304751Z"}},"outputs":[],"source":"from pathlib import Path\nfrom IPython.display import display\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import Circle, Rectangle\nimport pydicom\n\n%matplotlib inline\n\nCAND = [\n    Path(\"/kaggle/input/rsna-knee-abnormality-detection\"),\n    Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\"),\n    Path(\"..\") / \"data\",\n    Path(\"data\"),\n    Path(\"/home/lian/notebooks/RSNA26/data\"),\n]\nDATA = next(p.resolve() for p in CAND if (p / \"train.csv\").exists())\n\nSTUDY = \"1.2.826.0.1.3680043.8.498.10047035057544427318018579121635276191\"\nSTUDY_DIR = next(p for p in [DATA / \"test_series\" / STUDY, DATA / \"train_series\" / STUDY] if p.exists())\n\nLABELS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\",\n]\nSLOTS = [\"SAG_FS\", \"COR_FS\", \"AX_FS\", \"SAG_T1\", \"COR_T1\", \"SAG_NOFS\"]\nSLOT_MAP = {  # (Anatomical_Plane, cls) → slot; no match → DROP\n    (\"Sagittal\", \"FS\"): \"SAG_FS\",\n    (\"Coronal\",  \"FS\"): \"COR_FS\",\n    (\"Axial\",    \"FS\"): \"AX_FS\",\n    (\"Sagittal\", \"T1\"): \"SAG_T1\",\n    (\"Coronal\",  \"T1\"): \"COR_T1\",\n    (\"Sagittal\", \"NOFS_FLUID\"): \"SAG_NOFS\",\n}\nBANDS = {\"Sagittal\": (0.08, 0.92), \"Coronal\": (0.22, 0.78), \"Axial\": (0.10, 0.90)}\nCROP_MM = 130.0\n\nplt.rcParams.update({\n    \"axes.unicode_minus\": False,\n    \"figure.dpi\": 120,\n    \"figure.facecolor\": \"white\",\n    \"axes.facecolor\": \"white\",\n    \"axes.grid\": False,\n    \"font.size\": 11,\n})\nGREEN, RED, ORANGE, BLUE, GOLD = \"#199e70\", \"#c23b3b\", \"#d95926\", \"#2b6cb0\", \"#b8860b\"\n\nprint(\"DATA     \", DATA)\nprint(\"STUDY_DIR\", STUDY_DIR)\nprint(\"on disk  \", sorted(p.name[-12:] for p in STUDY_DIR.iterdir() if p.is_dir()))\n"},{"cell_type":"code","execution_count":2,"id":"29d2ddce","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.306824Z","iopub.status.busy":"2026-08-23T11:09:34.306688Z","iopub.status.idle":"2026-08-23T11:09:34.314101Z","shell.execute_reply":"2026-08-23T11:09:34.313879Z"}},"outputs":[],"source":"# --- helpers: same arithmetic as production dicom_io.py / knee/slots.py ---------\n\ndef classify_weighting(tr, te, desc=\"\"):\n    # Copy of src/knee/slots.py::classify_weighting.\n    if tr > 0:\n        if tr < 800:\n            return \"T1\"\n        return \"T2\" if te > 60 else \"PD\"\n    d = (desc or \"\").lower()\n    if d and d != \"dummyseriesdesc!\":\n        if \"t1\" in d: return \"T1\"\n        if \"t2\" in d or \"stir\" in d: return \"T2\"\n        if \"pd\" in d: return \"PD\"\n    return \"UNK\"\n\n\ndef series_cls(flag, weighting):\n    if flag == 1:\n        return \"FS\"\n    if weighting == \"T1\":\n        return \"T1\"\n    if weighting in (\"PD\", \"T2\"):\n        return \"NOFS_FLUID\"\n    return \"OTHER\"\n\n\ndef window(img, lo=1.0, hi=99.5):\n    a, b = np.percentile(img, [lo, hi])\n    return np.clip((img.astype(np.float32) - a) / (b - a + 1e-6), 0, 1)\n\n\ndef show(ax, img, border=None, title=None):\n    ax.imshow(img, cmap=\"gray\", origin=\"upper\")\n    ax.set_xticks([]); ax.set_yticks([])\n    for sp in ax.spines.values():\n        if border is None:\n            sp.set_visible(False)\n        else:\n            sp.set_color(border); sp.set_linewidth(2.0)\n    if title:\n        ax.set_title(title, fontsize=11, pad=6)\n\n\ndef callout(ax, xy_top, text, color, offset, ha=\"left\", va=\"center\"):\n    # xy_top = (x, y) in image fraction, y measured from the TOP\n    xy = (xy_top[0], 1.0 - xy_top[1])\n    txt = (xy[0] + offset[0], xy[1] + offset[1])\n    ax.annotate(\n        text, xy=xy, xycoords=\"axes fraction\",\n        xytext=txt, textcoords=\"axes fraction\",\n        color=\"#1a1a19\", ha=ha, va=va, zorder=10, fontsize=10,\n        arrowprops=dict(arrowstyle=\"-|>\", color=color, lw=1.35, mutation_scale=9),\n        bbox=dict(boxstyle=\"round,pad=0.28\", fc=\"white\", ec=color, lw=1.15, alpha=0.92),\n    )\n\n\ndef scale_bar(ax, ps_mm, mm=50):\n    px = mm / ps_mm\n    x0, x1 = ax.get_xlim()\n    y0, y1 = ax.get_ylim()\n    xmin, xmax = min(x0, x1), max(x0, x1)\n    ymin, ymax = min(y0, y1), max(y0, y1)\n    pad = 0.04 * (xmax - xmin)\n    x = xmax - pad - px\n    y = ymax - pad\n    ax.plot([x, x + px], [y, y], color=\"white\", lw=2.0, solid_capstyle=\"butt\", zorder=8)\n    ax.text(x + px / 2, y + 0.018 * (ymax - ymin), f\"{mm} mm\",\n            color=\"white\", ha=\"center\", va=\"top\", fontsize=8, zorder=8,\n            bbox=dict(facecolor=\"#1a1a19\", edgecolor=\"none\", pad=1.4, alpha=0.7))\n\n\ndef load_series(uid):\n    # Filename-order read, geometry-order stack. Sort key = IPP · (row × col).\n    files = sorted((STUDY_DIR / uid).glob(\"*.dcm\"))\n    slices, keys, insts, ipps = [], [], [], []\n    iop = None\n    for f in files:\n        ds = pydicom.dcmread(f)\n        arr = ds.pixel_array.astype(np.float32)\n        iop = np.asarray([float(x) for x in ds.ImageOrientationPatient], dtype=np.float64)\n        ipp = np.asarray([float(x) for x in ds.ImagePositionPatient], dtype=np.float64)\n        nvec = np.cross(iop[:3], iop[3:])\n        k = float(np.dot(ipp, nvec))\n        slices.append(arr); keys.append(k); insts.append(int(ds.InstanceNumber)); ipps.append(ipp)\n    keys, insts, ipps = map(np.asarray, (keys, insts, ipps))\n    order = np.argsort(keys)\n    ds0 = pydicom.dcmread(files[int(order[len(order) // 2])], stop_before_pixels=True)\n    ps = np.asarray(ds0.PixelSpacing, dtype=float)\n    return dict(\n        uid=uid, files=files,\n        vol=np.stack([slices[i] for i in order]),\n        vol_file=np.stack(slices),\n        keys=keys, insts=insts, ipps=ipps, iop=iop, order=order, nvec=np.cross(iop[:3], iop[3:]),\n        desc=str(getattr(ds0, \"SeriesDescription\", \"\")),\n        tr=float(getattr(ds0, \"RepetitionTime\", -1) or -1),\n        te=float(getattr(ds0, \"EchoTime\", -1) or -1),\n        ps=float(ps[0]), ps_row=float(ps[0]), ps_col=float(ps[1]),\n        rows=int(ds0.Rows), cols=int(ds0.Columns), n=len(slices),\n        sbs=float(getattr(ds0, \"SpacingBetweenSlices\", np.nan) or np.nan),\n        laterality=str(getattr(ds0, \"Laterality\", \"\") or \"\"),\n        photometric=str(getattr(ds0, \"PhotometricInterpretation\", \"\")),\n    )\n\n\nprint(\"helpers ready\")\n"},{"cell_type":"markdown","id":"80bf8cac","metadata":{},"source":"## 1. The task is not a normal classification contest\n\nOn paper: score a knee MRI **study** with 12 binary labels (ACL, MCL, medial/lateral meniscus, three-compartment OA, effusion, synovitis, Baker's cyst, contusion, fracture). Metric: **macro ROC AUC** over those 12 classes.\n\nWhat actually defines the contest is three structural facts:\n\n1. **Gold labels barely exist.** Of 4407 training studies, only **58** have expert labels (all 12 columns filled). The rest come with free-text reports in ~12 languages. The test set has no reports at all. Statistical power on 58: per-class AUC SE ≈ 0.07–0.09; bootstrap 95% CI on macro AUC is ~0.079 wide. **Tuning on the 58 is rolling dice.**\n2. **The pixels are raw DICOM — assume nothing.** Filenames are SOP UIDs, uncorrelated with anatomy. ~14% of series have their description wiped (`DummySeriesDesc!`). About half the series lack a laterality tag. The two official contrast flags are redundant: `Fluid_Sensitive` and `Fat_Suppression` are the *same column*.\n3. **Code competition + hidden test + efficiency track.** Inference has to finish on a Kaggle T4 in 9 hours. Keep a 0.5-everywhere fallback submission ready so a timeout does not zero you.\n\nOne sentence: this is a **weak-supervision + hard data-engineering** contest. Architecture novelty is second.\n\n## 2. Metric thinking: what macro AUC actually rewards\n\n- AUC only cares about **rank** → calibration is irrelevant, but ensembles must use **rank-mean** (member logits live on different scales).\n- Macro average → all 12 labels weigh the same, even though they are wildly different to learn: effusion / synovitis are large-area signals; contusion / fracture are focal and need a target-aware inference strategy.\n- The 58 gold studies cannot rank or tune anything → you need a **derived validation set** (high-confidence pseudo-label subset) as the primary selection signal.\n"},{"cell_type":"code","execution_count":3,"id":"5289d4cb","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.315265Z","iopub.status.busy":"2026-08-23T11:09:34.315099Z","iopub.status.idle":"2026-08-23T11:09:34.484688Z","shell.execute_reply":"2026-08-23T11:09:34.484441Z"}},"outputs":[],"source":"train = pd.read_csv(DATA / \"train.csv\")\nts = pd.read_csv(DATA / \"train_series.csv\")\ngold = train[train[LABELS].notna().all(axis=1)]\n\nprint(f\"train studies: {len(train):,}    gold (12-col complete): {len(gold)}\")\nprint(f\"train series : {len(ts):,}    series/study median {ts.groupby('StudyInstanceUID').size().median():.0f}  (min {ts.groupby('StudyInstanceUID').size().min()}, max {ts.groupby('StudyInstanceUID').size().max()})\")\nprint(f\"reports missing: {train['Report'].isna().sum()}\")\n\nprev = gold[LABELS].mean().sort_values()\nfig, ax = plt.subplots(figsize=(8.2, 4.2))\nax.barh(prev.index, prev.values * 100, color=BLUE)\nfor i, v in enumerate(prev.values):\n    ax.text(v * 100 + 0.6, i, f\"{v*100:.0f}%  n={int(round(v*len(gold)))}\", va=\"center\", fontsize=9)\nax.set_xlabel(\"prevalence in the 58 gold studies (%)\")\nax.set_title(\"12-class prevalence on 58 gold studies — macro-AUC, so MCL and effusion weigh the same\")\nax.set_xlim(0, 80)\nplt.tight_layout(); plt.show()\n"},{"cell_type":"markdown","id":"5b337f0c","metadata":{},"source":"### The series table is the modeling key\n\n`train_series.csv` is one row per series, five columns. **Do not read contrast off `SeriesDescription`** (~14% is `DummySeriesDesc!`). The official flag is already there.\n"},{"cell_type":"code","execution_count":4,"id":"11b858f1","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.486098Z","iopub.status.busy":"2026-08-23T11:09:34.485972Z","iopub.status.idle":"2026-08-23T11:09:34.598334Z","shell.execute_reply":"2026-08-23T11:09:34.597907Z"}},"outputs":[],"source":"print(\"columns:\", list(ts.columns))\nprint(\"Fluid_Sensitive == Fat_Suppression on ALL rows:\",\n      (ts.Fluid_Sensitive == ts.Fat_Suppression).all(),\n      f\"  ({len(ts):,} series)\")\n\nplanes = ts.groupby(\"StudyInstanceUID\")[\"Anatomical_Plane\"].nunique()\nprint(f\"studies with all 3 planes: {(planes == 3).mean():.3f}   (min planes/study={planes.min()})\")\n\n# Per-study (plane, FS=0/1) occupancy — CSV lower bound on slot fill.\n# T1 vs NOFS still needs TR, which the CSV does not have; the three FS slots are exact from this column.\nstudies = ts.StudyInstanceUID.unique()\nrows = []\nfor plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n    for fs, name in [(1, \"FS\"), (0, \"non-FS\")]:\n        hit = (ts[(ts.Anatomical_Plane == plane) & (ts.Fluid_Sensitive == fs)]\n               .groupby(\"StudyInstanceUID\").size()\n               .reindex(studies, fill_value=0).gt(0).mean())\n        rows.append((plane, name, hit))\n        print(f\"  {plane:10s} {name:6s} fill {hit:.3f}\")\n\nfill = pd.DataFrame(rows, columns=[\"plane\", \"contrast\", \"fill\"])\nfig, ax = plt.subplots(figsize=(7.2, 3.6))\npiv = fill.pivot(index=\"plane\", columns=\"contrast\", values=\"fill\").loc[[\"Sagittal\", \"Coronal\", \"Axial\"]]\npiv.plot.bar(ax=ax, color=[GREEN, ORANGE], rot=0)\nax.set_ylim(0, 1.05); ax.set_ylabel(\"fraction of studies with ≥1 series\")\nax.set_title(\"CSV-exact fill: AX_FS = 1.00; axial non-FS ≈ 0.20 — that is why there is no AX_NOFS slot\")\nax.legend(title=\"\")\nplt.tight_layout(); plt.show()\n\nhit = train.Report.str.contains(\"zonder en met fs\", case=False, na=False)\nprint(f\"\\nreports containing 'zonder en met fs': {hit.sum()}\")\nprint(train.loc[hit, \"Report\"].iloc[0][:420])\n"},{"cell_type":"markdown","id":"a4ac508c","metadata":{},"source":"## 3. Open one knee\n\nA sample-test left knee, Siemens 1.5T. CSV lists 5 series. Whatever is on disk we load; a missing series (common in a local stub download) is still a CSV row we can slot. Every figure below is this study.\n"},{"cell_type":"code","execution_count":5,"id":"637fcb72","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.599805Z","iopub.status.busy":"2026-08-23T11:09:34.599538Z","iopub.status.idle":"2026-08-23T11:09:34.841948Z","shell.execute_reply":"2026-08-23T11:09:34.841627Z"}},"outputs":[],"source":"meta = pd.read_csv(DATA / \"test_series.csv\")\nmeta = meta[meta.StudyInstanceUID == STUDY].copy()\nprint(meta.to_string(index=False))\nprint()\n\nSERIES = {}\nfor uid in sorted(p.name for p in STUDY_DIR.iterdir() if p.is_dir()):\n    rec = load_series(uid)\n    row = meta[meta.SeriesInstanceUID == uid].iloc[0]\n    rec[\"plane\"] = row.Anatomical_Plane\n    rec[\"flag\"] = int(row.Fluid_Sensitive)\n    rec[\"weighting\"] = classify_weighting(rec[\"tr\"], rec[\"te\"], rec[\"desc\"])\n    rec[\"cls\"] = series_cls(rec[\"flag\"], rec[\"weighting\"])\n    rec[\"slot\"] = SLOT_MAP.get((rec[\"plane\"], rec[\"cls\"]), \"\")\n    rec[\"area\"] = rec[\"rows\"] * rec[\"cols\"]\n    SERIES[uid] = rec\n    print(f\"{rec['desc']:16s}  {rec['plane']:9s}  flag={rec['flag']}  TR={rec['tr']:.0f} TE={rec['te']:.1f}  \"\n          f\"{rec['weighting']:3s} → {rec['cls']:11s}  slot={rec['slot'] or 'DROP':8s}  \"\n          f\"n={rec['n']:2d}  {rec['rows']}²  lat={rec['laterality'] or '-'}\")\n\nprint(\"\\nCSV series with no pixels on disk:\")\non_disk = set(SERIES)\nmissing = meta.loc[~meta.SeriesInstanceUID.isin(on_disk),\n                   [\"SeriesInstanceUID\", \"Anatomical_Plane\", \"Fluid_Sensitive\"]]\nprint(\"(none)\" if missing.empty else missing.to_string(index=False))\n"},{"cell_type":"markdown","id":"49ea252a","metadata":{},"source":"## 4. Geometry sort: `n = r × c`, `k = IPP · n`\n\nFilenames are SOP UIDs. Lexicographic order has nothing to do with anatomy (train-wide Spearman ρ ≈ 0.13 vs. the geometric key).\n\nEvery DICOM carries two tags:\n\n| tag | name | meaning |\n|---|---|---|\n| (0020,0037) | ImageOrientationPatient | 6 numbers = row direction `r` (image right) + col direction `c` (image down), patient LPS |\n| (0020,0032) | ImagePositionPatient | (x, y, z) of the top-left pixel, millimetres |\n\nProduction sort is three lines:\n\n```python\nn = np.cross(iop[:3], iop[3:])   # r × c\nk = float(np.dot(ipp, n))        # IPP · n\norder = np.argsort(k)\n```\n\n(`build_cache.py` unit-normalizes `n`; the order does not change.) Plug the numbers in on this exam's sagittal PD.\n"},{"cell_type":"code","execution_count":6,"id":"cfc6738e","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.843168Z","iopub.status.busy":"2026-08-23T11:09:34.843032Z","iopub.status.idle":"2026-08-23T11:09:34.996013Z","shell.execute_reply":"2026-08-23T11:09:34.995648Z"}},"outputs":[],"source":"# The sagittal PD with the most pixels — the series that wins SAG_NOFS\nsag = max((r for r in SERIES.values() if r[\"desc\"].startswith(\"pd_tse_sag\")), key=lambda r: r[\"area\"])\nrvec, cvec, nvec = sag[\"iop\"][:3], sag[\"iop\"][3:], sag[\"nvec\"]\ni1 = int(np.argmin(np.abs(sag[\"insts\"] - 1)))\nipp1, k1 = sag[\"ipps\"][i1], sag[\"keys\"][i1]\nterms = ipp1 * nvec\ndk = float(np.median(np.diff(np.sort(sag[\"keys\"]))))\n\nprint(f\"{sag['desc']}   n={sag['n']}   IOP unique? yes (series-constant)\")\nprint(f\"r  (row, image right) = {np.round(rvec, 3)}\")\nprint(f\"c  (col, image down)  = {np.round(cvec, 3)}\")\nprint(f\"n  = r × c            = {np.round(nvec, 3)}     ||n||={np.linalg.norm(nvec):.6f}\")\nprint()\nprint(\"cross product expanded:\")\nprint(f\"  n_x = r_y c_z − r_z c_y = {rvec[1]:.3f}×({cvec[2]:.3f}) − {rvec[2]:.3f}×{cvec[1]:.3f} = {nvec[0]:.3f}\")\nprint(f\"  n_y = r_z c_x − r_x c_z = {rvec[2]:.3f}×{cvec[0]:.3f} − {rvec[0]:.3f}×({cvec[2]:.3f}) = {nvec[1]:.3f}\")\nprint(f\"  n_z = r_x c_y − r_y c_x = {rvec[0]:.3f}×{cvec[1]:.3f} − {rvec[1]:.3f}×{cvec[0]:.3f} = {nvec[2]:.3f}\")\nprint()\ndef _sumtxt(xs):\n    bits = []\n    for i, x in enumerate(xs):\n        ax = abs(float(x))\n        if i == 0:\n            bits.append(f\"−{ax:.2f}\" if x < 0 else f\"{ax:.2f}\")\n        else:\n            bits.append(f\"− {ax:.2f}\" if x < 0 else f\"+ {ax:.2f}\")\n    return \" \".join(bits)\n\nprint(f\"example InstanceNumber=1   IPP = {np.round(ipp1, 2)}\")\nprint(f\"  k = IPP · n = {ipp1[0]:.2f}×({nvec[0]:.3f}) + ({ipp1[1]:.2f})×{nvec[1]:.3f} + {ipp1[2]:.2f}×({nvec[2]:.3f})\")\nprint(f\"    = {_sumtxt(terms)} = {k1:.2f} mm\")\nprint(f\"adjacent-slice Δk = {dk:.2f} mm   SpacingBetweenSlices = {sag['sbs']:.1f} mm\")\nprint(\"||n||≈1, so k is in millimetres. LPS: +X left  +Y posterior  +Z head.\")\nprint(\"This series n≈−X, i.e. we cut from the patient's left toward the right.\")\n\nimg = window(sag[\"vol\"][12])\nh, w = img.shape\nfig, ax = plt.subplots(figsize=(5.6, 5.6))\nshow(ax, img, title=f\"{sag['desc']}  ·  geometry-order slice 13\")\nox, oy, L = 0.46 * w, 0.40 * h, 0.22 * w\nax.annotate(\"\", xy=(ox + L, oy), xytext=(ox, oy),\n            arrowprops=dict(arrowstyle=\"-|>\", color=ORANGE, lw=2.4, mutation_scale=14))\nax.annotate(\"\", xy=(ox, oy + L), xytext=(ox, oy),\n            arrowprops=dict(arrowstyle=\"-|>\", color=BLUE, lw=2.4, mutation_scale=14))\nrad = 0.028 * w\nax.add_patch(Circle((ox, oy), rad, fill=False, ec=GREEN, lw=2.0))\ns = rad * 0.62\nax.plot([ox - s, ox + s], [oy - s, oy + s], color=GREEN, lw=2)\nax.plot([ox - s, ox + s], [oy + s, oy - s], color=GREEN, lw=2)\nax.text(ox + L + 8, oy, \"r  row dir\\nimage right ≈ +Y\", color=ORANGE, va=\"center\", fontsize=10,\n        bbox=dict(fc=\"white\", ec=ORANGE, pad=3, alpha=0.9))\nax.text(ox + 8, oy + L + 8, \"c  col dir\\nimage down ≈ −Z\", color=BLUE, va=\"top\", fontsize=10,\n        bbox=dict(fc=\"white\", ec=BLUE, pad=3, alpha=0.9))\nax.annotate(\"n into page ≈ −X\", xy=(ox - rad, oy), xytext=(0.06 * w, oy),\n            color=GREEN, fontsize=10, va=\"center\",\n            arrowprops=dict(arrowstyle=\"-|>\", color=GREEN, lw=1.3),\n            bbox=dict(fc=\"white\", ec=GREEN, pad=3, alpha=0.9))\nax.text(0.03, 0.04, \"right-hand rule: fingers r→c, thumb points along n (into the page)\",\n        transform=ax.transAxes, fontsize=9, color=\"#555\", va=\"bottom\")\nscale_bar(ax, sag[\"ps\"])\nplt.tight_layout(); plt.show()\n"},{"cell_type":"code","execution_count":7,"id":"649b760d","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:34.998731Z","iopub.status.busy":"2026-08-23T11:09:34.998629Z","iopub.status.idle":"2026-08-23T11:09:35.869278Z","shell.execute_reply":"2026-08-23T11:09:35.868869Z"}},"outputs":[],"source":"# Filename order vs k order. ρ computed live.\nrho_file = pd.Series(np.arange(sag[\"n\"])).corr(pd.Series(sag[\"keys\"]), method=\"spearman\")\nrho_inst = pd.Series(sag[\"insts\"]).corr(pd.Series(sag[\"keys\"]), method=\"spearman\")\ngeo_rank = np.empty(sag[\"n\"], dtype=int)\ngeo_rank[sag[\"order\"]] = np.arange(sag[\"n\"])\nadj = np.abs(geo_rank[1:] - geo_rank[:-1])\nprint(f\"ρ(filename, k) = {rho_file:.2f}    ρ(InstanceNumber, k) = {rho_inst:.2f}\")\nprint(f\"geometric distance of filename-adjacent files: median {np.median(adj):.0f} slices, max {adj.max()} slices\")\n\nnshow = 8\nfig, axes = plt.subplots(2, nshow, figsize=(14.0, 4.4))\nfor i in range(nshow):\n    show(axes[0, i], window(sag[\"vol_file\"][i]), border=RED)\n    axes[0, i].set_xlabel(f\"inst {int(sag['insts'][i])}\\nk={sag['keys'][i]:.0f}\", color=RED, fontsize=8)\n    gi = int(sag[\"order\"][i])\n    show(axes[1, i], window(sag[\"vol\"][i]), border=GREEN)\n    axes[1, i].set_xlabel(f\"inst {int(sag['insts'][gi])}\\nk={sag['keys'][gi]:.0f}\", color=GREEN, fontsize=8)\naxes[0, 0].set_ylabel(\"filename order\\nSOP UID\", color=RED, fontsize=10)\naxes[1, 0].set_ylabel(\"geometry order  k↑\", color=GREEN, fontsize=10)\nfig.suptitle(\"Sort by filename: anatomy jumps. Sort by k: subcutaneous fat → joint, one millimetre at a time\",\n             fontsize=13, y=1.02)\nplt.tight_layout(); plt.show()\n\nprint(\"Production dicom_io.py uses the same three lines (n = r×c, k = IPP·n, argsort).\")\nprint(\"On this series InstanceNumber is exactly reversed vs k — just a flip.\")\nprint(\"Interleaved acquisition writes odds then evens, so InstanceNumber walks the knee twice.\")\nprint(\"That is why production trusts k, and falls back to InstanceNumber only if IOP/IPP are missing.\")\n"},{"cell_type":"markdown","id":"71084170","metadata":{},"source":"## 5. Two orthogonal axes: how the knife cuts × which tissue is bright\n\n`SAG / COR / AX` is how the knife cuts. `FS / T1 / NOFS` is which tissue is bright. **Do not mix them.** Plane comes from the official `Anatomical_Plane` column (every study has all three).\n"},{"cell_type":"code","execution_count":8,"id":"af12e855","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:35.872661Z","iopub.status.busy":"2026-08-23T11:09:35.872475Z","iopub.status.idle":"2026-08-23T11:09:36.154617Z","shell.execute_reply":"2026-08-23T11:09:36.153898Z"}},"outputs":[],"source":"# Mid-slices of three planes. Callout coordinates match the teaching posters.\ncor = next(r for r in SERIES.values() if r[\"plane\"] == \"Coronal\")\naxl = next(r for r in SERIES.values() if r[\"plane\"] == \"Axial\")\nci = cor[\"n\"] // 2\nai = min(15, axl[\"n\"] - 1)\nfig, axes = plt.subplots(1, 3, figsize=(13.6, 5.2))\nfig.suptitle(\"Same knee, three planes — what you see is anatomy, not brightness\", fontsize=13)\n\nshow(axes[0], window(sag[\"vol\"][12]), title=\"Sagittal  ·  lateral view\")\ncallout(axes[0], (0.13, 0.20), \"patella\", BLUE, (0.12, 0.14))\ncallout(axes[0], (0.42, 0.41), \"ACL\", BLUE, (0.04, 0.16))\ncallout(axes[0], (0.56, 0.49), \"posterior horn\", ORANGE, (0.10, -0.16))\ncallout(axes[0], (0.42, 0.62), \"tibia\", BLUE, (0.18, -0.10))\nscale_bar(axes[0], sag[\"ps\"])\n\nshow(axes[1], window(cor[\"vol\"][ci]), title=\"Coronal  ·  AP view\")\ncallout(axes[1], (0.48, 0.38), \"joint effusion\", ORANGE, (0.16, 0.18))\ncallout(axes[1], (0.68, 0.42), \"collateral lig.\", BLUE, (0.08, -0.18))\ncallout(axes[1], (0.32, 0.22), \"femoral condyle\", BLUE, (-0.18, 0.12), ha=\"right\")\nscale_bar(axes[1], cor[\"ps\"])\n\nshow(axes[2], window(axl[\"vol\"][ai]), title=\"Axial  ·  transaxial\")\ncallout(axes[2], (0.48, 0.18), \"patella\", BLUE, (0.18, 0.10))\ncallout(axes[2], (0.45, 0.40), \"condylar cartilage\", BLUE, (-0.28, 0.08), ha=\"right\")\ncallout(axes[2], (0.50, 0.78), \"posterior (Baker region)\", ORANGE, (0.05, -0.12))\nscale_bar(axes[2], axl[\"ps\"])\nplt.tight_layout(); plt.show()\n"},{"cell_type":"markdown","id":"7526f0cc","metadata":{},"source":"### Contrast: flag + TR/TE, never the filename\n\n```python\nif Fluid_Sensitive == 1:                 # identical to Fat_Suppression\n    cls = FS\nelif TR < 800:\n    cls = T1\nelif weighting in {PD, T2}:              # TR≥800; TE>60 → T2 else PD\n    cls = NOFS_FLUID\n```\n\nTissue brightness under the three filters is the physical reason slots exist:\n\n| tissue | T1 (TR<800) | NOFS (PD/T2) | FS (flag=1) |\n|---|---|---|---|\n| fat / yellow marrow | bright | bright | **dark** |\n| fluid / edema | dark | medium (T2 brighter) | **bright** |\n| meniscus | dark | dark | dark (bright only if torn and filled) |\n"},{"cell_type":"code","execution_count":9,"id":"2c9503e8","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:36.15717Z","iopub.status.busy":"2026-08-23T11:09:36.157077Z","iopub.status.idle":"2026-08-23T11:09:36.437932Z","shell.execute_reply":"2026-08-23T11:09:36.437395Z"}},"outputs":[],"source":"# Same tissues, different knife: non-FS PD sagittal vs FS PD coronal\nfig, axes = plt.subplots(1, 2, figsize=(11.4, 6.0))\nshow(axes[0], window(sag[\"vol\"][8]), border=ORANGE,\n     title=f\"non-FS PD sagittal  flag=0  TR={sag['tr']:.0f} TE={sag['te']:.1f}\")\ncallout(axes[0], (0.10, 0.50), \"subcutaneous fat bright\", ORANGE, (0.12, 0.22))\ncallout(axes[0], (0.22, 0.48), \"Hoffa's fat pad bright\", ORANGE, (0.18, -0.22))\ncallout(axes[0], (0.58, 0.50), \"meniscus still dark\", BLUE, (0.08, 0.16))\nscale_bar(axes[0], sag[\"ps\"])\nshow(axes[1], window(cor[\"vol\"][ci]), border=RED,\n     title=f\"FS PD coronal       flag=1  TR={cor['tr']:.0f} TE={cor['te']:.0f}\")\ncallout(axes[1], (0.13, 0.50), \"subcutaneous fat suppressed\", RED, (-0.04, 0.28))\ncallout(axes[1], (0.48, 0.38), \"fluid now brightest\", ORANGE, (0.12, 0.20))\ncallout(axes[1], (0.38, 0.20), \"marrow also dark\", BLUE, (0.14, 0.12))\nscale_bar(axes[1], cor[\"ps\"])\nfig.suptitle(\"The filter is fat: non-FS fat is bright; FS kills fat and water becomes the brightest thing (Fluid_Sensitive=1)\",\n             fontsize=12)\nplt.tight_layout(); plt.show()\n\nprint(\"This exam's TR is 3000–5000 ms throughout — no T1. T1 looks like: fat bright, water dark.\")\nprint(\"A 300-series audit: flag=1 never co-occurs with TR<800, so the flag is a clean fat-sat marker.\")\n"},{"cell_type":"markdown","id":"9f70309e","metadata":{},"source":"## 6. Six parking spots: `(plane, cls)` → slot, else DROP\n\n| slot | cut × filter | rule | mainly sees |\n|---|---|---|---|\n| SAG_FS / COR_FS / AX_FS | fat-sat, three planes | `flag=1` | edema, effusion, bone bruise |\n| SAG_T1 / COR_T1 | sag/cor T1 | `flag=0` and TR<800 | anatomy, fracture lines, OA osteophytes |\n| SAG_NOFS | sagittal non-FS PD/T2 | `flag=0` and PD/T2 | meniscal tear, ACL fibers |\n\nNo AX_T1 / COR_NOFS / AX_NOFS: fill is too low (axial non-FS ≈ 0.19). Opening that slot teaches the model noise on four studies out of five.\n\nSame-slot ties: `n_slices` first, then `rows×cols`. **Never backfill across contrast.** Missing slots get a presence mask of 0.\n"},{"cell_type":"code","execution_count":10,"id":"12d40ab8","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:36.441208Z","iopub.status.busy":"2026-08-23T11:09:36.441105Z","iopub.status.idle":"2026-08-23T11:09:36.452316Z","shell.execute_reply":"2026-08-23T11:09:36.452076Z"}},"outputs":[],"source":"# Full sort of this study (including CSV rows whose pixels were not downloaded)\nrows = []\nfor _, row in meta.iterrows():\n    uid = row.SeriesInstanceUID\n    if uid in SERIES:\n        rec = SERIES[uid]\n        d = dict(desc=rec[\"desc\"], plane=rec[\"plane\"], flag=rec[\"flag\"],\n                 tr=rec[\"tr\"], te=rec[\"te\"], weighting=rec[\"weighting\"],\n                 cls=rec[\"cls\"], slot=rec[\"slot\"] or \"DROP\",\n                 n=rec[\"n\"], rows=rec[\"rows\"], cols=rec[\"cols\"], area=rec[\"area\"],\n                 on_disk=True)\n    else:\n        d = dict(desc=\"(pixels not on disk)\", plane=row.Anatomical_Plane,\n                 flag=int(row.Fluid_Sensitive), tr=np.nan, te=np.nan,\n                 weighting=\"?\", cls=(\"FS\" if row.Fluid_Sensitive == 1 else \"?\"),\n                 slot=SLOT_MAP.get((row.Anatomical_Plane, \"FS\" if row.Fluid_Sensitive == 1 else \"\"), \"DROP\"),\n                 n=np.nan, rows=np.nan, cols=np.nan, area=np.nan, on_disk=False)\n    rows.append(d)\ninfo = pd.DataFrame(rows).sort_values([\"slot\", \"n\", \"area\"], ascending=[True, False, False])\ndisplay(info[[\"desc\", \"plane\", \"flag\", \"tr\", \"te\", \"weighting\", \"cls\", \"slot\", \"n\", \"rows\", \"cols\", \"on_disk\"]])\n\ncands = info[info.slot == \"SAG_NOFS\"]\nprint(\"\\nSAG_NOFS candidates (n_slices first, then area):\")\nprint(cands[[\"desc\", \"n\", \"rows\", \"cols\", \"area\"]].to_string(index=False))\nwinner = cands.sort_values([\"n\", \"area\"], ascending=False).iloc[0]\nprint(\"winner:\", winner.desc)\n"},{"cell_type":"code","execution_count":11,"id":"88f390ab","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:36.45348Z","iopub.status.busy":"2026-08-23T11:09:36.453353Z","iopub.status.idle":"2026-08-23T11:09:36.848436Z","shell.execute_reply":"2026-08-23T11:09:36.848116Z"}},"outputs":[],"source":"pd_sag = sag\nt2_sag = next(r for r in SERIES.values() if r[\"desc\"].startswith(\"t2_tse_sag\"))\nfig, axes = plt.subplots(1, 2, figsize=(11.4, 6.0))\nshow(axes[0], window(pd_sag[\"vol\"][12]), border=GREEN,\n     title=f\"{pd_sag['desc']}  →  wins SAG_NOFS\")\naxes[0].text(0.03, 0.04, f\"TR={pd_sag['tr']:.0f} TE={pd_sag['te']:.1f}  PD\\n{pd_sag['n']} slices  {pd_sag['rows']}×{pd_sag['cols']}\",\n             transform=axes[0].transAxes, va=\"bottom\", fontsize=9,\n             bbox=dict(fc=\"white\", ec=GREEN, pad=4, alpha=0.9))\ncallout(axes[0], (0.40, 0.42), \"ACL fibers (morphology)\", BLUE, (-0.22, 0.16), ha=\"right\")\nscale_bar(axes[0], pd_sag[\"ps\"])\nshow(axes[1], window(t2_sag[\"vol\"][12]), border=\"#888\",\n     title=f\"{t2_sag['desc']}  →  same slot, loses\")\naxes[1].text(0.03, 0.04, f\"TR={t2_sag['tr']:.0f} TE={t2_sag['te']:.0f}  T2\\n{t2_sag['n']} slices  {t2_sag['rows']}×{t2_sag['cols']}\\nfluid brighter, fewer pixels\",\n             transform=axes[1].transAxes, va=\"bottom\", fontsize=9,\n             bbox=dict(fc=\"white\", ec=\"#888\", pad=4, alpha=0.9))\ncallout(axes[1], (0.55, 0.48), \"T2 fluid brighter\", ORANGE, (0.10, 0.16))\nscale_bar(axes[1], t2_sag[\"ps\"])\nfig.suptitle(\"SAG_NOFS catch-all = sagittal, not fat-sat, not T1. PD and T2 compete for one parking spot\",\n             fontsize=12)\nplt.tight_layout(); plt.show()\n\n# Axial non-FS: pretty picture, no slot\nfig, ax = plt.subplots(figsize=(6.4, 6.4))\nshow(ax, window(axl[\"vol\"][ai]), border=RED,\n     title=f\"{axl['desc']}  ·  {axl['plane']}  flag=0  →  DROP\")\nax.text(0.50, 0.06, f\"cls = {axl['cls']}, but SLOT_MAP has no AX_NOFS\",\n        transform=ax.transAxes, ha=\"center\", fontsize=10,\n        bbox=dict(fc=\"white\", ec=RED, pad=5, alpha=0.92))\ncallout(ax, (0.12, 0.50), \"pretty fat ring, but not a slot\", ORANGE, (0.10, 0.22))\ncallout(ax, (0.48, 0.18), \"patella\", BLUE, (0.18, 0.10))\nscale_bar(ax, axl[\"ps\"])\nfig.suptitle(\"Pretty is not enough: if (plane, contrast) is not in the map, drop it\", fontsize=13)\nplt.tight_layout(); plt.show()\n"},{"cell_type":"code","execution_count":12,"id":"fb99f924","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:36.850634Z","iopub.status.busy":"2026-08-23T11:09:36.85049Z","iopub.status.idle":"2026-08-23T11:09:37.052111Z","shell.execute_reply":"2026-08-23T11:09:37.05186Z"}},"outputs":[],"source":"# Six parking spots. Empty → mask=0. Never fill with the wrong contrast.\nby_slot = {r[\"slot\"]: r for r in SERIES.values() if r[\"slot\"]}\nnotes = {\n    \"SAG_FS\":  \"this exam has no sagittal fat-sat\",\n    \"COR_FS\":  \"flag=1 coronal\",\n    \"AX_FS\":   \"flag=1 axial\",\n    \"SAG_T1\":  \"TR is 3000–5000 ms, no T1\",\n    \"COR_T1\":  \"no coronal T1\",\n    \"SAG_NOFS\": f\"{winner.desc} wins\",\n}\nlayout = []\nfor name in SLOTS:\n    rec = by_slot.get(name)\n    if rec is not None:\n        layout.append((name, rec, \"parked\", f\"{notes[name]}   n={rec['n']}\", GREEN))\n        continue\n    csv_only = False\n    if name.endswith(\"_FS\"):\n        plane = {\"SAG_FS\": \"Sagittal\", \"COR_FS\": \"Coronal\", \"AX_FS\": \"Axial\"}[name]\n        csv_only = bool(((meta.Anatomical_Plane == plane) & (meta.Fluid_Sensitive == 1)).any())\n    if csv_only:\n        layout.append((name, None, \"CSV has it · pixels not on disk\",\n                       \"inference would still fill this slot\", GOLD))\n    else:\n        layout.append((name, None, \"empty  ·  mask=0\", notes[name], RED))\n\nfig, axes = plt.subplots(2, 3, figsize=(12.6, 8.0))\nfig.suptitle(\"What the model actually eats: 6 slots. Empty → mask=0. Do not backfill with another contrast.\",\n             fontsize=13, y=0.98)\nfor ax, (name, rec, status, note, col) in zip(axes.ravel(), layout):\n    if rec is not None:\n        mid = rec[\"n\"] // 2\n        show(ax, window(rec[\"vol\"][mid]), border=col, title=name)\n        ax.text(0.03, 0.04, f\"{status}\\n{note}\", transform=ax.transAxes, va=\"bottom\", fontsize=8.5,\n                bbox=dict(fc=\"white\", ec=col, pad=3.5, alpha=0.9))\n    else:\n        ax.set_xticks([]); ax.set_yticks([]); ax.set_facecolor(\"#f4f4f2\")\n        for sp in ax.spines.values():\n            sp.set_color(col); sp.set_linewidth(2)\n        ax.text(0.5, 0.62, name, transform=ax.transAxes, ha=\"center\", fontsize=16, color=col, fontweight=\"bold\")\n        ax.text(0.5, 0.44, status, transform=ax.transAxes, ha=\"center\", fontsize=11, color=\"#666\")\n        ax.text(0.5, 0.30, note, transform=ax.transAxes, ha=\"center\", fontsize=9, color=\"#666\")\nplt.tight_layout(); plt.show()\nprint(\"The presence mask is threaded slice → slot → study. Filling FS with T1 tells the model 'a fracture line is called effusion'.\")\n"},{"cell_type":"markdown","id":"d8cbac14","metadata":{},"source":"### A slot is a stack, not a postcard\n\nInside a plane-specific band we sample K groups of 3 adjacent slices as 2.5D:\n\n- sagittal (0.08, 0.92)\n- coronal (0.22, 0.78)\n- axial (0.10, 0.90)\n\nEdges are almost all air. Train K = 6–8 groups, infer K = 12 sliding windows.\n"},{"cell_type":"code","execution_count":13,"id":"6c72e96a","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:37.054431Z","iopub.status.busy":"2026-08-23T11:09:37.054222Z","iopub.status.idle":"2026-08-23T11:09:37.369844Z","shell.execute_reply":"2026-08-23T11:09:37.369503Z"}},"outputs":[],"source":"idxs = list(range(9, 16))\nfig, axes = plt.subplots(1, len(idxs), figsize=(14.0, 3.2))\nfor ax, i in zip(axes, idxs):\n    show(ax, window(sag[\"vol\"][i]), border=GREEN if i in (11, 12, 13) else None)\n    ax.set_xlabel(str(i), fontsize=10, color=GREEN if i in (11, 12, 13) else \"#888\")\nfig.suptitle(\"Green box = one 2.5D input (center ±1). From 9 to 15 the notch opens and the ACL appears\",\n             fontsize=12, y=1.05)\nplt.tight_layout(); plt.show()\n\nlo, hi = BANDS[\"Sagittal\"]\na, b = int(np.floor(lo * (sag[\"n\"] - 1))), int(np.ceil(hi * (sag[\"n\"] - 1)))\nprint(f\"sagittal band {lo, hi} → slice index [{a}, {b}]  (n={sag['n']})\")\nprint(\"That is why you do not feed a single 'most-central' slice.\")\n"},{"cell_type":"markdown","id":"0936a166","metadata":{},"source":"Which slots each of the 12 labels actually eats is anatomy written into the input:\n\n| label | primary slots | why |\n|---|---|---|\n| ACL | SAG_FS + SAG_NOFS | only a side view sees the full length; FS for edema, NOFS for fiber continuity |\n| MCL | COR_FS + COR_T1 | MCL lives on the AP view |\n| meniscus | SAG_NOFS first | a bright line inside a dark triangle = tear |\n| OA | T1 + FS all planes | T1 for osteophytes / sclerosis, FS for subchondral edema |\n| effusion / synovitis | AX_FS + FS all planes | water is brightest on FS |\n| Baker's cyst | AX_FS | a posterior pouch; only axial sees it in one glance |\n| bone contusion | `*_FS` | once fat is black, marrow water lights up |\n| fracture | T1 + FS | T1 for cortical break, FS for the surrounding bruise |\n"},{"cell_type":"code","execution_count":14,"id":"11995c0f","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:37.371862Z","iopub.status.busy":"2026-08-23T11:09:37.371758Z","iopub.status.idle":"2026-08-23T11:09:37.591645Z","shell.execute_reply":"2026-08-23T11:09:37.591338Z"}},"outputs":[],"source":"fig, axes = plt.subplots(1, 2, figsize=(11.6, 6.1))\nshow(axes[0], window(sag[\"vol\"][8]), border=BLUE, title=\"SAG_NOFS  ·  meniscus + ACL\")\ncallout(axes[0], (0.32, 0.50), \"anterior horn\", BLUE, (-0.18, 0.12), ha=\"right\")\ncallout(axes[0], (0.58, 0.50), \"posterior horn  ← dark triangle\", BLUE, (0.06, 0.16))\ncallout(axes[0], (0.12, 0.18), \"patella / PF OA\", GOLD, (0.16, 0.10))\ncallout(axes[0], (0.22, 0.48), \"Hoffa's fat\", ORANGE, (0.02, -0.20))\nscale_bar(axes[0], sag[\"ps\"])\nshow(axes[1], window(cor[\"vol\"][ci]), border=ORANGE, title=\"COR_FS  ·  effusion + collateral + marrow\")\ncallout(axes[1], (0.48, 0.38), \"effusion (brightest on FS)\", ORANGE, (0.10, 0.20))\ncallout(axes[1], (0.68, 0.42), \"collateral / MCL corridor\", BLUE, (0.02, -0.18))\ncallout(axes[1], (0.32, 0.22), \"marrow (dark on FS; bright = bruise)\", RED, (-0.06, 0.16), ha=\"right\")\nscale_bar(axes[1], cor[\"ps\"])\nfig.suptitle(\"Slots encode anatomy: dark fibers for tears, bright water for fluid / edema\", fontsize=13)\nplt.tight_layout(); plt.show()\n"},{"cell_type":"markdown","id":"4b47c199","metadata":{},"source":"## 7. Laterality: flip everything to a left knee\n\nAbout half the series lack tag (0020,0060) Laterality. Infer from the image-center patient-x (+x = patient left; |x| < 20 mm → unknown). Agreement with the tagged subset: 0.997.\n\nThen normalize every right knee to left:\n\n- sagittal: reverse slice order\n- coronal / axial: horizontal flip\n\n**Augmentation red line: no flips.** A horizontal flip undoes laterality normalization. A vertical flip swaps femur/tibia and wrecks the OA labels. Allowed: ±8° rotation, ≤8% zoom, ≤5% shift, ±10% intensity.\n"},{"cell_type":"code","execution_count":15,"id":"be75c5dc","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:37.593853Z","iopub.status.busy":"2026-08-23T11:09:37.593729Z","iopub.status.idle":"2026-08-23T11:09:37.832602Z","shell.execute_reply":"2026-08-23T11:09:37.832371Z"}},"outputs":[],"source":"def center_x(rec, slice_i=None):\n    # Image-center patient-x. Copy of src/knee/slots.py.\n    i = rec[\"order\"][len(rec[\"order\"]) // 2] if slice_i is None else slice_i\n    ipp, iop = rec[\"ipps\"][i], rec[\"iop\"]\n    return (ipp[0]\n            + 0.5 * rec[\"rows\"] * rec[\"ps_row\"] * iop[0]\n            + 0.5 * rec[\"cols\"] * rec[\"ps_col\"] * iop[3])\n\nprint(f\"{'desc':16s}  tag  center_x(mm)  inferred\")\nxs = []\nfor rec in SERIES.values():\n    cx = center_x(rec)\n    xs.append(cx)\n    inferred = \"L\" if cx > 20 else (\"R\" if cx < -20 else \"U\")\n    print(f\"{rec['desc']:16s}  {rec['laterality'] or '-':3s}  {cx:8.1f}       {inferred}\")\nprint(f\"\\nstudy median center_x = {np.median(xs):.1f} mm  →  LEFT  (tag is also L, they agree)\")\nprint(\"This study is already a left knee: no sagittal reverse, no coronal/axial flip.\")\n\nfig, axes = plt.subplots(1, 2, figsize=(10.4, 5.2))\nshow(axes[0], window(axl[\"vol\"][ai]), title=\"this series (left knee) axial\")\ncallout(axes[0], (0.48, 0.18), \"patella (anterior)\", BLUE, (0.18, 0.10))\ncallout(axes[0], (0.22, 0.48), \"medial\", GREEN, (-0.10, 0.16), ha=\"right\")\ncallout(axes[0], (0.50, 0.78), \"posterior Baker region\", ORANGE, (0.05, -0.12))\nshow(axes[1], window(axl[\"vol\"][ai][:, ::-1]), title=\"if it were right: horizontal flip → left\")\ncallout(axes[1], (0.48, 0.18), \"patella still anterior\", BLUE, (0.18, 0.10))\ncallout(axes[1], (0.78, 0.48), \"medial follows the flip\", GREEN, (0.04, 0.16))\ncallout(axes[1], (0.50, 0.78), \"posterior stays\", ORANGE, (0.05, -0.12))\nfig.suptitle(\"After laterality norm the model always sees: patella anterior, patient-medial on the same side of the frame\",\n             fontsize=12)\nplt.tight_layout(); plt.show()\n"},{"cell_type":"markdown","id":"8cbe031a","metadata":{},"source":"## 8. Window and crop\n\n- **Per-stack** (not per-slice) 1–99.5% percentile window. Per-slice wipes the through-plane brightness of effusion — that distribution *is* the lesion.\n- Invert MONOCHROME1; apply RescaleSlope / Intercept first.\n- **130 mm constant physical center crop** (below 99.6% of train FOVs) → resize to 384, train-time 336.\n"},{"cell_type":"code","execution_count":16,"id":"d8fc49ae","metadata":{"execution":{"iopub.execute_input":"2026-08-23T11:09:37.835531Z","iopub.status.busy":"2026-08-23T11:09:37.835407Z","iopub.status.idle":"2026-08-23T11:09:38.471867Z","shell.execute_reply":"2026-08-23T11:09:38.471669Z"}},"outputs":[],"source":"vol = sag[\"vol\"]\nlo_s, hi_s = np.percentile(vol, [1.0, 99.5])\ni_edge, i_mid = 1, 12\nlo_e, hi_e = np.percentile(vol[i_edge], [1.0, 99.5])\nlo_m, hi_m = np.percentile(vol[i_mid], [1.0, 99.5])\n\ndef apply(x, lo, hi):\n    return np.clip((x - lo) / (hi - lo + 1e-6), 0, 1)\n\nfig, axes = plt.subplots(2, 2, figsize=(8.8, 8.8))\nshow(axes[0, 0], apply(vol[i_mid], lo_s, hi_s), title=\"mid-joint · per-stack window\")\ncallout(axes[0, 0], (0.42, 0.41), \"ACL / joint\", BLUE, (0.08, 0.18))\nshow(axes[0, 1], apply(vol[i_mid], lo_m, hi_m), title=\"mid-joint · per-slice window\")\ncallout(axes[0, 1], (0.42, 0.41), \"almost the same\", BLUE, (0.08, 0.18))\nshow(axes[1, 0], apply(vol[i_edge], lo_s, hi_s), title=\"far-lateral · per-stack window\")\ncallout(axes[1, 0], (0.50, 0.50), \"almost no knee, stays dark\", ORANGE, (0.04, 0.18))\nshow(axes[1, 1], apply(vol[i_edge], lo_e, hi_e), title=\"far-lateral · per-slice (stretched)\")\ncallout(axes[1, 1], (0.50, 0.50), \"empty slice stretched full\", RED, (0.04, 0.18))\nfig.suptitle(\"Per-slice stretching makes empty slices as bright as the joint — stack-level fluid contrast is gone\",\n             fontsize=12)\nplt.tight_layout(); plt.show()\nprint(f\"per-stack  [{lo_s:.0f}, {hi_s:.0f}]\")\nprint(f\"mid slice  [{lo_m:.0f}, {hi_m:.0f}]\")\nprint(f\"edge slice [{lo_e:.0f}, {hi_e:.0f}]   ← narrower dynamic range; its own window stretches it full\")\n\nh, w = vol[i_mid].shape\nch = int(round(CROP_MM / sag[\"ps_row\"]))\ncw = int(round(CROP_MM / sag[\"ps_col\"]))\ny0, x0 = (h - ch) // 2, (w - cw) // 2\nfig, ax = plt.subplots(figsize=(5.6, 5.6))\nshow(ax, window(vol[i_mid]), title=f\"130 mm center crop  ({h} px, spacing {sag['ps']:.3f} mm)\")\nax.add_patch(Rectangle((x0, y0), cw, ch, fill=False, ec=ORANGE, lw=2.0))\nax.text(x0 + 8, y0 + 24, f\"{CROP_MM:.0f} mm × {CROP_MM:.0f} mm\", color=ORANGE, fontsize=10,\n        bbox=dict(fc=\"white\", ec=ORANGE, pad=3, alpha=0.9))\ncallout(ax, (0.13, 0.20), \"patella\", BLUE, (0.14, 0.12))\ncallout(ax, (0.42, 0.41), \"ACL\", BLUE, (0.08, 0.16))\ncallout(ax, (0.56, 0.49), \"posterior horn\", ORANGE, (0.10, -0.16))\nscale_bar(ax, sag[\"ps\"])\nplt.tight_layout(); plt.show()\nprint(f\"crop window {ch}×{cw} px  out of {h}×{w}   \"\n      f\"(this series FOV ~ {h * sag['ps']:.0f} mm; 130 mm is the production constant, below 99.6% of train FOVs)\")\n"},{"cell_type":"markdown","id":"2e4b8a6a","metadata":{},"source":"## 9. From DICOM to model input: a checklist\n\nWe just walked the production data path on this study:\n\n| step | what happened here | production |\n|---|---|---|\n| geometry sort | ρ(filename, k) is tiny; Δk = SpacingBetweenSlices | `n = r×c`, `k = IPP·n` |\n| contrast | flag + TR/TE → FS / T1 / NOFS_FLUID; ignore SeriesDescription | `classify_weighting` + `series_cls` |\n| 6 slots | park what matches; empty → mask=0 | `SLOT_MAP` |\n| same-slot race | two sagittal non-FS series; higher-res PD wins, T2 loses | `n_slices` then `area` |\n| DROP | axial non-FS PD — there is no `AX_NOFS` | fill 0.19, slot stays closed |\n| laterality | tag=L, center_x>0, already a left knee | right: reverse sag / flip cor+ax |\n| 2.5D | 3 adjacent slices inside the plane band | `BANDS` |\n| window / crop | per-stack 1–99.5%; 130 mm center crop | cache builder |\n\n**Do not:** sort by filename, read contrast from SeriesDescription, backfill empty slots with the wrong contrast, flip as augmentation, window per-slice, or tune on the 58 gold studies.\n\nNot in this EDA (the rest of a 0.90+ pipeline): 5-teacher pseudo-labels with disagreement weighting, 6-slot 2.5D DINOv2, epoch selection on derived high-confidence AUC — never on gold-58.\n"}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.2"}},"nbformat":4,"nbformat_minor":5}