Detects and extracts tabular laboratory panels from PDFs, scans, and images into structured rows ready for OpenMed and FHIR. Use when the user has a CBC, CMP, lipid panel, or other lab report as a scanned image / PDF / spreadsheet and needs the test name, value, unit, reference range, and abnormal flag as clean rows. Trigger keywords: lab table extraction, lab panel, OCR labs, table detection, layout analysis, header detection, reference range column, abnormal flag column, LOINC, UCUM, CBC, CMP, structured labs. Pairs before OpenMed: OCR/parse the table on-device (openmed.multimodal.ocr.ocr, read_table), de-identify embedded PHI with openmed.deidentify, then hand structured rows to LOINC/UCUM mapping and openmed.clinical lab flagging. Image/CSV/TSV intake is supported; PDF/DOCX raise UnsupportedDocumentError — render those to images or text first.
74
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Lab results arrive as tables: a column of test names, a value column, units, a reference range, and an abnormal flag (H/L/Crit). To use them downstream you must recover that grid from a PDF, scan, or spreadsheet into clean rows — then code each test to LOINC, normalize units with UCUM, and flag abnormals.
This skill is the intake step: it OCRs/parses the table on-device with
openmed.multimodal, de-identifies any embedded PHI, and emits structured rows.
It pairs before OpenMed's clinical helpers — the LOINC/UCUM coding and the
high/low/critical flag are downstream (see parsing-lab-values).
Observation/DiagnosticReport.openmed.multimodal ships the intake primitives (no heavy deps at import; the
OCR backend loads lazily):
openmed.multimodal.ocr.ocr(image, engine=...) → an OcrResult whose .words
are OcrWord(text, bbox, confidence, page) and .text is the joined string.
OcrResult.to_document() bridges each word (with its pixel bbox) into an
ExtractedDocument so detected PHI can project back to the source location.read_table(...) → a TableView (headers, rows, delimiter,
has_header, columns) for delimited text; classify_columns(...) labels
each column; redact_table(...) → a RedactedTable with a PHI-safe manifest.Engines: Tesseract (pip install "openmed[multimodal]" + the system binary) or
PaddleOCR (pip install "openmed[ocr-paddle]"). ocr() auto-selects the first
installed backend.
from openmed.multimodal.ocr import ocr
from openmed.multimodal import read_table, classify_columns, redact_table
# A) Scanned / image lab report -> words with pixel boxes.
result = ocr("cbc_report.png") # OcrResult
for w in result.words[:5]:
print(repr(w.text), w.bbox, round(w.confidence, 2), "p", w.page)
doc = result.to_document() # ExtractedDocument; bbox preserved
# B) Delimited lab export (CSV/TSV) -> classified, PHI-redacted rows.
csv_text = (
"PatientName,Test,Value,Unit,RefRange,Flag\n"
"Jane Roe,Hemoglobin,9.1,g/dL,12.0-15.5,L\n"
"Jane Roe,Glucose,148,mg/dL,70-99,H\n"
)
view = read_table(csv_text) # TableView
view = classify_columns(view) # tag PHI vs data columns
redacted = redact_table(view) # RedactedTable: PatientName redacted
for row in redacted.rows:
print(row) # name column masked; lab data intact
for col in redacted.manifest: # PHI-safe per-column audit manifest
print(col["column_name"], col["assigned_class"], col["action"])For an OCR'd (image) table, you reconstruct the grid yourself from word boxes (next section) — OCR yields positioned words, not a delimited table.
read_table. Image/scan → ocr().
PDF/DOCX are not directly parseable (they raise UnsupportedDocumentError);
render PDF pages to images first, or extract their text layer, then OCR.ocr() returns OcrWords carrying bbox and page.
Keep the boxes — they let you cluster words into rows/columns and project PHI
redaction back to pixels.bbox y into rows, by x
into columns. The header row names the columns; align body cells to those x
bands. Confidence (OcrWord.confidence) flags shaky cells for review.classify_columns
tags PHI columns (name/MRN/DOB) so redact_table masks them.redact_table) and run free-text
cells through openmed.deidentify before the rows leave the device.{test, value, unit, ref_range, flag} per result and
hand off to LOINC/UCUM coding and parsing-lab-values.parsing-lab-values (openmed.clinical.parse_reference_range,
derive_abnormal_flag): pass the parsed value + ref_range (+ any explicit
lab flag) to get a structured low/normal/high/critical signal.mapping-loinc: code each test name to a LOINC code; normalize the unit
with UCUM. OpenMed emits the row; the terminology binding is out-of-process.exporting-to-fhir): each row becomes an Observation
(code=LOINC, valueQuantity with UCUM unit, referenceRange,
interpretation) grouped under a DiagnosticReport.deidentifying-clinical-text (openmed.deidentify)
before export. OCR words carry pixel boxes so redaction maps back to the image.UnsupportedDocumentError. The multimodal dispatcher has no
PDF/DOCX handler — rasterize PDF pages to PNG (or pull the text layer) before
calling ocr(). Image formats (PNG/JPG/TIFF/…) and CSV/TSV are handled.bbox geometry. Multi-line cells, wrapped test names, and merged header cells
break naive x/y bucketing — tune the clustering tolerance per template.parse_reference_range downstream
expects it whole.OcrWord.confidence; a 0.4-confidence value
in a lab table is a patient-safety risk — route it to human review, don't
silently accept it.ocr() raises a clear MissingDependencyError if no
backend is installed — install Tesseract or PaddleOCR per the extras.valueQuantity, referenceRange, interpretation): https://hl7.org/fhir/R4/observation.htmlopenmed/multimodal/ocr.py, openmed/multimodal/tabular_csv.py.80da98c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.