Structuring radiology reports
A radiology report is prose, but its meaning is structured: a technique, a
comparison, a list of findings (each with anatomy, laterality, and a
measurement), and an impression that may carry an assessment category
(BI-RADS, Lung-RADS) and a follow-up recommendation. This skill turns the
narrative into that structure so findings are trackable — especially
incidental findings that need downstream follow-up.
OpenMed extracts the anatomy, disease/finding, and measurement spans on-device;
this skill organizes them into sectioned, coded findings. It is
decision-support, not a diagnostic device — every structured finding must be
attributable back to its source sentence for radiologist review.
When to use
- You have a CT/MRI/X-ray/US/mammography report and need
{technique, comparison, findings[], impression} with measurements and
laterality.
- You must capture BI-RADS (breast) or Lung-RADS (lung screening)
assessment categories and the recommended action.
- You need to track incidental findings and the follow-up interval/modality
the report recommends.
- You are mapping findings toward RadLex terms or a DICOM-SR structured
report.
Quick start
import openmed
report = (
"TECHNIQUE: CT chest without contrast.\n"
"COMPARISON: CT 2023-11-02.\n"
"FINDINGS: A 8 mm solid nodule is noted in the right upper lobe, "
"unchanged. No pleural effusion.\n"
"IMPRESSION: 8 mm right upper lobe nodule, stable. Lung-RADS 2. "
"Recommend annual low-dose CT screening."
)
# 1) De-identify the report on-device first (synthetic example shown).
deid = openmed.deidentify(report, policy="hipaa_safe_harbor")
text = deid.deidentified_text
# 2) Run NER for anatomy / finding / measurement spans.
ents = openmed.analyze_text(
text,
model_name="anatomy_detection_superclinical", # Anatomy category
output_format="dict",
)["entities"]
# 3) Split sections by header, then attach entities + measurements per finding.
import re
SECTION = re.compile(r"(?im)^(TECHNIQUE|COMPARISON|FINDINGS|IMPRESSION)\s*:")
sections, last, name = {}, 0, None
for m in SECTION.finditer(text):
if name: sections[name] = text[last:m.start()].strip()
name, last = m.group(1).upper(), m.end()
if name: sections[name] = text[last:].strip()
structured = {
"technique": sections.get("TECHNIQUE"),
"comparison": sections.get("COMPARISON"),
"findings": _split_findings(sections.get("FINDINGS", "")), # one per sentence
"impression": sections.get("IMPRESSION"),
"measurements": re.findall(r"\b\d+(?:\.\d+)?\s?(?:mm|cm)\b", text),
"laterality": sorted({w for w in ("right", "left", "bilateral")
if re.search(rf"\b{w}\b", text, re.I)}),
"assessment": (re.search(r"\b(?:BI-RADS|Lung-RADS)\s*\d[A-C]?\b", text, re.I)
or [None])[0] if re.search(r"RADS", text, re.I) else None,
"follow_up": _extract_followup(sections.get("IMPRESSION", "")),
}
_split_findings / _extract_followup are your sentence splitter and a
recommendation matcher ("recommend …", "follow-up in N months"); keep each
finding tied to its source sentence offsets.
Workflow
- De-identify first.
openmed.deidentify(report, policy=...); structure
from deidentified_text. Patient name, MRN, accession, and dates go before
anything is stored or shared.
- Split sections by the standard headers (TECHNIQUE, COMPARISON, FINDINGS,
IMPRESSION; also HISTORY/INDICATION). Reports vary — fall back to position if
headers are missing.
- Run
analyze_text for anatomy and finding entities; capture measurements
("8 mm", "1.2 cm") and laterality ("right", "left", "bilateral") near each
finding.
- Build one structured finding per observation:
{anatomy, finding, laterality, measurement, change_vs_prior, source_offsets}. "Unchanged",
"stable", "increased", "new" capture temporal change against the comparison.
- Pull the assessment category (BI-RADS 0-6, Lung-RADS 1-4X) from the
impression and the recommended follow-up (modality + interval).
- Flag incidental findings — findings unrelated to the exam indication — and
route them to a follow-up tracker so they aren't lost.
- Map toward RadLex / DICOM-SR if you need coded interoperability, and
surface the whole structure to a radiologist for verification.
Hand-off to / from OpenMed
OpenMed's analyze_text returns a dict; result["entities"] items carry
text, label, confidence, start, end.
- From
extracting-clinical-entities: Anatomy and Disease/finding entities
populate each structured finding; keep offsets so every field traces to a
source sentence.
- From
extracting-lab-tables / OCR: if the report is a scan, OCR it first
(openmed.multimodal.ocr.ocr), then run NER on the recognized text.
- From
segmenting-clinical-sections: reuse section detection if your reports
don't use canonical headers.
- To
building-patient-timelines: dated findings + change-vs-prior feed a
longitudinal view (e.g. nodule size over time).
- To
extracting-dicom-metadata: pair the structured findings with the
study's DICOM metadata when assembling a DICOM-SR object.
- De-identify with
deidentifying-clinical-text (openmed.deidentify)
before any export. Everything runs on-device.
Edge cases & gotchas
- Negation and uncertainty change meaning. "No pleural effusion" and "cannot
exclude metastasis" are findings about absence/uncertainty — don't record
them as positive findings. Use
resolving-clinical-context
(openmed.clinical) for negation/hedging before asserting a finding.
- Laterality errors are clinically dangerous. "Right" vs "left" must bind to
the correct finding; a misattributed side can drive wrong-site decisions. Tie
laterality to the nearest anatomy span by offset, not document-wide.
- Measurements need their unit and axis. "8 mm" vs "0.8 cm" are equal; a
bare "8" is ambiguous. Capture the unit; for masses, capture all reported
dimensions ("2.1 x 1.4 cm"), not just the first.
- Assessment categories have controlled value sets. BI-RADS 0-6 and
Lung-RADS 1, 2, 3, 4A, 4B, 4X each map to a defined management action — don't
invent or round categories; pull the literal value from the impression.
- Incidental findings get lost. A renal cyst mentioned in a chest CT is the
classic missed follow-up. Explicitly separate incidental from
indication-related findings and push incidentals to a tracker.
- The impression is the actionable summary, but findings may contain detail
the impression omits — structure both, and prefer the impression for
follow-up/assessment.
- Decision-support disclaimer. This is not a diagnostic medical device; it
organizes text a radiologist authored. Every structured field must be reviewable
against its source. Do not auto-act on a derived category or follow-up without
clinician sign-off.
Standards & references
1---2name: structuring-radiology-reports3description: Converts free-text radiology narratives into structured findings and impression — with measurements, laterality, anatomy, and follow-up recommendations — after OpenMed NER. Use when the user has a CT/MRI/X-ray/ultrasound/mammography report and needs the sections split (technique, comparison, findings, impression), lesion measurements and laterality captured, BI-RADS / Lung-RADS assessment categories pulled, or incidental findings and recommended follow-up tracked. Trigger keywords: radiology report, findings, impression, RadLex, DICOM-SR, BI-RADS, Lung-RADS, ACR, laterality, measurement, nodule, incidental finding, follow-up, structured reporting. Pairs after OpenMed: run openmed.analyze_text on the report (Anatomy/Disease/measurement entities), then assemble structured findings. De-identify the report first. Decision-support only — not a diagnostic medical device.4license: Apache-2.05---67# Structuring radiology reports89A radiology report is prose, but its *meaning* is structured: a **technique**, a10**comparison**, a list of **findings** (each with anatomy, laterality, and a11measurement), and an **impression** that may carry an **assessment category**12(BI-RADS, Lung-RADS) and a **follow-up recommendation**. This skill turns the13narrative into that structure so findings are trackable — especially14**incidental findings** that need downstream follow-up.1516OpenMed extracts the anatomy, disease/finding, and measurement spans on-device;17this skill organizes them into sectioned, coded findings. It is18**decision-support, not a diagnostic device** — every structured finding must be19attributable back to its source sentence for radiologist review.2021## When to use2223- You have a CT/MRI/X-ray/US/mammography report and need24 `{technique, comparison, findings[], impression}` with measurements and25 laterality.26- You must capture **BI-RADS** (breast) or **Lung-RADS** (lung screening)27 assessment categories and the recommended action.28- You need to **track incidental findings** and the follow-up interval/modality29 the report recommends.30- You are mapping findings toward **RadLex** terms or a **DICOM-SR** structured31 report.3233## Quick start3435```python36import openmed3738report = (39 "TECHNIQUE: CT chest without contrast.\n"40 "COMPARISON: CT 2023-11-02.\n"41 "FINDINGS: A 8 mm solid nodule is noted in the right upper lobe, "42 "unchanged. No pleural effusion.\n"43 "IMPRESSION: 8 mm right upper lobe nodule, stable. Lung-RADS 2. "44 "Recommend annual low-dose CT screening."45)4647# 1) De-identify the report on-device first (synthetic example shown).48deid = openmed.deidentify(report, policy="hipaa_safe_harbor")49text = deid.deidentified_text5051# 2) Run NER for anatomy / finding / measurement spans.52ents = openmed.analyze_text(53 text,54 model_name="anatomy_detection_superclinical", # Anatomy category55 output_format="dict",56)["entities"]5758# 3) Split sections by header, then attach entities + measurements per finding.59import re60SECTION = re.compile(r"(?im)^(TECHNIQUE|COMPARISON|FINDINGS|IMPRESSION)\s*:")61sections, last, name = {}, 0, None62for m in SECTION.finditer(text):63 if name: sections[name] = text[last:m.start()].strip()64 name, last = m.group(1).upper(), m.end()65if name: sections[name] = text[last:].strip()6667structured = {68 "technique": sections.get("TECHNIQUE"),69 "comparison": sections.get("COMPARISON"),70 "findings": _split_findings(sections.get("FINDINGS", "")), # one per sentence71 "impression": sections.get("IMPRESSION"),72 "measurements": re.findall(r"\b\d+(?:\.\d+)?\s?(?:mm|cm)\b", text),73 "laterality": sorted({w for w in ("right", "left", "bilateral")74 if re.search(rf"\b{w}\b", text, re.I)}),75 "assessment": (re.search(r"\b(?:BI-RADS|Lung-RADS)\s*\d[A-C]?\b", text, re.I)76 or [None])[0] if re.search(r"RADS", text, re.I) else None,77 "follow_up": _extract_followup(sections.get("IMPRESSION", "")),78}79```8081`_split_findings` / `_extract_followup` are your sentence splitter and a82recommendation matcher ("recommend …", "follow-up in N months"); keep each83finding tied to its source sentence offsets.8485## Workflow86871. **De-identify first.** `openmed.deidentify(report, policy=...)`; structure88 from `deidentified_text`. Patient name, MRN, accession, and dates go before89 anything is stored or shared.902. **Split sections** by the standard headers (TECHNIQUE, COMPARISON, FINDINGS,91 IMPRESSION; also HISTORY/INDICATION). Reports vary — fall back to position if92 headers are missing.933. **Run `analyze_text`** for anatomy and finding entities; capture measurements94 ("8 mm", "1.2 cm") and laterality ("right", "left", "bilateral") near each95 finding.964. **Build one structured finding per observation**: `{anatomy, finding,97 laterality, measurement, change_vs_prior, source_offsets}`. "Unchanged",98 "stable", "increased", "new" capture temporal change against the comparison.995. **Pull the assessment category** (BI-RADS 0-6, Lung-RADS 1-4X) from the100 impression and the **recommended follow-up** (modality + interval).1016. **Flag incidental findings** — findings unrelated to the exam indication — and102 route them to a follow-up tracker so they aren't lost.1037. **Map toward RadLex / DICOM-SR** if you need coded interoperability, and104 surface the whole structure to a radiologist for verification.105106## Hand-off to / from OpenMed107108OpenMed's `analyze_text` returns a `dict`; `result["entities"]` items carry109`text`, `label`, `confidence`, `start`, `end`.110111- **From** `extracting-clinical-entities`: Anatomy and Disease/finding entities112 populate each structured finding; keep offsets so every field traces to a113 source sentence.114- **From** `extracting-lab-tables` / OCR: if the report is a scan, OCR it first115 (`openmed.multimodal.ocr.ocr`), then run NER on the recognized text.116- **From** `segmenting-clinical-sections`: reuse section detection if your reports117 don't use canonical headers.118- **To** `building-patient-timelines`: dated findings + change-vs-prior feed a119 longitudinal view (e.g. nodule size over time).120- **To** `extracting-dicom-metadata`: pair the structured findings with the121 study's DICOM metadata when assembling a DICOM-SR object.122- **De-identify** with `deidentifying-clinical-text` (`openmed.deidentify`)123 before any export. Everything runs **on-device**.124125## Edge cases & gotchas126127- **Negation and uncertainty change meaning.** "No pleural effusion" and "cannot128 exclude metastasis" are findings *about* absence/uncertainty — don't record129 them as positive findings. Use `resolving-clinical-context`130 (`openmed.clinical`) for negation/hedging before asserting a finding.131- **Laterality errors are clinically dangerous.** "Right" vs "left" must bind to132 the correct finding; a misattributed side can drive wrong-site decisions. Tie133 laterality to the nearest anatomy span by offset, not document-wide.134- **Measurements need their unit and axis.** "8 mm" vs "0.8 cm" are equal; a135 bare "8" is ambiguous. Capture the unit; for masses, capture all reported136 dimensions ("2.1 x 1.4 cm"), not just the first.137- **Assessment categories have controlled value sets.** BI-RADS 0-6 and138 Lung-RADS 1, 2, 3, 4A, 4B, 4X each map to a defined management action — don't139 invent or round categories; pull the literal value from the impression.140- **Incidental findings get lost.** A renal cyst mentioned in a chest CT is the141 classic missed follow-up. Explicitly separate incidental from142 indication-related findings and push incidentals to a tracker.143- **The impression is the actionable summary**, but findings may contain detail144 the impression omits — structure both, and prefer the impression for145 follow-up/assessment.146- **Decision-support disclaimer.** This is not a diagnostic medical device; it147 organizes text a radiologist authored. Every structured field must be reviewable148 against its source. Do not auto-act on a derived category or follow-up without149 clinician sign-off.150151## Standards & references152153- RadLex (RSNA radiology lexicon): https://radlex.org/154- DICOM Structured Reporting (PS3.16 templates): https://www.dicomstandard.org/155- ACR BI-RADS Atlas: https://www.acr.org/Clinical-Resources/Reporting-and-Data-Systems/Bi-Rads156- ACR Lung-RADS: https://www.acr.org/Clinical-Resources/Reporting-and-Data-Systems/Lung-Rads157- RSNA Radiology Reporting templates: https://www.rsna.org/practice-tools/data-tools-and-standards/radreport-template-library158- ACR Incidental Findings white papers: https://www.acr.org/Clinical-Resources/Incidental-Findings159- HL7 FHIR R4 DiagnosticReport (imaging): https://hl7.org/fhir/R4/diagnosticreport.html