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.
{technique, comparison, findings[], impression} with measurements and
laterality.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.
openmed.deidentify(report, policy=...); structure
from deidentified_text. Patient name, MRN, accession, and dates go before
anything is stored or shared.analyze_text for anatomy and finding entities; capture measurements
("8 mm", "1.2 cm") and laterality ("right", "left", "bilateral") near each
finding.{anatomy, finding, laterality, measurement, change_vs_prior, source_offsets}. "Unchanged",
"stable", "increased", "new" capture temporal change against the comparison.OpenMed's analyze_text returns a dict; result["entities"] items carry
text, label, confidence, start, end.
extracting-clinical-entities: Anatomy and Disease/finding entities
populate each structured finding; keep offsets so every field traces to a
source sentence.extracting-lab-tables / OCR: if the report is a scan, OCR it first
(openmed.multimodal.ocr.ocr), then run NER on the recognized text.segmenting-clinical-sections: reuse section detection if your reports
don't use canonical headers.building-patient-timelines: dated findings + change-vs-prior feed a
longitudinal view (e.g. nodule size over time).extracting-dicom-metadata: pair the structured findings with the
study's DICOM metadata when assembling a DICOM-SR object.deidentifying-clinical-text (openmed.deidentify)
before any export. Everything runs on-device.resolving-clinical-context
(openmed.clinical) for negation/hedging before asserting a finding.