Incidentaloma follow-up checker for kidney and adrenal lesions on CT
AI · Radiology · Renal cell carcinoma · Adrenal incidentaloma · Python/Streamlit
Live demo: https://shaminiramkumart.github.io/incidentaloma-navigator/web/ (runs in the browser, no install)
Result: on 16 held-out reports written in new phrasing, the regex baseline caught every discordant recommendation (6/6, including both high-severity under-calls), but overall flag agreement fell from 100% on the dev set to 59%. Details below.
Why?
What?
How?
| Dev (30 reports, 32 lesions) | Held-out (16 reports, 17 lesions) | |
|---|---|---|
| Lesion detection recall | 100% | 76% |
| Guideline category agreement | 97% | 53% |
| Concordance flag agreement | 100% | 59% |
| Discordances caught (sensitivity) | 100% | 100% |
| Flags that were real (PPV) | 100% | 67% |
| High-severity under-calls caught | 5/5 | 2/2 |
What this shows: the regex rules look perfect on the reports they were tuned on and break on new phrasing: sizes in mm, “Hounsfield units”, “HU 30 (precontrast)”, “2.9 x 2.5 cm”, two lesions in one sentence, “nodular enhancing soft tissue component”. This is why I also wrote an AI reader (navigator/extract_llm.py) that uses Claude to read the report instead of keyword rules. It fills in the same fields and is scored the same way, and I kept its instructions general so it doesn’t know the test phrasings. I haven’t run it yet because it needs a paid API key.
What I’d change first: all 7 held-out misses come from the reader, not the guideline rules (4 lesions not detected, 3 misread). So I’d normalise units and size formats before extraction (mm to cm, “Hounsfield units” to HU, “2.9 x 2.5 cm” to longest diameter), split sentences that describe two lesions, and then score the Claude extractor on the same 16 reports, with a fresh held-out set afterwards so I’m not tuning on this one.
To see exactly where the reader went wrong, open results/eval_rules_holdout.csv. It has one row per lesion in the 16 test reports, with the correct answer, what the reader produced and which details it got wrong (for example, report H02: size read as 2.5 cm instead of 2.9 cm). The same table appears as “Error log” in the Validation dashboard tab of the Streamlit app (streamlit run app.py). The browser demo does not include it.
| Organ | Feature | Category | Action |
|---|---|---|---|
| Kidney | Macroscopic fat | Angiomyolipoma | None (<4 cm), urology review (≥4 cm) |
| Kidney | Cystic + enhancing nodule | Bosniak IV | Urology referral |
| Kidney | Wall/septa ≥4 mm or irregular | Bosniak III | Urology referral |
| Kidney | Wall/septa 3 mm or ≥4 thin septa | Bosniak IIF | Imaging at 6 and 12 months, then yearly to 5 years |
| Kidney | 1-3 thin septa; ≥70 HU unenhanced; 21-30 HU portal venous; too small to characterize | Bosniak II | None |
| Kidney | Simple fluid, thin wall, no septa | Bosniak I | None |
| Kidney | Solid, enhancement ≥20 HU, no fat | Suspicious for RCC | Urology referral (<1 cm: imaging follow-up) |
| Kidney | Anything else | Indeterminate | Renal mass protocol CT or MRI |
| Adrenal | <1 cm | Below threshold | None |
| Adrenal | Homogeneous ≤10 HU unenhanced | Lipid-rich adenoma | No more imaging; 1 mg dexamethasone suppression test |
| Adrenal | ≥4 cm or heterogeneous (not benign) | Indeterminate, high risk | MDT/surgical review + hormonal work-up |
| Adrenal | >10 HU or no unenhanced phase | Indeterminate | Further imaging or repeat in 6-12 months + hormonal work-up |
Sources:
web/index.html is a self-contained version of the checker that runs in any browser, with no install and no API key. Open the file directly, or serve the repository with GitHub Pages. It has three tabs: check a pasted report, a worklist of all 46 sample reports sorted by severity, and the guideline tables. web/engine.js is a JavaScript port of the regex reader and guideline engine. web/parity_check.js compares it against the Python version, and it matched on every sample report.
pip install -r requirements.txt
python data/build_reports.py && (cd data && python build_holdout.py) # regenerate datasets
python run_eval.py rules all # score the regex baseline
ANTHROPIC_API_KEY=... python run_eval.py llm all # score the AI reader (needs an API key)
python -m pytest -q tests # 53 guideline tests
streamlit run app.py
navigator/schema.py Finding model + action vocabulary
navigator/guidelines.py Bosniak / ACR / ESE rules engine + concordance check
navigator/extract_rules.py Regex baseline extractor
navigator/extract_llm.py Claude structured-output extractor
navigator/evaluate.py Matching + three-level scoring
data/ Synthetic reports and gold labels (dev + held-out)
results/ Per-lesion CSVs and summary JSON per run
app.py Streamlit app (report checker, validation dashboard, guideline tables)
web/ Browser demo (index.html) and its JavaScript engine
Built by Shamini Ramkumar T, 2026. Code and synthetic reports are released under the MIT license. See LICENSE.